Go 的优点
Go like C++
- 内存消耗少
- 执行速度快
- 启动快 Go not like C++
- 程序编译时间短
- 像动态语言一样灵活(runtime, interface, 闭包,反射)
- 内存并发正常
defer
defer 执行顺序是先进后出,栈的方式 FIFO ,参数的值在 defer 语句执行就已经确定了
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
执行结果:
4 3 2 1 0
append 不是线程安全的
slice 中,如果 a[x] 和 b[y] 指向同一个内存区域,那么存在竞态关系
package main import ( "fmt" ) func main() { a := []int{1, 2} b := a[1:] go func() { a[1] = 0 }() fmt.Println(b[0]) }
slice.go
package main import ( "fmt" ) func main() { a := []int{1, 2} b := a[1:] go func() { a[1] = 0 }() fmt.Println(b[0]) }
运行命令
go run -race slice.go
执行结果
2 ================== WARNING: DATA RACE Write at 0x00c0000bc018 by goroutine 7: main.main.func1() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:11 +0x47 Previous read at 0x00c0000bc018 by main goroutine: main.main() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:14 +0xb9 Goroutine 7 (running) created at: main.main() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:10 +0xab ================== Found 1 data race(s) exit status 66
零值
零值和未初始后的值并不相同, 不同类型的零值是什么
- 布尔类型是 false, 整型是0, 字符串是 “”
- 指针,函数,interface 、slice 、channel 和 map 的零值都是 nil
- 结构体的零值是递归生成的,每个成员都是对应的零值
使用要注意如下几点:
- 一个为nil的slice,除了不能索引外,其他的操作都是可以的
- nil的map,我们可以简单把它看成是一个只读的map
// 一个为nil的slice,除了不能索引外,其他的操作都是可以的 // Note: 如果这个slice是个指针,不适用这里的规则 var a []int fmt.Printf("len(a):%d, cap(a):%d, a==nil:%v\n", len(a),cap(a), a == nil) //0 0 true for _, v := range a{// 不会panic fmt.Println(v) } aa := a[0:0] // 也不会panic,只要索引都是0 // nil的map,我们可以简单把它看成是一个只读的map var b map[string]string if val, ok := b["notexist"];ok{// 不会panic fmt.Println(val) } for k, v := range b{// 不会panic fmt.Println(k,v) } delete(b, "foo") // 也不会panic fmt.Printf("len(b):%d, b==nil:%v\n", len(b), b == nil) // 0 true
值传递
Go 语言中所有的传参都是值传递,或者说一个拷贝,传入的数据能不能在函数内被修改,取决于是指针或者含有指针的类型(指针被值传递复制后依然指向同一块地址),什么时候传入的参数会修改会生效,什么时候不会生效。slice类型在 值传递的时候len和cap不会变,所以函数内append没有用:
type slice struct { array unsafe.Pointer len int cap int } // badcase func appendMe(s []int){ s = append(s, -1) }
map 和 chan 是引用类型,是个指针, 在函数内修改会生效
// map实际上是一个 *hmap func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap { //省略无关代码 } // chan实际上是个 *hchan func makechan(t *chantype, size int64) *hchan { //省略无关代码 }
结构体传参
// 这是一个典型的指针包裹类型 type Person struct { name string age *int } func modify(x Person){ x.name = "modified" *x.age = 66 }
这个结构体中 age 是个指针类型,在函数内会被修改
复制数据时,使用 copy 比 append 性能更好
import ( "crypto/rand" "testing" ) var ( src = make([]byte, 512) dst = make([]byte, 512) ) func genSource() { rand.Read(src) } func BenchmarkCopy(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() genSource() b.StartTimer() copy(dst, src) } } func BenchmarkAppend(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() genSource() b.StartTimer() dst = append(dst, src...) } }
dst 作为全局变量防止编译器优化 for-loop
uptime;go version;go test -bench=. ./ 11:56:10 up 294 days, 14:58, 3 users, load average: 0.58, 0.52, 0.63 go version go1.14.1 linux/amd64 goos: linux goarch: amd64 pkg: copyvsappend BenchmarkCopy-40 9808320 116 ns/op BenchmarkAppend-40 479055 8740 ns/op PASS
Go 语言中为啥没有继承
go 没子类型的概念,只能把类型嵌入另外一个类型中,所以没有类型系统。
- 使用伸缩性良好的组合,而不是继承
- 数据和方法不绑定在一起,数据的集合使用 struct, 方法的集合使用 interface ,保持正交
接收器是用指针还是值
go 接收器可以用指针,也可以传值,传值的时候接收器不会改变。如果以下两种情况,请使用指针:
- mystruct 很大时,需要拷贝的成本太高
- 方法需要修改 myStruct
Note:如果对象有可能并发执行方法,指针接收器中可能产生数据竞争,记得加锁
func(s * MyStruct)pointerMethod(){ // 指针方法 s.Age = -1 // useful } func(s MyStruct)valueMethod(){ // 值方法 s.Age = -1 // no use }
for 循环里的是副本
for key, element = range aContainer {...}
- 实际遍历的 aContainer 是原始值的一个副本
- element 是遍历到的元素原始值的一个副本
- key 和 Value 整个循环都是同一个变量,每次迭代都生成新变量
aContainer和element的拷贝成本。aContainer 数组的时候的拷贝成本比较大,而切片和map的拷贝成本比较小。如果想要缩小拷贝成本,我们有几个建议:
- 遍历大数组时,可以先创建大数组的切片再放在range后面
- element结构比较大的时候,直接用下标key遍历,舍弃element
map 的值不可取址
map 是哈希表的实现,所以值的地址在哈希表动态调整的时候会可能产生变化,因此,存在着 map 值的地址是没意义的,go 禁止了 map 取址操作,以下类型都不可取址
- map 元素
- string 的字节元素
- 常量(有名变量和字面量都不可以)
- 中间结果值(函数调用,显示值转换,各种操作)
// 下面这几行编译不通过。 _ = &[3]int{2, 3, 5}[0] //字面量 _ = &map[int]bool{1: true}[1] //字面量 const pi = 3.14 _ = &pi //有名常量 m := map[int]bool{1: true} _ = &m[1] //map的value lt := [3]int{2, 3, 5} _ = <[1:1] //切片操作
常用的仓库
strings
有 strings 库,不要重复造轮子,很多人试图再写一遍,没必要
字符串前后处理
var s = "abaay森z众xbbab" o := fmt.Println o(strings.TrimPrefix(s, "ab")) // aay森z众xbbab o(strings.TrimSuffix(s, "ab")) // abaay森z众xbb o(strings.TrimLeft(s, "ab")) // y森z众xbbab o(strings.TrimRight(s, "ab")) // abaay森z众x o(strings.Trim(s, "ab")) // y森z众x o(strings.TrimFunc(s, func(r rune) bool { return r < 128 // trim all ascii chars })) // 森z众
字符串分割与合并
// "1 2 3" -> ["1","2","3"] func Fields(s string) []string // 用空白字符分割字符串 // "1|2|3" -> ["1","2","3"] func Split(s, sep string) []string // 用sep分割字符串,sep会被去掉 // ["1","2","3"] -> "1,2,3" func Join(a []string, sep string) string // 将一系列字符串连接为一个字符串,之间用sep来分隔 // Note: // "1||3" -> ["1","","3"]
错误处理
- 可以把异常传递下去,并不丢失自己的类型
- 可以保存堆栈信息
for range
如果每个元素比较大,循环时,使用range 取值的方式遍历,性能比较差
package bench import "testing" var X [1 << 15]struct { val int _ [4096]byte } var Result int func BenchmarkRangeIndex(b *testing.B) { var r int for n := 0; n < b.N; n++ { for i := range X { x := &X[i] r += x.val } } Result = r } func BenchmarkRangeValue(b *testing.B) { var r int for n := 0; n < b.N; n++ { for _, x := range X { r += x.val } } Result = r } func BenchmarkFor(b *testing.B) { var r int for n := 0; n < b.N; n++ { for i := 0; i < len(X); i++ { x := &X[i] r += x.val } } Result = r }
执行命令
go test -bench=. bench_test.go