这里列举的Go语言常见坑都是符合Go语言语法的,可以正常的编译,但是可能是运行结果错误,或者是有资源泄漏的风险。
当参数的可变参数是空接口类型时,传入空接口的切片时需要注意参数展开的问题。
package main import "fmt" func main() { var a = []interface{}{1, 2, 3} fmt.Println(a) fmt.Println(a...)}
不管是否展开,编译器都无法发现错误,但是输出是不同的:
[1 2 3]1 2 3
在函数调用参数中,数组是值传递,无法通过修改数组类型的参数返回结果。
package main import "fmt" func main() { x := [3]int{1, 2, 3} // 匿名函数, 传入数组, 尝试通过数组索引修改数组 func(arr [3]int) { arr[0] = 7 fmt.Println("arr:", arr) }(x) fmt.Println("x:", x)}
输出:
arr: [7 2 3]x: [1 2 3]
必要时需要使用切片。
map是一种hash表实现,每次遍历的顺序都可能不一样。
package main import "fmt" func main(){ m := map[string]int{ "1":1, "2":2, "3":3, } // 遍历字典k,v for k, v := range m { fmt.Println(k, v) }}
每次执行结果,输出都不一样 输出:
3 31 12 2
在局部作用域中,命名的返回值内同名的局部变量屏蔽:
package main import "fmt" func Bar() error { return fmt.Errorf("func err Bar()... ")} func Foo() (err error) { if err := Bar(); err != nil { return } return} func main() { err := Foo() fmt.Printf("err is %v", err)}
重新定义返回的变量名,导致输出错误, 输出
D:\gopath\src\Go_base\lesson\someNots>go run demo.go# command-line-arguments.\demo.go:11:3: err is shadowed during return
正确方式: 必须在defer函数中直接调用才有效:
package main import "fmt" func main() { defer func() { err := recover() if err != nil { fmt.Printf("err:%v", err) } }() panic(1)}
后台Goroutine无法保证完成任务。
package main func main() { go println("hello")}
main函数相当于主线程, go启用单独的线程,无法满足 一致性
休眠并不能保证输出完整的字符串:
package main import "time" func main() { go func() { time.Sleep(time.Microsecond) println("hello, this is a goroutine") }() time.Sleep(time.Microsecond)}
因为主线程于协程之间并不能满足一致性原则
Goroutine是协作式抢占调度,Goroutine本身不会主动放弃CPU:
package main import ( "fmt" "runtime") func main() { runtime.GOMAXPROCS(1) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } }() for { } // 占用CPU}
结果会一直出于阻塞状态
解决办法
因为在不同的Goroutine,main函数中无法保证能打印出hello, world:
package main var msg stringvar done bool func setup() { msg = "hello, world" done = true} func main() { go setup() println(done) for !done { } println(msg)}
输出:
falsehello, world
解决的办法:是用显式同步:
package main import "fmt" var msg stringvar done = make(chan bool) func setup() { msg = "hello, world" done <- true} func main() { go setup() // 无缓冲通道,写入优先于读取,所以当通道无数据时,会一直进行阻塞 d := <-done fmt.Println(d) println(msg)}
msg的写入是在channel发送之前,所以能保证打印hello, world
package main func main() { for i := 0; i < 5; i++ { // defer会压栈,只会存储最后一个变量值 defer func() { println(i) }() }}
输出:
55555
改进:
defer在*函数退出时才能执行**,所以直接在for循环内执行defer会导致资源延迟释放:
package main import ( "log" "os") func main() { for i := 0; i < 5; i++ { f, err := os.Open("/path/to/file") if err != nil { log.Fatal(err) } // 会导致同时打开5个文档的操作句柄, 最后才会关闭 defer f.Close() }}
解决的方法: 在for中构造一个局部函数,在局部函数内部执行defer:
package main import ( "log" "os") func main() { for i := 0; i < 5; i++ { // 构建一个局部函数 func() { f, err := os.Open("/path/to/file") if err != nil { log.Fatal(err) } // 函数执行完毕后,就可以直接执行 close操作 defer f.Close() }() }}
切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大会对内存产生很大的压力。
package main import ( "io/ioutil" "log") func main() { headerMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/path/to/file" // data是一个 byte数组 data, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } // map赋值时,对数组进行了切片 headerMap[name] = data[:1] } // do some thing}
解决的方法: 将结果克隆一份,这样可以释放底层的数组:
package main import ( "io/ioutil" "log") func main() { headerMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/path/to/file" data, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } // 将数组data切片后直接克隆一份儿 headerMap[name] = append([]byte{}, data[:1]...) } // do some thing}
比如返回了一个错误指针,但是并不是空的error接口:
func returnsError() error { var p *MyError = nil if bad() { p = ErrBad } return p // Will always return a non-nil error.}
Go语言中对象的地址可能发生变化,因此指针不能从其它非指针类型的值生成:
package main import ( "runtime" "unsafe") func main() { var x int = 42 // p 为x的指针 var p uintptr = uintptr(unsafe.Pointer(&x)) runtime.GC() // 取地址 var px *int = (*int)(unsafe.Pointer(p)) println(*px)}
当内存发送变化的时候,相关的指针会同步更新,但是非指针类型的uintptr不会做同步更新。
同理CGO中也不能保存Go对象地址。
Go语言是带内存自动回收的特性,因此内存一般不会泄漏。但是Goroutine确存在泄漏的情况,同时泄漏的Goroutine引用的内存同样无法被回收。
package main import "fmt" func main() { // 定义一个匿名函数, 返回一个只读int类型通 ch := func() <-chan int { // 定义一个无缓冲读写通道 ch := make(chan int) // 协程用于向通道写入数据 go func() { for i := 0; ; i++ { ch <- i } }() return ch }() // 遍历结果 for v := range ch { fmt.Println(v) if v == 5 { break } }}
上面的程序中后台Goroutine向管道输入自然数序列,main函数中输出序列。但是当break跳出for循环的时候,后台Goroutine就处于无法被回收的状态了。
解决方法: 可以通过context包来避免这个问题:
package main import ( "context" "fmt") func main() { ctx, cancel := context.WithCancel(context.Background()) ch := func(ctx context.Context) <-chan int { ch := make(chan int) go func() { for i := 0; ; i++ { select { case <-ctx.Done(): return case ch <- i: } } }() return ch }(ctx) for v := range ch { fmt.Println(v) if v == 5 { cancel() break } }}
当main函数在break跳出循环时,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏
append
的本质是向切片中追加数据,而随着切片中元素逐渐增加,当切片底层的数组将满时,切片会发生扩容.
如下: 函数Validation()用于一些合法性检查,每遇到一个错误,就生成一个新的error并追加到切片errs中, 最后返回包含所有错误信息的切片。 为了简单起见,假定函数发现了三个错误,如下所示:
func Validatior() []error { var errors []error append(errs, errors.New("error 1") append(errs, errors.New("error 2") append(errs, errors.New("error 3")}
函数Validation()有什么问题?
目前有很多的工具可以自动检查出类似的问题,比如GolandIDE就会给出很明显的提示。但是并不知道为何出错。
append每个追加元素,都有可能触发切片扩容,也即有可能返回一个新的切片,这也是append函数声明中返回值为切片的原因。实际使用中应该总是接收该返回值。
上述题目一中,由于初始切片长度为0,所以实际上每次append都会产生一个新的切片并迅速抛弃(被gc回收)。 原始切片并没有任何改变。需要特别说明的是,不管初始切片长度为多少,不接收append返回都是有极大风险的。 所以正确的方式如下:
func Validatior() []error { var errs []error errs=append(errs, errors.New("error 1") errr=append(errs, errors.New("error 2") errs=append(errs, errors.New("error 3")}
函数ValidateName()
用于检查某个名字是否合法,如果不为空则认为合法,否则返回一个error。
类似的,还可以有很多检查项,比如检查性别、年龄等,我们统称为子检查项。
函数Validations()
用于收集所有子检查项的错误信息,将错误信息汇总到一个切片中返回。
请问函数Validations()
有什么问题?
func ValidateName(name string) error { if name != "" { return nil } return errors.New("empty name")} func Validations(name string) []error { var errs []error errs = append(errs, ValidateName(name)) return errs}
向切片中追加一个nil值是完全不会报错的,如下代码所示:
slice := append(slice, nil)
经过追加后,slice的长度递增1。
实际上nil是一个预定义的值,即空值,所以完全有理由向切片中追加。
单纯从技术上讲是没有问题,但在使用场景中就有很大的问题。
比如你可能会根据切片的长度来判断是否有错误发生,比如
func foo() { errs := Validations("") if len(errs) > 0 { println(errs) os.Exit(1) }}
如果向切片中追加一个nil元素,那么切片长度则不再为0,程序很可能因此而退出,更糟糕的是,这样的切片是没有内容会打印出来的,这无疑又增加了定位难度.
首先看下如下几种方式的代码:
func Process1(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func() { fmt.Printf("Worker start process task: %s\n", task) }() }}
2.函数Process2()用于处理任务,每个任务均启动一个协程进行处理。 协程匿名函数接收一个任务作为参数,并进行处理。
func Process2(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func(t string) { fmt.Printf("Worker start process task: %s\n", t) }(task) }}
3.项目中经常需要编写单元测试,而单元测试最常见的是table-driven风格的测试,如下所示: 待测函数很简单,只是计算输入数值的2倍值。
func Double(a int) int { return a * 2}
测试函数如下:
func TestDouble(t *testing.T) { var tests = []struct { name string input int expectOutput int }{ { name: "double 1 should got 2", input: 1, expectOutput: 2, }, { name: "double 2 should got 4", input: 2, expectOutput: 4, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.expectOutput != Double(test.input) { t.Fatalf("expect: %d, but got: %d", test.input, test.expectOutput) } }) }}
上述测试函数也很简单,通过设计多个测试用例,标记输入输出,使用子测试进行验证。
上述三个函数是否有问题?
有个共同点就是都引用了循环变量。即在for index, value := range xxx
语句中,
index和value便是循环变量。不同点是循环变量的使用方式,有的是直接在协程中引用(1),有的作为参数传递(2),而3则是兼而有之。
回答以上问题,记住以下两点即可。
首先,循环变量实际上只是一个普通的变量。
语句for index, value := range xxx
中,每次循环index
和value
都会被重新赋值(并非生成新的变量)。
如果循环体中会启动协程(并且协程会使用循环变量),就需要格外注意了,因为很可能循环结束后协程才开始执行 , 此时,所有协程使用的循环变量有可能已被改写。(是否会改写取决于引用循环变量的方式)
1.(1)中,协程函数体中引用了循环变task
,协程从被创建到被调度执行期间循环变量极有可能被改写,这种情况下,我们称之为变量没有绑定。函数1 打印结果是混乱的。很有可能(随机)所有协程执行的task都是列表中的最后一个task。
task
,而是使用的参数。而在创建协程时,循环变量task
作为函数参数传递给了协程。参数传递的过程实际上也生成了新的变量,也即间接完成了绑定。所以,题目二实际上是没有问题的。test.name
通过函数参数完成了绑定,而test.input
和 test.expectOutput
则没有绑定。然而题目三实际执行却不会有问题,因为t.Run(...)
并不会启动新的协程,也就是循环体并没有并发。此时,即便循环变量没有绑定也没有问题。
但是风险在于,如果t.Run(...)执行的测试体有可能并发(比如通过t.Parallel()),此时就极有可能引入问题。对于3中的测试用例,建议显式地绑定,例如:
for _, test := range tests { tc := test // 显式绑定,每次循环都会生成一个新的tc变量 t.Run(tc.name, func(t *testing.T) { if tc.expectOutput != Double(tc.input) { t.Fatalf("expect: %d, but got: %d", tc.input, tc.expectOutput) } }) }
通过tc := test显式地绑定,每次循环会生成一个新的变量。
简单点来说
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。