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("这里是汉字:" + 字符串变量) 字符串变量的值打印不出来的问题

现有如下程序:

package main

import "fmt"

func main()  {

m1 := getString()

fmt.Println("现在是:" + m1)

}

func getString()string{

return "abd"

}

运行指令go run test.go

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

import "fmt"

func main()  {

m1 := getString()

fmt.Println(m1)

fmt.Println("现在是:" + m1)

}

func getString()string{

return "abd"

}

这是为什么呢?很奇怪啊!

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

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

package main

import "fmt"

func main(){

defer func(){

fmt.Println("1")

}()

defer func(){

fmt.Println("2")

}()

defer func(){

fmt.Println("3")

}()

}

对应的输出是:

3

2

1

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

package main

import "fmt"

func main(){

defer func(){

if r := recover();r != nil{

fmt.Println(r)

}

}()

panic([]int{12312})

}

输出:

[12312]

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

import "fmt"

type student struct{

Name string

Age  int

}

func main(){

var stus []student

stus = []student{

{Name:"one", Age: 18},

{Name:"two", Age: 19},

}

data := make(map[int]*student)

for i, v := range stus{

data[i] = &v  //应该改为:data[i] = &stus[i]

}

for i, v := range data{

fmt.Printf("key=%d, value=%v \n", i,v)

}

}

所以,结果输出为:

key=0, value=&{two 19}

key=1, value=&{two 19}

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

import "fmt"

type student struct{

Name string

Age  int

}

func (p *student) love(){

fmt.Println("love")

}

func (p *student) like(){

fmt.Println("like first")

p.love()

}

type boy struct {

student

}

func (b * boy) love(){

fmt.Println("hate")

}

func main(){

b := boy{}

b.like()

}

输出:

like first

love

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

func main(){

a := 1

defer print(function(a))

a = 2;

}

func function(num int) int{

return num

}

func print(num int){

fmt.Println(num)

}

输出:

1

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

import "fmt"

type people interface {

speak()

}

type student struct{

name string

age int

}

func (stu *student) speak(){

fmt.Println("I am a student, I am ", stu.age)

}

func main(){

var p people

p = student{name:"RyuGou", age:12} //应该改为 p = &student{name:"RyuGou", age:12}

p.speak()

}

输出:

cannot use student literal (type student) as type people in assignment:

student does not implement people (speak method has pointer receiver)

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

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

import "fmt"

func main(){

ch := make(chan int) //改为 ch := make(chan int, 1) 就好了

ch <- 1

fmt.Println("success")

}

输出:

fatal error: all goroutines are asleep - deadlock!

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

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

import "fmt"

func main(){

ch := make(chan int, 1)

ch <- 1

select {

case msg :=<-ch:

fmt.Println(msg)

default:

fmt.Println("default")

}

fmt.Println("success")

}

输出:

1

success

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

import "fmt"

func main(){

ch := make(chan int, 1)

select {

case msg :=<-ch:

fmt.Println(msg)

default:

fmt.Println("default")

}

fmt.Println("success")

}

输出:

default

success

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

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

变量定义基本方式为:

var 发量名字 类型 = 表达式

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

数值变量对应的是0值

布尔变量对应的是false

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

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

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

var s string

fmt.Println(s) // ""

:=注意的问题

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

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

in, err := os.Open(infile)

// TODO

out, err := os.Create(outfile)

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

例如:

func delta(old, new int) int {

return new - old

}

以上是正确的。

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

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

请看例子:

var global *int

func f() {

var x int x=1

global = &x

}

func g() {

y := new(int)

*y = 1

}

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

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

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

Golang中没有“对象”

package main

import (

"fmt"

)

type test struct {

name string

}

func (t *test) getName(){

fmt.Println("hello world")

}

func main() {

var t *test

t = nil

t.getName()

}

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

输出为:

hello world

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

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

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

func main() {

(*test).getName(nil)

}

