大家好,我是ProgrammerSpectre。由于语法简单,Go给人的印象是容易上手。不过初学者还是比较容易犯一些错误的。本文总结了5个常见错误。你检查过自己了吗?!这些是我写Go时犯的错误。希望对您有所帮助!1.循环在循环内。有几种情况可能会引起Confused,你需要搞清楚。1.1.在循环迭代器变量中使用引用出于效率原因,通常使用单个变量来循环迭代器。有时,由于每次循环迭代时的值不同,这会导致未知行为。例如:in:=[]int{1,2,3}varout[]*intfor_,v:=rangein{out=append(out,&v)}fmt.Println("Values:",*out[0],*out[1],*out[2])fmt.Println("Addresses:",out[0],out[1],out[2])Output:Values:333Addresses:0xc0000141880xc0000141880xc000014188你惊讶吗?out这个切片中的元素都是3。其实很容易解释为什么会这样:在每次迭代中,我们将v附加到out切片。因为v是单个变量(内存地址不变),所以每次迭代都采用一个新值。第二行输出证明地址是相同的,并且它们都指向相同的值。简单的解决方法是将循环迭代器变量复制到一个新变量中:in:=[]int{1,2,3}varout[]*intfor_,v:=rangein{v:=vout=append(out,&v)}fmt.Println("值:",*out[0],*out[1],*out[2])fmt.Println("地址:",out[0],out[1],out[2])Newoutput:Values:123Addresses:0xc0000b60100xc0000b60180xc0000b6020在goroutine中使用循环迭代变量也会有同样的问题。list:=[]int{1,2,3}for_,v:=rangelist{gofunc(){fmt.Printf("%d",v)}()}输出将是:333可以使用与以上解决方案修复。请注意,如果您不使用goroutine来运行该函数,代码将按预期工作。这个错误错误率很高,要特别注意!!1.2.循环调用WaitGroup.Wait,看一段代码:varwgsync.WaitGroupwg.Add(len(tasks))for_,t:=rantasks{gofunc(t*task){defergroup.Done()}(t)//group.Wait()}group.Wait()WaitGroup常用于等待多个goroutine完成。但是如果在循环内部调用Wait,在代码的第7行,结果不是您所期望的。错误率应该比较低。1.3.在循环中使用defer,因为defer的执行时机是在函数返回之前。因此,除非您确切地知道自己在做什么,否则通常不应在循环内使用defer。看一段代码:varmutexsync.MutextypePersonstruct{Ageint}persons:=make([]Person,10)for_,p:=rangepersons{mutex.Lock()//defermutex.Unlock()p.Age=13mutex.Unlock()}在上面的例子中,如果使用第8行而不是第10行,下一次迭代将无法获取互斥锁,因为锁没有被释放,因此循环将永远阻塞。如果你真的需要在循环中使用defer,你可以通过委托另一个函数来实现:varmutexsync.MutextypePersonstruct{Ageint}persons:=make([]Person,10)for_,p:=rangepersons{func(){mutex.Lock()defermutex.Unlock()p.Age=13}()}2.Channel阻塞一般认为goroutine+channel是Go的利器。Go强调的不是通过共享内存来通信,而是通过通信来共享内存。但是在使用channel的过程中需要注意阻塞问题,避免goroutine泄露。例如下面的代码:funcdoReq(timeouttime.Duration)obj{//ch:=make(chanobj)ch:=make(chanobj,1)gofunc(){obj:=do()ch<-result}()select{caseresult=<-ch:returnresultcase<-time.After(timeout):returnil}}查看上面代码的doReq函数,它在第4行创建了一个子goroutine来处理请求,这是常见的做法去服务器程序。子goroutine执行do函数并将结果通过第6行的通道ch发送回父goroutine。子goroutine将在第6行阻塞,直到父goroutine在第9行收到来自ch的结果。同时,父goroutine将阻塞选择,直到子goroutine将结果发送到ch(第9行)或超时(第11行)。如果先超时,parentgoroutine会从doReqline12返回,导致没有goroutines从ch读取,childgoroutine会卡在第6行。解决方法是把ch从unbufferedchannel改成bufferedchannelchannel,因此即使在父goroutine退出后,子goroutine也可以始终发送结果。这个错误的概率不会低。还有一点需要特别注意的是time.After引起的内存泄露,只要程序不经常执行上面的select即可(毕竟time.After到时候还是会回收资源的)。3.不使用接口接口可以使代码更加灵活。这是在代码中引入多态性的一种方法。接口允许您定义一组行为而不是特定类型。不使用接口可能不会导致任何错误,但会导致代码简单性、灵活性和可扩展性降低。在Go接口中,io.Reader和io.Writer可能是最常用的。typeReaderinterface{Read(p[]byte)(nint,errerror)}typeWriterinterface{Write(p[]byte)(nint,errerror)}这几个接口很强大,假设你想把对象写入文件,你可以定义一个Save方法:func(o*obj)Save(fileos.File)error如果第二天要写入http.ResponseWriter,显然不适合再创建一个Save方法,那么应该使用io.Writer:func(o*obj)Save(wio.Writer)error另外,你应该知道的重要说明,始终关注行为。在上面的示例中,您只需要Write方法,尽管io.ReadWriteCloser也可以。接口越大,抽象越弱。在Go中,普遍提倡小接口。因此,我们应该优先使用接口而不是具体类型。4、不注意结构体字段顺序的问题,不会导致程序出错,但可能会占用更多的内存。看一个例子:typeBadOrderedPersonstruct{Veteranbool//1byteNamestring//16byteAgeint32//4byte}typeOrderedPersonstruct{NamestringAgeint32Veteranbool}貌似两种类型都占21个字节,但事实并非如此。我们用GOARCH=amd64编译代码,发现BadOrderedPerson类型占用了32个字节,而OrderedPerson类型只占用了24个字节。为什么?原因是数据结构对齐[1]。在64位架构上,内存分配为连续的8字节数据。需要添加的padding可以通过以下方式计算:padding=(align-(offsetmodalign))modalignaligned=offset+padding=offset+((align-(offsetmodalign))modalign)typeBadOrderedPersonstruct{Veteranbool//1byte_[7]byte//7byte:paddingforalignmentNamestring//16byteAgeint32//4byte_struct{}//topreventunkeyedliterals//zerosizedvalues,likestruct{}and[0]byteoccurringat//theendofastructureareassumedtohaveasizeofonebyte.//sopaddingalsowillbeaddeddhereaswell.}typeOrderedPersonstruct{NamestringAgeint32Veteranbool_struct{}}当你使用大型常用类型那时,可能会导致性能问题。但别担心,您不必手动处理所有结构。这个工具可以轻松解决此类问题:https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment。5.在测试中不使用竞争检测器数据竞争会导致莫名其妙的失败,通常是在代码已经在线部署很久之后。因此,它们是并发系统中最常见和最难调试的错误类型。为了帮助区分此类错误,Go1.1引入了一个内置的竞争检测器。只需添加-race标志即可使用。$gotest-racepkg#totestthepackage$gorun-racepkg.go#torunthesourcefile$gobuild-race#tobuildthepackage$goinstall-racepkg#toinstallthepackage当启用数据竞争检测器时,编译器将在代码中记录访问内存的时间和方式,而运行时监控对共享变量的非同步访问。当发现数据竞争时,竞争检测器将打印一份报告,其中包括违规访问的堆栈跟踪。这是一个示例:WARNING:DATARACEReadbygoroutine185:net.(*pollServer).AddFD()src/net/fd_unix.go:89+0x398net.(*pollServer).WaitWrite()src/net/fd_unix.go:247+0x45net。(*netFD).Write()src/net/fd_unix.go:540+0x4d4net.(*conn).Write()src/net/net.go:129+0x101net.func060()src/net/timeout_test.go:603+0xafPreviouswritebygoroutine184:net.setWriteDeadline()src/net/sockopt_posix.go:135+0xdfnet.setDeadline()src/net/sockopt_posix.go:144+0x9cnet.(*conn).SetDeadline()src/net/net.go:161+0xe3net.func·061()src/net/timeout_test.go:616+0x3edGoroutine185(running)createdat:net.func·061()src/net/timeout_test.go:609+0x288Goroutine184(running)createdat:net.TestProlongTimeout()src/net/timeout_test.go:618+0x298testing.tRunner()src/testing/testing.go:301+0xe8总结错误并不可怕,但我们要吸取教训,避免陷入又是同一个坑。当我们掉进坑里的时候,我们应该想办法找出原因。如果知道为什么,下次掉坑的可能性就会小很多。原文地址:https://hackernoon.com/dont-make-these-5-golang-mistakes-3l3x3wcw参考文献【1】数据结构对齐:https://en.wikipedia.org/wiki/Data_structure_alignment
