Go语言的那些坑

释放双眼,带上耳机,听听看~!

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

文件名字不要轻易以__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文件集体放到一个目录中,否则会编译不过的。

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

现有如下程序:


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

运行指令go run test.go

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


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

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1package main
2
3import "fmt"
4
5func main(){
6    defer func(){
7        fmt.Println("1")
8    }()
9
10    defer func(){
11        fmt.Println("2")
12    }()
13
14    defer func(){
15        fmt.Println("3")
16    }()
17
18
19}
20复制代码
21

对应的输出是:


1
2
3
4
5
13
22
31
4复制代码
5

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


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

输出:


1
2
3
1[12312]
2复制代码
3

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


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

所以,结果输出为:


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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1import "fmt"
2
3type student struct{
4    Name string
5    Age  int
6}
7
8func (p *student) love(){
9    fmt.Println("love")
10
11}
12
13func (p *student) like(){
14    fmt.Println("like first")
15    p.love()
16}
17
18type boy struct {
19    student
20}
21
22func (b * boy) love(){
23    fmt.Println("hate")
24}
25
26func main(){
27
28    b := boy{}
29
30    b.like()
31}
32复制代码
33

输出:


1
2
3
4
1like first
2love
3复制代码
4

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
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}
13复制代码
14

输出:


1
2
3
11
2复制代码
3

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


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

输出:


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

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

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


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

输出:


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

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1 import &quot;fmt&quot;
2
3
4func main(){
5    ch := make(chan int, 1)
6
7    ch &lt;- 1
8
9    select {
10    case msg :=&lt;-ch:
11        fmt.Println(msg)
12    default:
13        fmt.Println(&quot;default&quot;)
14    }
15
16    fmt.Println(&quot;success&quot;)
17}
18复制代码
19

输出:


1
2
3
4
11
2success
3复制代码
4

default可以判断chan是否已经满了


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1import &quot;fmt&quot;
2
3
4func main(){
5    ch := make(chan int, 1)
6
7    select {
8    case msg :=&lt;-ch:
9        fmt.Println(msg)
10    default:
11        fmt.Println(&quot;default&quot;)
12    }
13
14    fmt.Println(&quot;success&quot;)
15}
16复制代码
17

输出:


1
2
3
4
1default
2success
3复制代码
4

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

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

变量定义基本方式为:


1
2
3
1var 发量名字 类型 = 表达式
2复制代码
3

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

  • 数值变量对应的是0值

  • 布尔变量对应的是false

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

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

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


1
2
3
4
1 var s string
2 fmt.Println(s) // &quot;&quot;
3复制代码
4

:=注意的问题

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

1
2
3
4
5
1in, err := os.Open(infile)
2// TODO
3out, err := os.Create(outfile)
4复制代码
5

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

例如:


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

以上是正确的。

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

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

请看例子:


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

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

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

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

Golang中没有“对象”


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1package main
2
3import (
4    &quot;fmt&quot;
5)
6type test struct {
7    name string
8}
9func (t *test) getName(){
10    fmt.Println(&quot;hello world&quot;)
11}
12func main() {
13    var t *test
14    t = nil
15    t.getName()
16}
17复制代码
18

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

输出为:


1
2
3
1hello world
2复制代码
3

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

调用getName方法,在Go中还可以转换,转换为:Type.method(t Type, arguments) 所以,以上代码main函数中还可以写成:


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

Go中的指针*符号的含义

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

例如:


1
2
3
4
1a := 1
2b := &amp;a
3复制代码
4

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

下面对a的值加一


1
2
3
4
5
1a := 1
2b := &amp;a
3*b++
4复制代码
5

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

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

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


1
2
3
4
5
6
7
8
9
10
1package main
2import (
3    &quot;fmt&quot;
4    &quot;os&quot;
5)
6func main() {
7    fmt.Println(os.Args[0])
8}
9复制代码
10

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

输出:./main

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

请看下列代码:


1
2
3
4
5
6
7
8
9
10
11
12
1import (
2    &quot;fmt&quot;
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}
11复制代码
12

请问输出什么? 答案是:


1
2
3
1[10 21]
2复制代码
3

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


1
2
3
4
5
6
7
8
9
10
11
12
1import (
2    &quot;fmt&quot;
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}
11复制代码
12

输出为:


1
2
3
1[10 20]
2复制代码
3

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

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

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


1
2
3
4
5
6
7
8
9
10
1import (
2    &quot;fmt&quot;
3)
4
5func main(){
6    newMap := make(map[string]int)
7    fmt.Println(newMap[&quot;a&quot;])
8}
9复制代码
10

答案是:


1
2
3
10
2复制代码
3

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

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

请看下面的例子:


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

输出:


1
2
3
4
5
6
7
8
9
10
11
12
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 6
10key is 8, value is 8
11复制代码
12

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

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

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

例如:只可以发送值


1
2
3
4
5
1func setData(ch chan &lt;- string){
2    //TODO
3}
4复制代码
5

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

如下是只可以取值:


1
2
3
4
5
1func setData(ch &lt;- chan  string){
2    //TODO
3}
4复制代码
5

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

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


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

以上代码的执行流程是怎样的呢? 一个基于无缓存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之间通信的,且通信过程中会阻塞。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

c++ list, vector, map, set 区别与用法比较

2022-1-11 12:36:11

安全资讯

招财宝坏账3个亿 担保方广发银行报案了

2016-12-26 19:50:02

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索