AICon上海|与字节、阿里、腾讯等企业共同探索Agent 时代的落地应用 了解详情
写点什么

Go 语言的那些坑

  • 2019-09-23
  • 本文字数:5844 字

    阅读完需:约 19 分钟

Go语言的那些坑

1. 背景

Golang 是我最喜欢的一门语言,它简洁、高效、易学习、开发效率高、还可以编译成机器码。虽然它一出世,就饱受关注,而且现在在市面上逐渐流行开来,但是,它毕竟是一门新兴语言,还有很多让人不太习惯的地方(即坑),我们一边学习,一边踩坑,希望对大家有借鉴作用。

2. 详述

1)文件名字不要轻易以__test.go 为结尾


Golang 的 source 文件的命名和其他语言本无差别,但是 Golang 自带 Unit test,它的 unit test 有个小规范:所有 unit test 文件都要以__test.go 为结尾!


所以,当你命名一个非 unit test 文件为 XXX_test.go,而且执意要编译时,就会报错:no buildable Go source files in XXXXXX(你的文件路径)。


所以,切记,以__test.go 为结尾的都是 unit test 的文件,且切记不要把 unit test 文件和普通 Go 文件放到一起,一定要把 unit test 文件集体放到一个目录中,否则会编译不过的。


2)语句 fmt.Println(“这里是汉字:” + 字符串变量)字符串变量的值打印不出来的问题


现有如下程序:


 1package main 2 3import "fmt" 4 5func main()  { 6    m1 := getString() 7 8    fmt.Println("现在是:" + m1) 9}1011func getString()string{12    return "abd"13}
复制代码


运行指令 go run test.go



是单独打印变量 m1 却可以正常显示


 1import "fmt" 2 3func main()  { 4    m1 := getString() 5 6    fmt.Println(m1) 7 8    fmt.Println("现在是:" + m1) 9}1011func getString()string{12    return "abd"13}
复制代码



是为什么呢?很奇怪啊!


其实这要怪 IDE,我的 IDE 是 phpstorm + Golang 插件包,IDE 自带的 console 对中文的支持很不友好,带中文的字符串打印出来后,容易显示不全,其实通过 terminal 打印出来,是正确的!



3)多个 defer 出现的时候,多个 defer 之间按照 LIFO(后进先出)的顺序执行


 1package main 2 3import "fmt" 4 5func main(){ 6    defer func(){ 7        fmt.Println("1") 8    }() 910    defer func(){11        fmt.Println("2")12    }()1314    defer func(){15        fmt.Println("3")16    }()171819}
复制代码


对应的输出是:


132231
复制代码


4)panic 中可以传任何值,不仅仅可以传 string


 1package main 2 3import "fmt" 4 5func main(){ 6 7    defer func(){ 8        if r := recover();r != nil{ 9            fmt.Println(r)10        }11    }()1213    panic([]int{12312})14}
复制代码


输出:


1[12312]
复制代码


5)用 for range 来遍历数组或者 map 的时候,被遍历的指针是不变的,每次遍历仅执行 struct 值的拷贝


 1import "fmt" 2 3type student struct{ 4    Name string 5    Age  int 6} 7 8func main(){ 9    var stus []student1011    stus = []student{12        {Name:"one", Age: 18},13        {Name:"two", Age: 19},14    }1516    data := make(map[int]*student)1718    for i, v := range stus{19        data[i] = &v   //应该改为:data[i] = &stus[i]20    }2122    for i, v := range data{23        fmt.Printf("key=%d, value=%v \n", i,v)24    }25}
复制代码


所以,结果输出为:


1key=0, value=&{two 19} 2key=1, value=&{two 19}
复制代码


6)Go 中没有继承!没有继承!Go 中是叫组合!是组合!


 1import "fmt" 2 3type student struct{ 4    Name string 5    Age  int 6} 7 8func (p *student) love(){ 9    fmt.Println("love")1011}1213func (p *student) like(){14    fmt.Println("like first")15    p.love()16}1718type boy struct {19    student20}2122func (b * boy) love(){23    fmt.Println("hate")24}2526func main(){2728    b := boy{}2930    b.like()31}
复制代码


输出:


1like first2love
复制代码


7)不管运行顺序如何,当参数为函数的时候,要先计算参数的值


 1func main(){ 2    a := 1 3    defer print(function(a)) 4    a = 2; 5} 6 7func function(num int) int{ 8    return num 9}10func print(num int){11    fmt.Println(num)12}
复制代码


输出:


11
复制代码


