七叶笔记 » golang编程 » Go 语言中的坑

Go 语言中的坑

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
  

零值

零值和未出世后的值并不相同, 不同类型的零值是什么

  1. 布尔类型是 false, 整型是0, 字符串是 “”
  2. 指针,函数,interface 、slice 、channel 和 map 的零值都是 nil
  3. 结构体的零值是递归生成的,每个成员都是对应的零值

使用要注意如下几点:

  • 一个为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 {...}
  
  1. 实际遍历的 aContainer 是原始值的一个副本
  2. element 是遍历到的元素原始值的一个副本
  3. key 和 Value 整个循环都是同一个变量,每次迭代都生成新变量

aContainer和element的拷贝成本。aContainer数组的时候的拷贝成本比较大,而切片和map的拷贝成本比较小。如果想要缩小拷贝成本,我们有几个建议:

  1. 遍历大数组时,可以先创建大数组的切片再放在range后面
  2. element结构比较大的时候,直接用下标key遍历,舍弃element

map 的值不可取址

map 是哈希表的实现,所以值的地址在哈希表动态调整的时候会可能产生变化,因此,存在着 map 值的地址是没意义的,go 禁止了 map 取址操作,一下类型都不可取址

  • map 元素
  • string 的字节元素
  • 常量(有名变量和字面量都不可以)
  • 中间结果值(函数调用,显示值转换,各种操作)
 // 下面这几行编译不通过。
_ = &[3]int{2, 3, 5}[0]        //字面量
_ = ↦[int]bool{1: true}[1]  //字面量
const pi = 3.14
_ = π                        //有名常量
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"]
  

错误处理

  1. 可以把异常传递下去,并不丢失自己的类型
  2. 可以保存堆栈信息

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
  

欢迎关注:程序员开发者社区

学习资料

  • 《Go Tour》(一个小时学会Go)
  • 《The Go Programming Language Specification》(语法细节)#Introduction(中文版《Go语言编码规范》)
  • 《Go语言圣经》(语法细节)
  • 《Effective Go》(适合刚学完Go的基础语法时候读)
  • 《Go语言设计和实现》(适合想了解Go某个特性实现原理的时候参考)
  • 《Go Q&A 101》(可以和官方QA结合看)#time-sleep-after
  • 《Go 语言高级编程》
  • 《Go语言原本》
  • 《Google Go代码规范》
  • 《Uber Go规范》
  • #

相关文章