Go中的指针*符号的含义

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

例如:

a := 1

b := &a

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

*的意思是对指针取值。

下面对a的值加一

a := 1

b := &a

*b++

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

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

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

package main

import (

"fmt"

"os"

)

func main() {

fmt.Println(os.Args[0])

}

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

输出:./main

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

请看下列代码:

import (

"fmt"

)

func main(){

array := [4]int{10, 20, 30, 40}

slice := array[0:2]

newSlice := append(slice, 50)

newSlice[1] += 1

fmt.Println(slice)

}

请问输出什么?

答案是:

[10 21]

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

import (

"fmt"

)

func main(){

array := [4]int{10, 20, 30, 40}

slice := array[0:2]

newSlice := append(append(append(slice, 50), 100), 150)

newSlice[1] += 1

fmt.Println(slice)

}

输出为:

[10 20]

这特么是什么鬼?

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

建议尽量避免bug的产生。

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

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

import (

"fmt"

)

func main(){

newMap := make(map[string]int)

fmt.Println(newMap["a"])

}

答案是:

0

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

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

请看下面的例子:

import (

"fmt"

)

func main(){

newMap := make(map[int]int)

for i := 0; i < 10; i++{

newMap[i] = i

}

for key, value := range newMap{

fmt.Printf("key is %d, value is %d\n", key, value)

}

}

输出:

key is 1, value is 1

key is 3, value is 3

key is 5, value is 5

key is 7, value is 7

key is 9, value is 9

key is 0, value is 0

key is 2, value is 2

key is 4, value is 4

key is 6, value is 6

key is 8, value is 8

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

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

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

例如:只可以发送值

func setData(ch chan <- string){

//TODO

}

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

如下是只可以取值:

func setData(ch <- chan  string){

//TODO

}

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

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

package main

import (

"fmt"

)

func main(){

ch := make(chan string)

go setData(ch)

fmt.Println(<-ch)

fmt.Println(<-ch)

fmt.Println(<-ch)

fmt.Println(<-ch)

fmt.Println(<-ch)

}

func setData(ch  chan  string){

ch <- "test"

ch <- "hello wolrd"

ch <- "123"

ch <- "456"

ch <- "789"

}

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

一个基于无缓存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之间通信的,且通信过程中会阻塞。

原文地址:https://www.cnblogs.com/ExMan/p/12401478.html

时间: 2024-08-30 18:28:58

Go语言的那些坑的相关文章

被C语言操作符优先级坑了

今天有一个枚举的题目的代码是这样的: 重点在于maxXor这个函数的实现,枚举两个数字,其中maxr保存了最大值的 i 异或 j , 可是这个程序执行结果大大出乎意外-_-. 然后就把 i 异或 j 的结果临时保存在int,进行比较,程序正确的执行了.原来是被操作符优先级坑到了.位操作的优先级比比较操作符的优先级更低. #include <map> #include <set> #include <list> #include <cmath> #includ

Go语言调用C语言函数的坑

