文档和资源
要注意的点
Packages
每个go程序都由package构成。
Exported name
在一个包中,如果一个name是以大写字母开头的,那么这个name将会被从这个包中导出。
当import一个包的时候,只能引用被导出的name。
函数
参数
参数名称在前,类型在后,Go’s Declaration Syntax。
当多个连续的函数参数有共同的类型是,可以省略不写除最后一个外的类型。
|
|
返回值
一个函数可以返回任意个数的结果。
返回值可以是有名字的。当return
不带任何参数时,函数返回named return values,这叫做"naked" return。
|
|
变量
var
声明多个变量时,只能写最后一个的类型。
|
|
如果声明时给出了初始值,那么会以初始值的类型作为变量的类型,此时声明中的类型可省略。
在函数内部可以用短变量声明:=
来声明变量,类型由值的类型来决定。但在函数外部,由于所有语句都需要以关键字开头,因此不可用这个方法。
初始化 在声明变量的时候,变量的值总是会被初始化,要么是用指定的值,要么是零值(变量类型的默认值)。
基本数据类型
|
|
声明变量但不显式给出初始值,变量会被赋予零值。数值类型:0
,bool类型:false
,string:""
。
类型转换和推断
在进行类型转换时,go只能使用显式类型转换。
使用:=
或var =
声明变量、未指明类型、但给出初始值时,变量的类型由对初始值进行类型推断得到。如果右侧是数值常量,那么变量的类型可能是int
,float64
,complex128
。
常量
只可以用const
来声明。数值常量可表示任意精度,且不会溢出。一个未指定类型的常量由上下文来决定其类型。
全局变量
在程序运行期间,始终存在。声明和初始化方式与普通变量相同,需要在函数外部声明。
语句
for
|
|
if
|
|
switch
求值顺序,按case的顺序,自上向下进行。
|
|
defer
使用defer
时,被defer
的函数会被push到一个stack,参数会立即计算,但函数结束时,stack中的函数才会被pop出来执行。
|
|
defer
的函数可以读取和赋值到函数的返回值。
|
|
defer、panic和recover
当调用panic
时,所有defer
的函数都被正常执行。然后函数返回到调用者。
recover
仅在defer
的函数中有用,正常执行时调用,只会返回nil
。
|
|
其他类型
关于slice,map和channel,某些书中会将它们描述为引用,但从实现上看(例如:slice、map、chan),这些类型不过只是封装了底层指针的struct,且go spec也早就在文档中移除了reference一词的使用,而在THE WAY TO GO一书中虽然使用了reference一词,但也明确指出,
A reference type variable r1 contains the address (a number) of the memory location where the value of r1 is stored. … When assigning r2 = r1, only the reference (the address) is copied. … In Go pointers (see § 4.9) are reference types, as well as slices (ch 7), maps (ch 8) and channels (ch 13). ……
指针
存储了内存地址,零值为nil
。&
获得变量的地址,*
解引用。
对比c的指针,go的指针无法进行算数运算。
|
|
指针的类型转换
unsafe.Pointer
:type Pointer int
,代表了变量的内存地址,可以将任意变量的内存地址与Pointer
指针相互转换。
uintptr
:type uintptr int
,Pointer
无法进行加减运算,需要转换为uintptr
才可以,可以将Pointer
与uintptr
指针相互转换。
unsafe.Offsetof
:可以得到字段在结构体内的偏移量。
|
|
Struct
字段的集合,使用.
来访问字段。首字母大写和小写分别代表公开和私有。私有变量只有同一个package才可以访问。
对于struct指针,可以使用(*p).X
或直接使用p.X
来进行访问。对比c,go不能用->
来访问成员。
初始化
|
|
copy
- 结构体之间的copy是深拷贝,不共享结构体内部字段。
- 结构体指针的copy是浅拷贝,共享内部字段。
组合
|
|
Array
和c一样,数组的大小也是数组类型的一部分,声明数组时必须有大小,通过下标访问数组中的元素。
程序执行时,go会检查访问是否越界。
|
|
内部存储 连续分配的内存区域。
copy 数组的类型由元素的类型和数组的大小决定,相同类型的数组之间才可以copy。拷贝一个数组,数组的内部的元素也会被逐一拷贝,因此作为变量传递时,需要注意copy的开销。
Slice
slice的类型为[]int
,对数组进行,
a[low_index:high_index]
后得到,区间是前闭后开,可以省略low_index
或high_index
,默认值分别为0
和数组长度。cap(a)
=len(array) - low_index
a[low_index:high_index:cap_index]
后得到,区间是“闭、开、开”。cap_index
代表可用到的底层数组的最大index,必须小于len(array)
。cap(a)
=cap_index - low_index
|
|
内部存储
|
|
例如:
|
|
slice和array
slice本身并不存储任何数据,仅仅是数组选定区间的描述,和数组共享底层的数据。len()
和cap()
对应了slice的长度,和底层数组从low
起的大小,即:len(array) - low
。
对已有slice再做一次slice,实际上是改变slice对底层数组的引用范围。
|
|
slice字面值类似数组的,区别是没有大小。底层实际上创建了相同大小的数组,然后再创建slice。
nil slice
一个nil slice,是未初始化的slice,len
和cap
都为0,且不会分配底层的数组,数组指针为nil
。
|
|
空slice
一个空slice的len
和cap
都为0,且不会分配底层的数组,数组指针值不为空,但是也未分配底层数组。
|
|
当想声明一个空的slice时,nil slice和空slice都可以,两者在功能上完全等价,但是更推荐nil slice。但二者进行序列化的时候,结果会不同,nil slice会编码为null,而空slice是[]
。
make slice
通过make
来创建动态长度的数组。
|
|
append
append
函数能够将相同类型元素追加至现有slice,若底层数组大小不够,则会重新分配内存,并将slice指向新数组。
如果发生了扩容,且有另一个slice存在,那么另一个slice的仍然指向老的数组。
扩容时,如果cap < 1024
,那么会扩100%,否则扩25%。
|
|
range
除了普通方法遍历slice,还能使用range
,
|
|
当使用range
返回的值,v
时,要注意的是range
返回的是元素的copy,而不是引用,如果对齐进行&
,那么得不到期望的结果。具体来说,Go会使用同一个变量,在每轮迭代中保存元素的copy。可以使用kyoh86/scopelint来检查代码中的unpinned variables。
取地址。
1 2 3 4 5
a := make([]int, 5) for i, v := range a { fmt.Println(&v, &a[i]) // v的地址保持不变,且不等于a中任意元素的地址 }
这个原因还有可能导致使用goroutine时出现意外。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// 由于closure已经绑定到了val,又因为goroutine可能在for结束后才执行 // 因此打印出的可能全都是values的最后一个值。 for _, val := range values { go func() { fmt.Println(val) }() } // ok for _, val := range values { go func(val interface{}) { fmt.Println(val) }(val) } // ok for i := range valslice { val := valslice[i] // val在每次迭代中都会分配新的 go func() { fmt.Println(val) }() }
copy
slice的copy是浅拷贝,两个slice共享底层数组。本质上copy的是:指向底层数组的指针、len
和cap
。
|
|
go还提供了一个函数copy
来实现数组内容的copy,copy时,会以目的切片的容量为准。
|
|
结合slice的内部存储、append
和拷贝,有的使用场景不注意可能导致意料之外的结果。
|
|
具体过程分析如下:
s[0]++
:a
和s
都指向同一个底层数组arr1
,此时a->arr1
,s->arr1
,修改了arr1
。append
:由于扩容,append
返回了一个新的底层数组arr2
,a->arr1
,s->arr2
。s[0]++
:修改了arr2
,arr1
不变。
Map
一个仅做了声明的map是nil
,需要使用make
来进行初始化。切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的key,使用这些类型会造成编译错误。
|
|
map字面值和struct字面值类似。
|
|
mil map nil map未进行初始化,不能用于存储key-value。
|
|
make map
通过make
来创建map。
|
|
range 类似slice,且slice中存在的问题,map中也同样存在。由于无法获取index,因此只能通过每轮迭代创建变量来解决。
|
|
操作
- 新增和更新:
m[key] = elem
- 访问key的值:
elem = m[key]
,如果不存在,那么elem
为此类型的零值,但如果值真的是零值,那通过这个方法来判断key是否存在就失效了。 - 删除:
delete(m, key)
- test:
var elem, ok = m[key]
,如果不存在,那么elem
为此类型的零值。
字符和字符串
rune
rune字面值代表一个rune常量,是一个标识Unicode code point的整型值。alias for int32
。
rune字面值可以用'单个字符'
来表示,可以用\
转义的多个字符来表示,具体见Rune literals。
string
string字面值代表了包含一系列字符的string常量,只读。有两种形式:raw string字面值和interpreted string字面值。
- raw string字面值,是经过未转义处理的,在raw string内部,可以出现任意字符。string中出现的
'\r'
会被忽略。 - interpreted string字面值,go会进行转义处理。具体见String literals
|
|
内部存储
|
|
|
|
由于底层存储是数组,因此可以做slice,但要注意的是,这里本质上是对字节来做slice,因此如果slice的Unicode code point不是一个完整的字符,那么打印的时候,是不会正确显示的。
|
|
从字符串得到字节slice或者从字节slice得到字符串,会发生底层数组的copy。如果想避免copy,可以手动一个string或slice,获得一个原始string或者slice的“reference”,这种方式不可以通过slice修改string,因为修改后,“reference”到的原有string失效了,可能会被gc回收。
|
|
遍历
- 按字节遍历:通过下标。
- 按字符遍历:range方式遍历。
Function values
函数也是值,可作为参数传递,作为返回值返回。
闭包(closure)是function value引用了函数体外部的变量,函数可以访问和修改这些变量。换句话说,闭包包含了函数、以及所在的环境的上下文。
方法和接口
方法集(method sets)和调用
方法集和函数调用的规范明确了一个类型有哪些方法,以及在什么时候可以调用什么样的方法。go wiki上关于这两个概念有比较详细的例子。
method set
对于一个接口类型,接口是方法集。
对于一个类型
T
,所有receiver为T的方法是方法集。对于类型T对应的指针类型
*T
,所有receiver为T
或*T
的方法是方法集。
Type | method sets |
---|---|
interface type | interface |
T | func (T) f() |
*T | func (*T) f(), func (T) f() |
一个类型T
的方法集决定了,这类型T
的接口类型的实现,和使用T
作为receiver时可以被调用的方法。
call
对于一个方法调用x.m()
,
如果
x
的method set包含m()
,且调用时的参数列表合法,那么这个调用是合法的。如果
x
可以取地址的,并且&x
的method set包含m()
,那么x.m()
等价于(&x).m()
。map元素和interface存储的具体值不可取地址。
方法
方法是带有特殊receiver参数(func
和函数名之间)的函数。这个receiver不必是struct,但要求receiver的类型定义必须在同一个package里面,且不能直接将内置类型作为receiver。
|
|
Point receiver 若要修改字段,则必须使用point receiver,无论变量本身是否是指针类型,非指针receiver调用时发生了copy。
从这里可以得出使用point receiver的场景:1. 避免copy;2. 修改值本身。一般来说,某个类型的receiver应该统一,要么是point receive,要么是普通receiver。
|
|
- 若v不是指针类型,那么go会把
v.Scale
自动转换为(&v).Scale
。 - 反过来,若v是指针类型,在调用
Scale2
时,go会把v.Scale2
转换为(*v).Scale2
。
对比c++的成员函数,this
指针类似于point receiver,但go的普通receiver是不同于c++的。
接口
接口是方法签名的集合。
接口的实现是隐式的,无需类似implement
的关键字。隐式实现解耦接口的定义和实现,在package中,接口的定义可以出现在方法和类型定义之后。注意方法的实现区分普通receiver和point receiver。
一个接口值可以被赋值为任何实现了接口中所有方法的值。接口值底层实际包含了具体值的类型,接口值可以看做是值和具体类型的元组。调用接口值的方法,实际上会调用具体类型的方法。
|
|
对比c++的多态,c++中通过继承基类,并覆盖基类的虚函数,在运行时进行动态绑定,以此实现多态。go的接口方法定义可以看做是基类和虚函数,而a = f
相当于将子类的指针赋值给基类指针,这样完成了动态绑定。
不同的点还是receiver,实现接口的方法时,go区分了point receiver和普通receiver。
内部存储 实现上,一个接口值底层包含了指向类型和数据的指针。
接口类型之间的赋值和类型转换是共享数据的,而结构体之间的赋值、结构体转接口、接口转结构体,都会导致数据的copy。
空的具体类型值
如果接口的具体类型值是空的,那么将会使用nil
receiver来调用方法,不引发空指针异常。
|
|
空的接口值
会发生运行时错误,没有具体的Abs
方法可以调用。
|
|
空接口 空接口的值可以包含任何类型。
|
|
接口变量的赋值
对于数值类型,底层的具体类型只能是int
,float64
,complex128
。
类型断言 类型断言提供了访问接口底层具体类型值的能力。
t := i.(T)
断言接口i
拥有具体类型T
,并把类型T
的值赋值给t
。如果不是类型T
,则触发panic。- test:
t, ok := i.(T)
断言不正确的情况,不触发panic,而是ok
为false,且t为类型T
的零值。
|
|
Type swtiches是允许断言多个类型的结构。类似switch语句,但是每个case是特定的类型。
|
|
对比scala的pattern matching,go的type swtiches像,但不是pattern matching。scala的pattern matching会检查值和pattern是否匹配,能够把值解构为构成值的各部分。猜测go的type swtiches是类型字符串是否相等的test。
一些内置的接口
Stringer
类似python的__str__
,定义在fmt
中。
|
|
error
类似Stringer,fmt在print的时候也会查找error
接口。从fmt的实现上看,是error优先。
|
|
error
更适合用于专门定义的错误类型。否则功能上,stringer
和error
就冗余了。
可以使用fmt.Errorf
或errors.New
来创建error
类型的值。
|
|
Reader
io包定义了io.Reader
接口,代表读取stream,有多个实现(文件、网络等)。
其中func (T) Read(b []byte) (n int, err error)
方法使用现有数据填充b
,并返回填充的字节数和error
。stream结束时,error
为io.EOF
。
并发
Goroutines
由go运行时管理的轻量级线程。
|
|
参数的计算在当前goroutine中完成,函数f
的调用发生在新的goroutine。所有子协程都是平级的关系(包括在子协程内部启动另一个协程)。
Channels
channel是带类型的管道(typed conduit),每次只能发送或接受一个元素。默认情况下,发送方和接收方会一直阻塞到另一方ready,每次只能唤醒一个发送或接受方。
方向
|
|
unbuffered channel unbuffered channel必须保证先有goroutine正在接收,否则发送方会一直阻塞到有goroutine来接收为止。
|
|
buffered channel
ch := make(chan int, 100)
,buffered channel在满或空的情况下,分别会导致发送方和接收方阻塞。
range
for i := range ch
可以从channel逐个接收值,直到channel被关闭。
close
- 发送方可以通过
close(ch)
来告诉接收方没有后续的值会发送。如果向关闭的channel发送元素,那么会导致抛出异常。 - 接收方可以使用
v, ok := <-ch
判断channel是否被关闭。如果从一个已经关闭的channel接收元素,会返回channel类型的零值,因此是不能用这个方式来判断channel是否关闭的。
select select语句可以让goroutine等待多个通信操作(发送或接受都可以),block直到其中某个case能执行。如果同时有多个case能执行,则随机选择一个。
若存在default,则当没有case ready的时候,执行default,因此可以通过default实现非阻塞式的发送或接受。
|
|
并发模式
内存模型
go内存模型
反射
构建
static build
go build
启用race,也需要启用cgo。
测试
总结
抛开go的运行时环境和gc不说,go很像c,同时还有着少量函数式语言的特性。
go中,我很喜欢的几点是:
- 变量和函数的声明简洁清晰
- goroutine
- 提供了CSP来实现goroutine之间的通信