写点什么

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:371480

评论 1 条评论

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

11

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

14岁懂社会-《关于“工作的幸福”这件事儿》读书笔记

懒时小窝

14岁懂社会

网络营销之四大误解

源字节1号

微信小程序 前端开发 后端开发 网站开发

阅读Skeleton.css源码,改善睡眠质量(尽管它只有419行代码)

德育处主任

CSS 源码 前端 6月月更 skeleton.css

Flutter的特别之处在哪里

Geek_99967b

小程序 Flutter 小菜

今晚19:00知识赋能第2期直播丨OpenHarmony智能家居项目之控制面板界面设计

OpenHarmony开发者

Open Harmony

科普达人丨漫画图解什么是eRDMA?

阿里云弹性计算

大数据 TCP/IP RDMA

Dart 开发技巧

Geek_0a3437

flutter android dart 6月月更

ABAP-时间函数

桥下本有油菜花

abap

直播带货源码开发中,如何降低直播中的延迟?

开源直播系统源码

软件开发 直播系统 直播源码

用Python写一个简易机器人,超级简单!

王小王-123

python编写机器人 python项目 语法知识大全

OpenMLDB Meetup No.4 会议纪要

第四范式开发者社区

机器学习 数据库 特征平台 特征工程 实时

K8S V1.23 安装--Kubeadm+contained+公网 IP 多节点部署

云原生 k8s Kubernetes 集群

用Python编写学生成绩计算系统

王小王-123

Python 成绩计算系统 成绩项目 日常编程

IOS技术分享| iOS快速生成开发文档(二)

anyRTC开发者

ios objective-c 音视频 移动开发 Jazzy

国内首批!阿里云云原生数据湖产品通过信通院评测认证

阿里云大数据AI技术

大数据 运维 存储

【值得收藏】HTML5使用多种方法实现移动页面自适应手机屏幕的方法总结

迷彩

前端 自适应 HTML5, CSS3 6月月更

Vue3核心之响应式

Python研究所

6月月更

Go 语言入门很简单:Go 处理 XML 文件

宇宙之一粟

xml Go 语言 6月月更

​web前端培训 | JavaScript私有属性的实现方式

@零度

JavaScript 前端开发

我们如何拿到自己满意的薪资呢?这些套路还是需要掌握的

看山

闲聊

用Python自动化办公(csv项目实战)

王小王-123

csv python项目 自动化办公 大数据分割

利用Docker极速下载OpenJDK11源码

程序员欣宸

Docker Openjdk 6月月更

云技能提升好伙伴,亚马逊云师兄今天正式营业

亚马逊云科技 (Amazon Web Services)

亚马逊云

OLAP数据库引擎如何选型?

奇点云

OLAP 数据库引擎 OLAP数据库

优惠券种类那么多,先区分清楚再薅羊毛!

CRMEB

led背光板的作用是什么呢?

Dylan

LED LED显示屏

Flutter 中的 ValueNotifier 和 ValueListenableBuilder

坚果

flutter dart 6月月更

我的远程办公深度体验 | 社区征文

6个核桃

初夏征文

Scala 基础 (四):函数式编程【从基础到高阶应用】

百思不得小赵

scala 函数式编程 大数据开发 6月月更

用Python编写学生成绩管理系统(内附源码)

王小王-123

Python 系统设计 用python编写成绩管理系统 学生成绩管理系统

揭秘得物客服IM全链路通信过程

得物技术

前端 大前端 通信 IM 客服

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