最近在看人民邮电出版社 许式伟 吕桂华编著的<Go语言编程>,看到[1.2.9 语言交互性]小节的cprint.go.未按照书中源代码格式编写,而是把 import "C" 和 import "unsafe" 合并放入小括号中并和注释结束符 */ 间隔了一行,这也是大多数Go语言学习和开发者经常使用的代码风格,我修改后的代码如下: package main /*#include <stdio.h>#include <stdlib.h&g

嵌入式c语言中的坑

标题其实为了引起注意和刚刚的文章一致,其实是想说明如何写好优质的c语言 1.要注意优先级,不清楚的地方就加括号. 2.在if语句,while语句,不要过多的混合多个语句,简单明了,不要炫技巧. 3.浮点数判断要注意 4.switch中一定要加default语句,哪怕里面是空的. 5.注意for循环,切记误写成死循环 6.goto语句合理使用 7.sizeof关键字是不要有语句 8.指针的操作,统一使用p++,p--,避免+=1,-=1. 9.结构体的填充,其实涉及到内存对齐,主要是cpu读写效率

c语言基础笔试坑点

1.#define N 5 + 5 求 k = N*N*5 错误解法:10*10*5 = 500 正解(重点:注意黄色部分):5 + 5 * 5 + 5 * 5 = 55 2.死锁的必要条件: 1.相互排斥使用(资源独占) 一个资源每次仅仅能给一个进程使用 2.不可强占(不可剥夺)    资源申请者不能强行的从资源占有者手中夺取资源,资源仅仅能由占有者自愿释放 3.请求和保持(部分分配,占有申请) 一个进程在申请新的资源的同一时候保持对原有资源的占有(仅仅有这样才是动态申请,动态分配) 4.循环

C++的坑真的多吗

先说明一下,我不希望本文变成语言争论贴.希望下面的文章能让我们客观理性地了解C++这个语言.(另,我觉得技术争论不要停留在非黑即白的二元价值观上,这样争论无非就是比谁的嗓门大,比哪一方的观点强,毫无价值.我们应该多看看技术是怎么演进的,怎么取舍的.) 事由 周五的时候,我在我的微博上发了一个贴说了一下一个网友给我发来的C++程序的规范和内存管理写的不是很好(后来我删除了,因为当事人要求),我并非批判,只是想说明其实程序员是需要一些“疫苗”的,并以此想开一个“程序员疫苗的网站”,结果,@简悦云风同

听说你会 Python ?

前言                              可以加我交流群:565266089       里面大牛萌新都有,欢迎进来指导交流学习 最近觉得 Python 太"简单了",于是在师父川爷面前放肆了一把:"我觉得 Python 是世界上最简单的语言!".于是川爷嘴角闪过了一丝轻蔑的微笑(内心 OS:Naive!,作为一个 Python 开发者,我必须要给你一点人生经验,不然你不知道天高地厚!)于是川爷给我了一份满分 100 分的题,然后这篇文章就是记

听说你会Python?11篇Python技术热文

<听说你会 Python ?> Python 是世界上最简单的语言?Python 的动态特性可以让其用众多 black magic 去实现一些很舒服的功能,当然这也对我们对语言特性及坑的掌握也变得更严格了.本文总结了Python中的各种难掌握和易踩的"坑". <Python 函数式编程,从入门到?放弃?> 本文使用python介绍了什么是函数式编程,函数式编程的特点,函数式编程的用途,函数式编程相比于命令式编程和面向对象编程的优缺点等. <如何获得 Num

如何学好一门编程语言

首先要看自己目前的编程水平 如果是什么基础都没有(但是还是要有计算机基础的,如果还不会操作计算机.那你也不用学编程了.先会了斗地主再说) 这个时候,你根本不知道你学了语言能具体做什么.你可能知道的仅仅是学了这个语言以后.可以开发软件.可以开发游戏等. 寻找一门语言书籍,可以是java  c(谭浩强的树)   php来从头到尾看,死磕.不要错过一些细节.你看不懂看不明白的地方可以反复的看.还是不懂不明白.可以记录然后跳过,后续再过来看.所有例子都亲自完全实践. 这么语言学习到你能够单独做一个项目的

设计模式理解

常有人调侃设计模式:语言留下的坑,就要设计模式来填. 设计模式就是描述在各种不同情况下,要怎么解决问题的一种方案. 对于一些项目,我们不可能做到一次开发终身使用,而是随着需求的不断增多,我们需要对程序进行不断地修改,以此来适应新的需求,通过好的设计模式,可以使我们程序松耦合,减少彼此之间的依赖,从而使其扩展性更强,大大减少后期维护成本,降低后期开发难度. 设计模式分类设计模式按照其目标进行分类可以分为创建型, 结构型,行为型 创建型 单例模式(Singleton) 生成器模式(Builder)