NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

评论 1 条评论

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

11

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

【2024最新版】Sapphire视觉特效插件功能介绍 附蓝宝石插件破解补丁

南屿

视觉特效插件 Boris FX Sapphire 蓝宝石插件

指标平台详解(上):为什么有了 BI ,还需要指标平台?

Aloudata

BI 指标体系 ETL BI 分析工具 指标中台

矩阵起源通过2023年“专精特新”中小企业认定!

MatrixOrigin

云原生 数据库· 分布式数据库 Matrixone

从零开始用Rust编写nginx,命令行参数的设计与解析及说明

不在线第一只蜗牛

nginx rust 开发语言

unitypro安装教程 Unity Pro 2018 Mac破解版下载安装

南屿

Unity Pro 2019 Unity Pro 安装教程 3D游戏动画开发工具

用Python实现高效数据记录!Web自动化技术助你告别重复劳动!

测吧(北京)科技有限公司

测试

测试管理 | 测试开发高薪私教线下班手把手带你提升职业技能

测吧(北京)科技有限公司

测试

如何实现一个百万亿规模的时序数据库,百度智能云 BTS 架构解析和实践分享

Baidu AICLOUD

时序数据库

哪里有BricsCAD中文版资源?最新2024版BricsCAD破解中文 for Mac安装包

南屿

CAD设计软件 BricsCAD 24破解版 BricsCAD 24下载

FTM 暴跌23% — 除了价格下跌之外还有什么?

Footprint Analytics

区块链 Token 代币 FTM

矩阵起源荣获《2023大数据产业年度国产化优秀代表厂商》

MatrixOrigin

云原生 分布式, 数据库· 分布式数据库 Matrixone

BOE(京东方)与JDG京东电竞俱乐部达成全面品牌战略合作 赋能多款高端电竞新品开启2024电竞产业“开门红”

科技热闻

React和Vue的有何不同?

伤感汤姆布利柏

青否数字人的源码之家!

青否数字人

数字人

zbrush2024新功能介绍 含zbrush2024下载破解资源

南屿

ZBrush 2024新功能 zbrush2024破解版 zbrush2024下载 zbrush雕刻

​比特币大跌的 2 个原因

TechubNews

一分钟带你搞定MySQL5.5安装教程

小魏写代码

云上业务一键性能调优,应用程序性能诊断工具 Btune 上线

Baidu AICLOUD

性能优化 运维监控

【新手快速入门】在线快速搭建AI原生应用

AI大咚咚

深入了解Redis数据结构

EquatorCoco

redis 数据结构 前端

良心推荐!五个超好用的Vue3工具

伤感汤姆布利柏

青否互动数字人的实时驱动!

青否数字人

数字人

wolframmathematica14激活密钥 mathematica mac破解安装教程

南屿

Wolfram Mathematica下载 Wolfram Mathematica密钥 Mathematica 14新功能 数学软件Mac版

NebulaGraph is nothing without you | 社区 2023 年度人物合集

NebulaGraph

和鲸CEO范向伟入选2022年上海市东方英才计划创业项目领军创业人才

ModelWhale

人工智能 创业 AI 东方英才 前沿产业

聚焦AI4S,产学研专家齐聚,探讨AI工具在多领域应用的现状与趋势

ModelWhale

livehome3dpro破解版 Mac室内设计软件 中文版Live Home 3D Pro下载

南屿

Mac软件 3D家庭室内设计工具 装修设计 livehome3dpro破解版

1Password 7 :为用户提供安全高效的密码管理

南屿

1Password 7 Mac密码管理器

从零开始:编写个性化的 Spring Boot 启动 Banner

Liam

Java 程序员 DevOps Spring Boot 后端

MCtalk·CEO对话 x 高成资本丨2024年,SaaS 还是不是一门好生意?

ToB行业头条

共话 AI for Science | 北京大学王超名:BrainPy,迈向数字化大脑的计算基础设施

ModelWhale

人工智能 AI 脑科学 AI4S 类脑智能

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