8)注意是 struct 的函数,还是* struct 的函数


 1import "fmt" 2 3type people interface { 4    speak() 5} 6 7type student struct{ 8    name string 9    age int10}11func (stu *student) speak(){12    fmt.Println("I am a student, I am ", stu.age)13}141516func main(){17    var p people18    p = student{name:"RyuGou", age:12} //应该改为 p = &student{name:"RyuGou", age:12}19    p.speak()20}
复制代码


输出:


1cannot use student literal (type student) as type people in assignment:2student does not implement people (speak method has pointer receiver)
复制代码


9)make(chan int) 和 make(chan int, 1)是不一样的


chan 一旦被写入数据后,当前 goruntine 就会被阻塞,知道有人接收才可以(即 " <- ch"),如果没人接收,它就会一直阻塞着。而如果 chan 带一个缓冲,就会把数据放到缓冲区中,直到缓冲区满了,才会阻塞


 1import "fmt" 2 3 4func main(){ 5    ch := make(chan int) //改为 ch := make(chan int, 1) 就好了 6 7    ch <- 1 8 9    fmt.Println("success")10}
复制代码


输出:


1fatal error: all goroutines are asleep - deadlock!
复制代码


10)golang 的 select 的功能和 select, poll, epoll 相似, 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。


select 的代码形式和 switch 非常相似, 不过 select 的 case 里的操作语句只能是"IO 操作"(不仅仅是取值<-channel,赋值 channel<-也可以), select 会一直等待等到某个 case 语句完成,也就是等到成功从 channel 中读到数据。 则 select 语句结束


 1 import "fmt" 2 3 4func main(){ 5    ch := make(chan int, 1) 6 7    ch <- 1 8 9    select {10    case msg :=<-ch:11        fmt.Println(msg)12    default:13        fmt.Println("default")14    }1516    fmt.Println("success")17}
输出:
112success
default可以判断chan是否已经满了
1import "fmt" 2 3 4func main(){ 5 ch := make(chan int, 1) 6 7 select { 8 case msg :=<-ch: 9 fmt.Println(msg)10 default:11 fmt.Println("default")12 }1314 fmt.Println("success")15}
复制代码


输出:


1default2success
复制代码


此时因为 ch 中没有写入数据,为空,所以 case 不会读取成功。 则 select 执行 default 语句。


11)Go 语言中不存在未初始化的变量


变量定义基本方式为:


1var 发量名字 类型 = 表达式


其中类型和表达式均可省略,如果初始化表达式被省略,将用零值初始化该变量。


数值变量对应的是 0 值


布尔变量对应的是 false


字符串对应的零值是空字符串


接口或者引用类型(包括 slice,map,chan)变量对应的是 nil


数组或者结构体等聚合类型对应的零值是每个元素或字段对应该类型的零值。


1 var s string 2 fmt.Println(s) // ""
复制代码


12):=注意的问题


使用:=定义的变量,仅能使用在函数内部。


在定义多个变量的时候:=周围不一定是全部都是刚刚声明的,有些可能只是赋值,例如下面的 err 变量


Go<br />in, err := os.Open(infile)<br />// TODO<br />out, err := os.Create(outfile)<br />
复制代码


13)new 在 Go 语言中只是一个预定义的函数,它并不是一个关键字,我们可以将 new 作为变量或者其他


例如:


1func delta(old, new int) int { 2    return new - old 3}
复制代码


以上是正确的。


14)并不是使用 new 就一定会在堆上分配内存


编译器会自动选择在栈上还是在堆上分配存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。


请看例子:


 1var global *int  2 3func f() { 4    var x int x=1  5    global = &x 6} 7 8func g() { 9    y := new(int)10    *y = 1 11}
复制代码


f()函数中的 x 就是在堆上分配内存,而 g()函数中的 y 就是分配在栈上。


15)init 函数在同一个文件中可以包含多个


在同一个包文件中,可以包含有多个 init 函数,多个 init 函数的执行顺序和定义顺序一致。


16)Golang 中没有“对象”


 1package main 2 3import ( 4    "fmt" 5) 6type test struct { 7    name string 8} 9func (t *test) getName(){10    fmt.Println("hello world")11}12func main() {13    var t *test14    t = nil15    t.getName()16}
复制代码


能正常输出吗?会报错吗?


输出为:


1hello world
复制代码


可以正常输出。Go 本质上不是面向对象的语言,Go 中是不存在 object 的含义的,Go 语言书籍中的对象也和 Java、PHP 中的对象有区别,不是真正的”对象”,是 Go 中 struct 的实体。


调用 getName 方法,在 Go 中还可以转换,转换为:Type.method(t Type, arguments)


所以,以上代码 main 函数中还可以写成:


1func main() {2    (*test).getName(nil)3}
复制代码


17)Go 中的指针*符号的含义


&的意思大家都明白的,取地址,假如你想获得一个变量的地址,只需在变量前加上 &即可。


