Home>Article>Backend Development> When did the go language appear?
The go language is an open source programming language released by Google in 2009. The Go language was originally designed and invented by Robert Griesemer, Ken Thompson and Rob Pike of Google in 2007, and was officially released in 2009; the ultimate goal of the three founders is to design a language that adapts to the network and multi-core era. The C language, so the Go language is often described as a "C-like language" or the "C language of the 21st century". Go inherits similar syntax, programming ideas, etc. from C.
The operating environment of this tutorial: Windows 7 system, GO version 1.18, Dell G3 computer.
Before learning the basic grammar of go language in detail, let’s understand the timing and timing of the emergence of go language. its characteristics.
The Go language was originally designed and invented by Robert Griesemer, Ken Thompson and Rob Pike of Google in 2007. It was open sourced in November 2009 and was initially used as a 20% project within Google. run.
Go language originated in 2007 and was officially released in 2009. It started as a 20% part-time project at Google on September 21, 2009, that is, relevant employees use 20% of their free time to participate in the research and development of the Go language.
The ultimate goal of the three founders is to design aC language that adapts to the network and multi-core era, so the Go language is often described as a "C-like language", or It is the "C language of the 21st century". Of course, from various perspectives, Go language does inherit similar expression syntax, control flow structure, basic data types, call parameter value transfer, pointers and many other programming ideas from C language. However, the Go language is the most complete abandonment of the C language. It abandons the flexible but dangerous pointer arithmetic in the C language, and redesigns the priorities of some unreasonable operators in the C language, and in many subtle places Necessary polishing and changes have been made.
In this part we just use " hello world" program to introduce to you the basic composition of programs written in the Go language.
package main import "fmt" func main() { // 终端输出hello world fmt.Println("Hello world!") }
Similar to C language, the basic components of go language are:
package main
.import "fmt"
actually introduces fmt Bag.It should be noted that:Identifiersare used to name program entities such as variables and types. An identifier is actually a sequence of one or more letters, numbers, and underscores, but the first character must be a letter or underscore and not a number.
When the identifier (including constants, variables, types, function names, structure fields, etc.) starts with an uppercase letter, such as: Group1, then use this form of identifier The object can be used by the code of the external package (the client program needs to import this package first), which is called export (like public in object-oriented languages);
identifier If they start with a lowercase letter, they are not visible outside the package, but they are visible and available inside the entire package (like protected in object-oriented languages).
In the Go programming language, data types are used to declare functions and variables.
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。具体分类如下:
类型 | 详解 |
---|---|
布尔型 | 布尔型的值只可以是常量 true 或者 false。 |
数字类型 | 整型 int 和浮点型 float。Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
字符串类型 | 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
派生类型 | (a) 指针类型(Pointer)(b) 数组类型© 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型 |
声明变量的一般形式是使用 var 关键字,具体格式为:var identifier typename
。如下的代码中我们定义了一个类型为int的变量。
package main import "fmt" func main() { var a int = 27 fmt.Println(a); }
在go语言中定义了一个变量,指定变量类型,如果没有初始化,则变量默认为零值。零值就是变量没有做初始化时系统默认设置的值。
类型 | 零值 |
---|---|
数值类型 | 0 |
布尔类型 | false |
字符串 | “”(空字符串) |
在go语言中如果没有指定变量类型,可以通过变量的初始值来判断变量类型。如下代码
package main import "fmt" func main() { var d = true fmt.Println(d) }
当我们定义一个变量后又使用该符号初始化变量,就会产生编译错误,因为该符号其实是一个声明语句。
使用格式:typename := value
也就是说intVal := 1
相等于:
var intVal int intVal =1
可以同时声明多个类型相同的变量(非全局变量),如下图所示:
var x, y int var c, d int = 1, 2 g, h := 123, "hello"
关于全局变量的声明如下:var ( vname1 v_type1 vname2 v_type2 )
具体举例如下:
var ( a int b bool )
匿名变量的特点是一个下画线_
,这本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。
使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。
示例代码如下:
func GetData() (int, int) { return 10, 20 } func main(){ a, _ := GetData() _, b := GetData() fmt.Println(a, b) }
需要注意的是匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
作用域指的是已声明的标识符所表示的常量、类型、函数或者包在源代码中的作用范围,在此我们主要看一下go中变量的作用域,根据变量定义位置的不同,可以分为一下三个类型:
函数内定义的变量为局部变量,这种局部变量的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。这种变量在存在于函数被调用时,销毁于函数调用结束后。
函数外定义的变量为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,甚至可以使用import引入外部包来使用。全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
函数定义中的变量成为形式参数,定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。形式参数会作为函数的局部变量来使用。
类型 | 描述 |
---|---|
uint8 / uint16 / uint32 / uint64 | 无符号 8 / 16 / 32 / 64位整型 |
int8 / int16 / int32 / int64 | 有符号8 / 16 / 32 / 64位整型 |
float32 / float64 | IEEE-754 32 / 64 位浮点型数 |
complex64 / complex128 | 32 / 64 位实数和虚数 |
byte | 类似 uint8 |
rune | 类似 int32 |
uintptr | 无符号整型,用于存放一个指针 |
以上就是go语言基本的数据类型,有了数据类型,我们就可以使用这些类型来定义变量,Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
与C相同,Go语言让程序员决定何时使用指针。变量其实是一种使用方便的占位符,用于引用计算机内存地址。Go 语言中的的取地址符是,放到一个变量前使用就会返回相应变量的内存地址。
指针变量其实就是用于存放某一个对象的内存地址。
和基础类型数据相同,在使用指针变量之前我们首先需要申明指针,声明格式如下:var var_name *var-type
,其中的var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
代码举例如下:
var ip *int /* 指向整型*/ var fp *float32 /* 指向浮点型 */
指针的初始化就是取出相对应的变量地址对指针进行赋值,具体如下:
var a int= 20 /* 声明实际变量 */ var ip *int /* 声明指针变量 */ ip = &a /* 指针变量的存储地址 */
当一个指针被定义后没有分配到任何变量时,它的值为nil,也称为空指针。它概念上和其它语言的null、NULL一样,都指代零值或空值。
和c语言相同,Go语言也提供了数组类型的数据结构,数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var variable_name [SIZE] variable_type
以上就可以定一个一维数组,我们举例代码如下:
var balance [10] float32
数组的初始化方式有不止一种方式,我们列举如下:
直接进行初始化:var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
通过字面量在声明数组的同时快速初始化数组:balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
数组长度不确定,编译器通过元素个数自行推断数组长度,在[ ]中填入,举例如下:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
和balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
数组长度确定,指定下标进行部分初始化:balanced := [5]float32(1:2.0, 3:7.0)
注意:
- 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小。
在c语言中我们知道数组名在本质上是数组中第一个元素的地址,而在go语言中,数组名仅仅表示整个数组,是一个完整的值,一个数组变量即是表示整个数组。
所以在go中一个数组变量被赋值或者被传递的时候实际上就会复制整个数组。如果数组比较大的话,这种复制往往会占有很大的开销。所以为了避免这种开销,往往需要传递一个指向数组的指针,这个数组指针并不是数组。关于数组指针具体在指针的部分深入的了解。
通过数组和指针的知识我们就可以定义一个数组指针,代码如下:
var a = [...]int{1, 2, 3} // a 是一个数组 var b = &a // b 是指向数组的指针
数组指针除了可以防止数组作为参数传递的时候浪费空间,还可以利用其和for range
来遍历数组,具体代码如下:
for i, v := range b { // 通过数组指针迭代数组的元素 fmt.Println(i, v) }
具体关于go语言的循环语句我们在后文中再进行详细介绍。
通过上述数组的学习,我们就可以直接定义多个同类型的变量,但这往往也是一种限制,只能存储同一种类型的数据,而我们在结构体中就可以定义多个不同的数据类型。
在声明结构体之前我们首先需要定义一个结构体类型,这需要使用type和struct,type用于设定结构体的名称,struct用于定义一个新的数据类型。具体结构如下:
type struct_variable_type struct { member definition member definition ... member definition }
定义好了结构体类型,我们就可以使用该结构体声明这样一个结构体变量,语法如下:
variable_name := structure_variable_type {value1, value2...valuen} variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
如果要访问结构体成员,需要使用点号.
操作符,格式为:结构体变量名.成员名
。举例代码如下:
package main import "fmt" type Books struct { title string author string } func main() { var book1 Books Book1.title = "Go 语言入门" Book1.author = "mars.hao" }
关于结构体指针的定义和申明同样可以套用前文中讲到的指针的相关定义,从而使用一个指针变量存放一个结构体变量的地址。
定义一个结构体变量的语法:var struct_pointer *Books
。
这种指针变量的初始化和上文指针部分的初始化方式相同struct_pointer = &Book1
,但是和c语言中有所不同,使用结构体指针访问结构体成员仍然使用.
操作符。格式如下:struct_pointer.title
一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。
Go语言字符串的底层结构在reflect.StringHeader中定义,具体如下:
type StringHeader struct { Data uintptr Len int }
也就是说字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。
字符串其实是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制,所以我们也可以将字符串数组看作一个结构体数组。
字符串和数组类似,内置的len函数返回字符串的长度。
根据Go语言规范,Go语言的源文件都是采用UTF8编码。因此,Go源文件中出现的字符串面值常量一般也是UTF8编码的(对于转义字符,则没有这个限制)。提到Go字符串时,我们一般都会假设字符串对应的是一个合法的UTF8编码的字符序列。
Go语言的字符串中可以存放任意的二进制字节序列,而且即使是UTF8字符序列也可能会遇到坏的编码。如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符‘\uFFFD’,这个字符在不同的软件中的显示效果可能不太一样,在印刷中这个符号通常是一个黑色六角形或钻石形状,里面包含一个白色的问号‘�’。
下面的字符串中,我们故意损坏了第一字符的第二和第三字节,因此第一字符将会打印为“�”,第二和第三字节则被忽略;后面的“abc”依然可以正常解码打印(错误编码不会向后扩散是UTF8编码的优秀特性之一)。代码如下:
fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") // �界abc
不过在for range迭代这个含有损坏的UTF8字符串时,第一字符的第二和第三字节依然会被单独迭代到,不过此时迭代的值是损坏后的0:
// 0 65533 // \uFFFD, 对应 � // 1 0 // 空字符 // 2 0 // 空字符 // 3 30028 // 界 // 6 97 // a // 7 98 // b // 8 99 // c
在上文中我们知道源代码往往会采用UTF8编码,如果不想解码UTF8字符串,想直接遍历原始的字节码:
可以将字符串强制转为[]byte字节序列后再行遍历(这里的转换一般不会产生运行时开销):
采用传统的下标方式遍历字符串的字节数组
除此以外,字符串相关的强制类型转换主要涉及到[]byte和[]rune两种类型。每个转换都可能隐含重新分配内存的代价,最坏的情况下它们的运算时间复杂度都是O(n)。
不过字符串和[]rune的转换要更为特殊一些,因为一般这种强制类型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应的[]byte和[]int32类型是完全不同的内部布局,因此这种转换可能隐含重新分配内存的操作。
简单地说,切片就是一种简化版的动态数组。因为动态数组的长度不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,而切片则使用得相当广泛。
切片高效操作的要点是要降低内存分配的次数,尽量保证append操作(在后续的插入和删除操作中都涉及到这个函数)不会超出cap的容量,降低触发内存分配的次数和每次分配内存大小。
我们先看看切片的结构定义,reflect.SliceHeader:
type SliceHeader struct { Data uintptr // 指向底层的的数组指针 Len int // 切片长度 Cap int // 切片最大长度 }
和数组一样,内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。
切片可以和nil进行比较,只有当切片底层数据指针为空时切片本身为nil,这时候切片的长度和容量信息将是无效的。如果有切片的底层数据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了
只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。
当我们想定义声明一个切片时可以如下:
在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息·(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。
append()
:内置的泛型函数,可以向切片中增加元素。
在切片尾部追加N个元素
var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式 a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
注意:尾部添加在容量不足的条件下需要重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用append函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
在切片开头位置添加元素
var a = []int{1,2,3} a = append([]int{0}, a...) // 在开头位置添加1个元素 a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
注意:在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
append链式操作
var a []int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
每个添加操作中的第二个append调用都会创建一个临时切片,并将a[i:]的内容复制到新创建的切片中,然后将临时创建的切片再追加到a[:i]。
append和copy组合
a = append(a, 0) // 切片扩展1个空间 copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置 a[i] = x // 设置新添加的元素
第三个操作中会创建一个临时对象,我们可以借用copy函数避免这个操作,这种方式操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。
根据要删除元素的位置有三种情况:
1、从开头位置删除;
a = []int{1, 2, 3, ...} a = a[1:] // 删除开头1个元素 a = a[N:] // 删除开头N个元素
a = []int{1, 2, 3, ...} a = append(a[:0], a[1:]...) // 删除开头1个元素 a = append(a[:0], a[N:]...) // 删除开头N个元素
a = []int{1, 2, 3} a = a[:copy(a, a[1:])] // 删除开头1个元素 a = a[:copy(a, a[N:])] // 删除开头N个元素
2、从中间位置删除;
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:
a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1], ...) a = append(a[:i], a[i+N:], ...)
a = []int{1, 2, 3} a = a[:copy(a[:i], a[i+1:])] // 删除中间1个元素 a = a[:copy(a[:i], a[i+N:])] // 删除中间N个元素
3、从尾部删除。
代码如下所示:
a = []int{1, 2, 3, ...} a = a[:len(a)-1] // 删除尾部1个元素 a = a[:len(a)-N] // 删除尾部N个元素
删除切片尾部的元素是最快的
为完成某一功能的程序指令(语句)的集合,称为函数。
在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。
举例代码如下:
具名函数:就和c语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。
func Add(a, b int) int { return a+b }
匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。
var Add = func(a, b int) int { return a+b }
解释几个名词如下:
- 闭包函数:返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。
- 一级对象:支持闭包的多数语言都将函数作为第一级对象,就是说函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
- 包:go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的。
Go 语言函数定义格式如下:
func fuction_name([parameter list])[return types]{ 函数体 }
解析 | |
---|---|
func | 函数由func开始声明 |
function_name | 函数名称 |
parameter list | 参数列表 |
return_types | 返回类型 |
函数体 | 函数定义的代码集合 |
Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果,我们解释一下解包的含义,代码如下:
func main(){ var a = []int{1, 2, 3} Print(a...) // 解包 Print(a) // 未解包 } func Print(a ...int{}) { fmt.Println(a...) }
以上当传入参数为a...
时即是对切片a进行了解包,此时其实相当于直接调用Print(1,2,3)
。当传入参数直接为a
时等价于直接调用Print([]int{}{1,2,3})
不仅函数的参数可以有名字,也可以给函数的返回值命名。
举例代码如下:
func Find(m map[int]int, key int)(value int, ok bool) { value,ok = m[key] return }
如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值,举例代码如下:
func mian() { for i := 0 ; i<3; i++ { defer func() { println(i) } } } // 该函数最终的输出为: // 3 // 3 // 3
以上代码中如果没有defer其实返回值就是0,1,2
,但defer语句会在函数return之后才会执行,也就是或只有以上函数在执行结束return之后才会执行defer语句,而该函数return时的i
值将会达到3,所以最终的defer语句执行printlin的输出都是3。
defer语句延迟执行的其实是一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。
这种方式往往会带来一些问题,修复方法为在每一轮迭代中都为defer函数提供一个独有的变量,修改代码如下:
func main() { for i := 0; i < 3; i++ { i := i // 定义一个循环体内局部变量i defer func(){ println(i) } () } } func main() { for i := 0; i < 3; i++ { // 通过函数传入i // defer 语句会马上对调用参数求值 // 不再捕获,而是直接传值 defer func(i int){ println(i) } (i) } }
Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。这部分的知识将会涉及goroutint和动态栈的相关知识,我们将会在之后的博文中向大家解释。
它的语法和c很相似,格式如下:
func recursion() { recursion() /* 函数调用自身 */ } func main() { recursion() }
The above is the detailed content of When did the go language appear?. For more information, please follow other related articles on the PHP Chinese website!