写点什么

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

评论 1 条评论

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

11

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

想做DBA,多租户管理你一定要知道这些

华为云开发者联盟

多租户 GaussDB(DWS) 资源池 存储空间 资源隔离

如何针对美工与设计师的Maya工具进行版本控制

龙智—DevSecOps解决方案

☕️【Java 技术之旅】带你一起攻克String类创建的难点分析

洛神灬殇

Java string pool string 6月日更

从零开始学习3D可视化之模型动画

ThingJS数字孪生引擎

可视化 模型 大屏可视化 数字时代 3D可视化

开源之夏来啦,欢迎报名 Apache APISIX 项目!

API7.ai 技术团队

开源 后端 技术人生 API 网关

待办事项列表,敏捷项目管理的核心工件

万事ONES

Scrum 敏捷 研发管理 ONES

面试官:如何给字符串设计索引?

一个优秀的废人

MySQL 索引 字符串 索引优化

准备3个月,面试10分钟,Java中高级岗面试为何越来越难?

Java架构师迁哥

毕昇JDK:为啥是ARM上超好用的JDK

华为云开发者联盟

Java 华为 jdk Openjdk 毕昇 JDK

Flink + Iceberg 在去哪儿的实时数仓实践

Apache Flink

flink

建信金科大咖访谈:ISO20000及ISO27001标准体系解读

金科优源汇

博云作为专业独立PaaS厂商,入选中国PaaS市场研究报告

BoCloud博云

PaaS

油管视频下载: 如何下载油管视频到本地

科技猫

分享 教程 经验 油管视频下载 下载油管视频

阿里云官方出品:全面总结阿里云云原生架构方法论与实践经验

尹文敏

云计算 阿里云 云原生

react源码解析9.diff算法

全栈潇晨

react源码

理解Linux之文件I/O——知其然,知其所以然

奔着腾讯去

文件管理 Linux内核 文件I/O I/O模型

企业应用AI之路怎么走?飞桨实践有真知

百度大脑

AI 飞桨

构建高可用的MySQL

林一

MySQ MySQL 高可用 Maxscale

程序员需要了解数据库知识么?

escray

学习 极客时间 朱赟的技术管理课 6月日更

质量分析工具-监控大厅大揭秘

anyRTC开发者

音视频 WebRTC sdk

基于传感器的人体生命体征监控技术

不脱发的程序猿

物联网 传感器 智能医疗 人体生命体征监控技术

我人生的里程碑之【作为独立开发者,第一次承接外包项目的心得经历,也许说出你的心声哦!】

洛神灬殇

程序人生 6月日更

宜兴牵手百度智能云共建人工智能应用中心,推动数字经济创新发展

百度大脑

人工智能

百度灵医智惠明星案例获人民日报点赞:智慧医疗让看病更便捷

百度大脑

人工智能 智慧医疗

☕️【Java技术之旅】带你一起探究String类不可变的特性

洛神灬殇

string 原理 字符串 6月日更

小白必看的,JS中循环语句大集合

华为云开发者联盟

JavaScript js 循环语句 while循环 for循环

如何科学制定和管理项目计划?

万事ONES

项目管理 ONES 项目经理

24道几乎必问的JVM面试题,我只会7道,你能答出几道?

北游学Java

Java 面试 JVM

福利时刻 十年黑客大佬的Web安全渗透技术分享

学神来啦

Linux 黑客 安全 运维自动化

Kubernetes学习笔记之Calico CNI Plugin源码解析(二)

360技术

带你剖析鸿蒙轻内核任务栈的源代码

华为云开发者联盟

鸿蒙 任务栈 任务调度 任务上下文

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