类型
- 引用类型特指slice、map、channel这三种预定义类型。
- 内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关的初始化。(除new/make外,还可以使用初始化表达式,编译器生成的指令基本相同)
- 具有相同声明的未命名类型被视作同一类型:
a). 具有相同基类型的指针。
b). 具有相同元素类型和长度的数组(array)。
c). 具有相同元素类型的切片(slice)。
d). 具有相同键值类型的字典(map)。
e). 具有相同数据类型及操作方向的通道(channel)。
f). 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体(struct)。
g). 具有相同签名(参数和返回值列表,不包括参数名)的函数(func)。
h). 具有相同方法集(方法名、方法签名,不包括顺序)的接口(interface)。 - 未命名类型转换规则:
a). 所属类型相同。
b). 基础类型相同,且其中一个是未命名类型。
c). 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型。
d). 将默认值nil赋值给切片、字典、通道、指针、函数或接口。
e). 对象实现了目标接口。
表达式
- 指针类型支持相等运算符,但不能做加减法运算和类型和转换。可以通过unsafe.Pointer将指针转化为uintptr后进行加减法运算,但可能会造成非法访问。
- Pointer类似C语言中的void*万能指针,可用来转换指针类型。他能安全持有对象或对象成员,但uintptr不行。后者仅仅是一种特殊的整形,并不引用对象,无法阻止垃圾回收器回收对象内存。
- for ... range 会赋值底层对象,如数组,则会复制底层数组。可以改用切片作为range的对象,减少复制整个数组的开销。相关的数据类型中,字符串、切片本身基本结构是个很小的结构体,而字典、通道本身是指针的封装,复制成本都很小,无须专门的优化。
- 如果range的对象是一个函数,那么该函数也只被调用一次。
for i := range data() {
}
- 切片用来代替数组传参可避免复制开销。并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而小数组在栈上的拷贝消耗也未必就比make代价大。
- 新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见。
- 从表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,可能还会导致它分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
函数
- Go中函数特点:
a). 无须前置声明
b). 不支持命名嵌套定义(nested)
c). 不支持同名函数重载(overload)
d). 不支持默认参数
e). 支持不定长参数
f). 支持多返回值
g). 支持命名返回值
h). 支持匿名函数和闭包 - 函数只能判断其是否为nil,不支持其它操作
- 变参本质上就是一个切片。只能接收一到多个类型参数,且必须放在列表尾部。
- 将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别,当然,编译器会为匿名函数生成一个“随机”符号名。
- 闭包是函数和引用环境的组合体。本质上返回的是一个funcval结构。
138 type funcval struct {
139 fn uintptr
140 // variable-size, fn-specific data here
141 }
- 正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,设置被分配到堆内存。还有延迟求值的特性。
func test() []func () {
var s []func()
for i:=0; i < 2 ; i++ {
s = append ( s , func() {
println(&i , i )
})
}
return s
}
func main() {
// 这里的test只会被调用一次
for _, f := range test() {
f()
}
}
// 输出结果
0xc420070000 2
0xc420070000 2
// 解决办法
for i:=0 ; i < 2 ; i++ {
x := i
s = append (s , func() {
println(&x , x )
})
}
- return 语句不是ret汇编指令,它会先更新返回值。return和panic语句都会终止当前函数流程,引发延迟调用。
- 千万记住,延迟调用在函数结束时才被执行。不合理的使用方式会浪费资源,甚至造成逻辑错误。如对一个日志文件的close使用defer可能导致文件不能及时关闭,资源不能释放。延迟调用的性能和直接手工调用效率相差4倍~5倍。Go 1.5 version
- 实现接口的方法集的receiver必须不是pointer reciver,赋值给接口的实例必须不是一个pointer实例。
- 在延迟调用中再次panic,不会影响后续延迟调用执行。而recover之后panic,可能被再次捕获,另外,recover必须在延迟调用函数中执行才能正常工作。
- 在正式代码中,我们不能忽略error返回值,应严格检查,否则可能会导致错误的逻辑状态。调用多返回值函数时,除error外,其它返回值同样需要关注,如os.File.Read方法,它同时会返回剩余内容和EOF
- 大量的error处理的解决思路:
- 使用专门的检查函数处理错误逻辑,简化检查代码
- 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
- 除非是不可恢复性、导致系统无法工作的错误,否则不建议使用panic
数据
- 动态构建字符串容易造成性能问题,通常推荐使用strings.Join函数,它会统计所有参数长度,并一次性完成分配操作。
字符串buffer可以用类似于vector.reserve(),也能完成相似的工作,并且性能相当
var b bytes.Buffer
b.Grow(1000)
b.WriteString("hello world")
对于数量较小的字符串格式化拼接,可以使用fmt.Sprintf、text/template
字符串操作通常在堆上分配内存,这会对Web等高并发应用会造成较大影响,会有大量字符串要做垃圾回收。建议使用[]byte缓存池,或在栈上自行拼装等方式来实现zero-garbage。
- 内置函数len和cap都返回第一纬度的长度
- 数组传参数时候,为了减少内存拷贝,可以用指针接收或者切片
func main() {
var a []int
b := []int {}
println(a == nil , b == nil)
}
上述两种方式定义的区别在与,a仅仅定义了一个[]int类型的变量,并未执行初始化操作,而b则用初始化表达式完成了全部创建过程。
自然的,a为nil,b不为nil。
另外,a==nil仅仅表示a是一个未初始化的切片对象,切片本身依然会分配所需内存。可以直接对切片做slice[:]操作,同样返回nil
- 并非所有时候都适合用切片代替数组,因为切片地城数组可能会在栈上分配内存。而且小数组在栈上拷贝的消耗也未必就比make代价大。
- slice在append时候,如果超出当前slice的cap限制,则会重新分配内存
新分配的数组长度是原cap的2倍,并非原数组的2倍(并非总是2倍,对于较大的切片,会尝试扩容1/4,以节约内存) - 向nil切片追加数据时,会为其分配底层数组内存
- 正因为可能会重新分配内存,所以需要留足空间,防止重新分配内存的情况
- 如果切片长时间引用大数组中很小的片段,那么建议独立建立切片,复制出所需要数据,以便原数组内存可以被GC随时回收。
- 字典不能被cap,并被设置为no addressable,所以当需要更新map的key-value时候,应当先读取值存变量中,修改value之后,在重新赋值:
type user struct {
name string
age byte
}
func main() {
m := map[int]user {
1: {"Tom",19},
}
u := m[1]
u.age += 1
m[1] = u
}
但如果内部存储的是指针类型,则可以直接修改:
m2 := map[int]*user {
1 : &user { "wind" , 20 },
}
m2[1].age++
- 不能对nil字典做写操作,但可以读。
- 内容为空的字典,与nil是不同的:
var m1 map[string]int // nil 字典
m2 := map[string]int{} // 内容为空的字典
println( m1 == nil , n2 == nil )
// true false
- 字典和切片对象本身就是指针封装,传参数时,无需要再去地址
最好预先分配好足够的空间,减小map扩张时候,内存分配和重新hash造成的运行时开销。 - 只有在所有的结构字段都支持相等操作时候,才能对结构进行相等比较。
- 空结构(struct{})没有字段结构类型,无论是单个struct{}变量,或者struct{}数组,长度都为0。尽管没分配数组内存,但依然可以操作元素,对应切片的len和cap属性也正常。这类“长度”为0的对象通常都指向runtime.zerobase的变量。
- 空结构可作为通道元素类型,用于事件通知。
- 未命名类型没有名字标识,无法作为匿名字段,接口指针和多级指针都不能作为匿名字段。
- 不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同。
- tag并不是注释,而是对字段进行描述元数据。尽管其不属于数据成员,但确实类型的组成部分(在运行时,可以用反射获取标签信息。被作为格式校验,数据库关系映射等)
方法
- 不能用多级指针调用方法,指针类型的receiver必须是合法指针(包括nil都可以),或者能获取实例地址
type X struct{}
func (x *X) test() {
println("hi!",x)
}
func main() {
var a *X
a.text() // 相当于 test(nil)
}
X{}.test() // 错误 cannot take the address of X literal
- 如何选择方法的reveiver类型?
- 要修改实例状态,用*T
- 无须修改状态的小对象或固定值,建议用T
- 大对象建议用*T,以减少复制成本
- 引用类型、字符串、函数等指针包装对象,直接用T
- 若包含Mutex等同步字段,用*T,避免因为复制造成锁操作无效
- 其它无法确定的情况,都用*T
- 方法会有同名遮蔽问题,利用这种特性,可以实现类似覆盖(override)操作。(name hiding)
- 类型集的判别:
- 类型T方法集包含所有receiver T方法
- 类型*T方法集包含所有receiver T + *T方法
- 匿名嵌入S,T方法集包含所有receiver S方法
- 匿名嵌入S,T方法集包含所有receiver S+S方法
- 匿名嵌入S或S,T方法集包含所有receiver S+*S方法
- 方法集仅影响接口实现和方法表达式转换,与通过实例或者实例指针调用方法无关。实例并不使用方法集,而是直接调用(通过隐士字段名)
- 面向对象的三大特征“封装”、“继承”和“多态”,Go仅实现了部分特征,它更倾向于“组合优于继承”这种思想。将模块分解成相互独立的更小但愿,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。
- Method Expression 和 Method Value的区别:
- 通过类型引用的method expression 会被还原为普通函数样式,receiver是第一参数,调用时须显式传递。类型可以是T或者*T,只要目标方法存在于该类型方法集中即可
type N int
func (n N) test() {
fmt.Printf("test.n:%p,%d\n" , &n , n )
}
func main() {
var n N = 25
fmt.Printf("main.n: %p,%d\n" , &n , n)
f1 := N.test // func (n N)
f1(n) //
f2 := (*N).test // func(n *N)
f2(&n) // 按方法集中的签名传递正确类型的参数
}
- method value,参数签名不会改变,依旧按照正常方式调用。但当method value 被赋值给变量或作为参数传递时,会立即计算并复制该方法执行锁需要的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。
type N int
func (n N) test() {
fmt.Printf("test.n: %p, %v\n" , &n ,n)
}
func main() {
var n N = 100
p := &n
n++
f1 := n.test // 因为test方法的receiver是N类型
// 因此复制n , 等于101
n++
f2 := p.test // 复制p指向的值 等于102
n++
fmt.Prinf("main.n: %p,%v\n" , p , n )
f1()
f2()
}
type N int
func (n N) test() {
fmt.Printf("test.n: %p, %v\n" , &n ,n)
}
func main() {
var n N = 100
p := &n
n++
f1 := n.test // 因为test方法的receiver是N类型
// 因此复制n , 等于101
n++
f2 := p.test // 复制p指向的值 等于102
n++
fmt.Prinf("main.n: %p,%v\n" , p , n )
f1()
f2()
}
// main.n: 0xc42007c008,103
// test.n: 0xc42007c020,101
// test.n: 0xc42007c030,102
- 编译器会为method value生成一个包装函数,实现间接调用。至于receiver复制,和闭包的实现方法基本相同,打包成funcval,经由DX寄存器传递。
- 当method value作为参数时,会复制含receiver在内的整个method value,当目标方法的receiver是指针类型,那么被复制的仅是指针。
接口
- 接口除了类型以来,有助于减少用户可视方法,屏蔽内部结构和实现细节。但接口实现机制会有运行期开销。对于相同包,或者不会频繁变化的内部模块之间,并不需要抽象出接口来强行分离。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。
- 从内部实现来看,接口自身也是一种结构类型,只是编译器会对其作出很多限制。
type iface struct {
tab *itab
data unsafe.Pointer
}
- 接口不能有字段
- 不能定义自己的方法
- 只能声明方法,不能实现
- 可嵌入其它接口类型
- 编译器根据方法集判断是否实现了接口。接口变量的默认值是nil,如果实现接口的类型支持,可以做相等运算。
- 嵌入其它接口类型,相当于将其声明的方法集导入。这就要求不能有同名方法,因为不支持重载。还有,不能嵌入自身或者循环嵌入,那会导致递归错误。
- 超级接口变量可以隐士转换为子集,反过来不行。
- 接口使用一个名为itab的结构存储运行期所需的相关类型信息。
type iface struct {
tab *itab // 类型信息
data unsafe.Pointer // 实际对象指针
}
type itab struct {
inter *interfacetype // 接口类型
_type *_type // 实际对象类型
fun [1]uintptr // 实际对象方法地址
}
- 相关类型信息里保存了接口和实际对象的元数据。同时,itab还用fun数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。
除此之外,接口还有一个重要特征:将对象复制给接口变量时,会复制该对象。