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]        //字面量
_ = &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}
_ = &lt[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

image.png

相关文章
|
4月前
|
Go C语言 C++
人生苦短,开始用go
人生苦短,开始用go
|
5月前
|
Java 编译器 Go
什么让 Go 如此之快?
【8月更文挑战第31天】
56 0
|
6月前
|
Go
go NewTicker 得使用
go NewTicker 得使用
46 1
|
5月前
|
人工智能 安全 Go
深入理解 go RWMutex
深入理解 go RWMutex
45 0
|
存储 JSON JavaScript
GO中`gjson`的应用和分享
GO中`gjson`的应用和分享
|
人工智能 网络协议 Java
|
JSON Go 数据格式
Go slog
Go slog
173 0
Go slog
|
存储 数据采集 XML
GO中 gjson 的应用和分享
咱们上次分享到使用 GO 爬取静态网页的数据,一起来回顾一下 • 分享静态网页和动态网页的简要说明 • GO 爬取静态网页简单数据 • GO 爬取网页上的图片 • 并发爬取网页上的资源
GO中 gjson 的应用和分享
|
编译器 Go
go:linkname
go:linkname
121 2

热门文章

最新文章