例如:


1a := 12b := &a
复制代码


现在,我拿到 a 的地址了,但是我想取得 a 指针指向的值,该如何操作呢?用*号,*b 即可。


*的意思是对指针取值。


下面对 a 的值加一


1a := 12b := &a3*b++
复制代码


和 &可以相互抵消,同时注意,&可以抵消,但是 &不可以;所以 a 和 &a 是一样的,和*&&&a 也是一样的。


18)os.Args 获取命令行指令参数,应该从数组的 1 坐标开始


os.Args 的第一个元素,os.Args[0], 是命令本身的名字


1package main2import (3    "fmt"4    "os"5)6func main() {7    fmt.Println(os.Args[0])8}
复制代码


以上代码,经过 go build 之后,打包成一个可执行文件 main,然后运行指令./main 123


输出:./main


19)数组切片 slice 的容量问题带来的 bug


请看下列代码:


 1import ( 2    "fmt" 3) 4func main(){ 5    array := [4]int{10, 20, 30, 40} 6    slice := array[0:2] 7    newSlice := append(slice, 50) 8    newSlice[1] += 1 9    fmt.Println(slice)10}
复制代码


请问输出什么?


答案是:


1[10 21]
复制代码


如果稍作修改,将以上 newSlice 改为扩容三次,newSlice := append(append(append(slice, 50), 100), 150)如下:


 1import ( 2    "fmt" 3) 4func main(){ 5    array := [4]int{10, 20, 30, 40} 6    slice := array[0:2] 7    newSlice := append(append(append(slice, 50), 100), 150) 8    newSlice[1] += 1 9    fmt.Println(slice)10}
复制代码


输出为:


1[10 20]
复制代码


这特么是什么鬼?


这就要从 Golang 切片的扩容说起了;切片的扩容,就是当切片添加元素时,切片容量不够了,就会扩容,扩容的大小遵循下面的原则:(如果切片的容量小于 1024 个元素,那么扩容的时候 slice 的 cap 就翻番,乘以 2;一旦元素个数超过 1024 个元素,增长因子就变成 1.25,即每次增加原来容量的四分之一。)如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组(这就是产生 bug 的原因);如果扩容之后,超过了原数组的容量,那么,Go 就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。


建议尽量避免 bug 的产生。


20)map 引用不存在的 key,不报错


请问下面的例子输出什么,会报错吗?


1import (2    "fmt"3)45func main(){6    newMap := make(map[string]int)7    fmt.Println(newMap["a"])8}
复制代码


答案是:


10
复制代码


不报错。不同于 PHP,Golang 的 map 和 Java 的 HashMap 类似,Java 引用不存在的会返回 null,而 Golang 会返回初始值


21)map 使用 range 遍历顺序问题,并不是录入的顺序,而是随机顺序


请看下面的例子:


 1import ( 2    "fmt" 3) 4 5func main(){ 6    newMap := make(map[int]int) 7    for i := 0; i < 10; i++{ 8        newMap[i] = i 9    }10    for key, value := range newMap{11        fmt.Printf("key is %d, value is %d\n", key, value)12    }13}
复制代码


输出:


 1key is 1, value is 1 2key is 3, value is 3 3key is 5, value is 5 4key is 7, value is 7 5key is 9, value is 9 6key is 0, value is 0 7key is 2, value is 2 8key is 4, value is 4 9key is 6, value is 610key is 8, value is 8
复制代码


是杂乱无章的顺序。map 的遍历顺序不固定,这种设计是有意为之的,能为能防止程序依赖特定遍历顺序。


22)channel 作为函数参数传递,可以声明为只取(<- chan)或者只发送(chan <-)


一个函数在将 channel 作为一个类型的参数来声明的时候,可以将 channl 声明为只可以取值(<- chan)或者只可以发送值(chan <-),不特殊说明,则既可以取值,也可以发送值。


例如:只可以发送值


1func setData(ch chan <- string){2    //TODO3}
复制代码


如果在以上函数中存在<-ch 则会编译不通过。


如下是只可以取值:


1func setData(ch <- chan  string){2    //TODO3}
复制代码


如果以上函数中存在 ch<-则在编译期会报错


23)使用 channel 时,注意 goroutine 之间的执行流程问题


 1package main 2import ( 3    "fmt" 4) 5func main(){ 6    ch := make(chan string) 7    go setData(ch) 8    fmt.Println(<-ch) 9    fmt.Println(<-ch)10    fmt.Println(<-ch)11    fmt.Println(<-ch)12    fmt.Println(<-ch)13}14func setData(ch  chan  string){15    ch <- "test"16    ch <- "hello wolrd"17    ch <- "123"18    ch <- "456"19    ch <- "789"20}
复制代码


以上代码的执行流程是怎样的呢?


一个基于无缓存 channel 的发送或者取值操作,会导致当前 goroutine 阻塞,一直等待到另外的一个 goroutine 做相反的取值或者发送操作以后,才会正常跑。


以上例子中的流程是这样的:


主 goroutine 等待接收,另外的那一个 goroutine 发送了“test”并等待处理;完成通信后,打印出”test”;两个 goroutine 各自继续跑自己的。


主 goroutine 等待接收,另外的那一个 goroutine 发送了“hello world”并等待处理;完成通信后,打印出”hello world”;两个 goroutine 各自继续跑自己的。


主 goroutine 等待接收,另外的那一个 goroutine 发送了“123”并等待处理;完成通信后,打印出”123”;两个 goroutine 各自继续跑自己的。


主 goroutine 等待接收,另外的那一个 goroutine 发送了“456”并等待处理;完成通信后,打印出”456”;两个 goroutine 各自继续跑自己的。


主 goroutine 等待接收,另外的那一个 goroutine 发送了“789”并等待处理;完成通信后,打印出”789”;两个 goroutine 各自继续跑自己的。


记住:Golang 的 channel 是用来 goroutine 之间通信的,且通信过程中会阻塞。


作者介绍:


刘刚,贝壳找房研发工程师,目前负责贝壳找房运维开发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/14WJ6FMNX9YQm26aaHDE_w


2019-09-23 09:371516

评论 1 条评论

发布
用户头像
不管运行顺序如何,当参数为函数的时候,要先计算参数的值——结果应该是1吧?

11

2023-02-07 17:10 · 上海
回复
没有更多了
发现更多内容

学习Typescript(一)

bo

前端 ts 7月月更

Snowflake vs. Databricks谁更胜一筹?2022年最新战报

雨果

数据中台 数据平台 snowflake DaaS数据即服务 Databricks

2200字详细讲解计算机网络的“流量管理员”:QoS 服务质量

wljslmz

流量控制 QoS 网络技术 7月月更

为啥国内大厂都把云计算当成香饽饽,这个万亿市场你真的了解吗

雨果

DaaS数据即服务

音乐体验天花板!14个网易云音乐的情感化设计细节

张姣发

创新 交互设计 网易云音乐

C# 窗体应用常用基础控件讲解(适合萌新)

IC00

C# 7月月更

如何使用TDengine Sink Connector?

TDengine

tdengine Kafk

关于 StatefulWidget,你不得不知道的原理和要点!

岛上码农

flutter ios 前端 安卓开发 签约计划第三季

Bootstrap的导航元素和Well详解【前端Bootstrap框架】

恒山其若陋兮

7月月更

次轮Okaleido Tiger即将登录Binance NFT,引发社区热议

鳄鱼视界

客户案例 | 关注老年用户体验,银行APP适老化改造要避虚就实

易观分析

用户体验 银行app 老龄化

Qt | 关于Qt Creator打开项目编译不过的问题

YOLO.

qt 7月月更

语音直播系统——开发推送通知需要遵守的原则

开源直播系统源码

聊聊自动化测试的度量指标

老张

自动化测试 质量度量

数据资产管理的概念

奔向架构师

数据资产 7月月更

C# 中的转译字符'/b'

陈言必行

7月月更

缓存一致性与内存屏障

蝉沐风

volatile 内存屏障 缓存一致性 MESI

一骑入秦川——浅聊Beego AutoRouter是如何工作

Regan Yue

Go 源码刨析 Go web 签约计划第三季 Beego

Okaleido Tiger 7.27日登录Binance NFT,首轮已获不俗成绩

BlockChain先知

Plato Farm通过LaaS协议Elephant Swap,为社区用户带来全新体验

股市老人

传统车企数字化转型如何打通最后一公里?

雨果

车联网 DaaS数据即服务

React 学习记录2📝

程序员海军

React 7月月更

Prometheus 运维工具 Promtool (三)Debug 功能

耳东@Erdong

Prometheus 7月月更 Promtool

平成千字文(へいせいせんじもん)    (平成12年9月10日 石渡 明 作)  宇宙広遠 銀河永久 日月運行 不乱無休 地球公転 季節変移 黄道星座 太陽年周 故郷群島 南熱北冷 海洋温暖 気候順良 青空飛雲 諸野深緑 湖泉静息 谷川清流 春桜一面 新芽

贾献华

7月月更

次轮Okaleido Tiger即将登录Binance NFT,引发社区热议

股市老人

小程序怎样助力智能家居生态新模式

Geek_99967b

物联网

Go语言的那些坑_文化 & 方法_刘刚_InfoQ精选文章