实习笔记-Go基础语法
1.下划线
1.1 下划线在import中
当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import 引用该包。即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
比如:
1 | package main |
表示只引入当前文件夹中的hello包;
1.2 下划线在代码中
1 | package main |
这个下划线的意思是忽略这个变量,正常的写法是:
1 | f,err := os.Open("xxxxxxx") |
但是此时不需要知道返回的错误值,就可以用下划线代替,忽略error变量。
2.指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。
传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
Go语言中的指针操作非常简单,只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
比如:
1 | func main() {//指针取值 |
输出如下:
1 | type of b:*int |
总结:&和是一对互补操作符,&取出地址,根据地址取出地址指向的值。
3.结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了。
使用type和struct关键字来定义结构体,具体代码格式如下:
1 | type 类型名 struct { |
其中:
1 | 类型名:标识自定义结构体的名称,在同一个包内不能重复。 |
结构体实例化:
1 | var 结构体实例 结构体类型 |
如:
1 | type person struct { |
可以通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。
4.for循环
go中for循环3种形式
1 | for init; condition; post { } |
如下:
1 | package main |
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
1 | for key, value := range oldMap { |
如果只想读key:for key := range oldMap
;
如果只想读value:for _, value := range oldMap
5.函数
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
Go 语言函数定义格式如下:
1 | func function_name( [parameter list] ) [return_types] { |
函数定义解析:
- func:函数由 func 开始声明
- function_name:函数名称,参数列表和返回值类型构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
6.切片
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
声明一个未指定大小的数组来定义切片:var identifier []type
,切片不需要说明长度
或使用 make() 函数来创建切片:
1 | var slice1 []type = make([]type, len) |
也可以指定容量,其中 capacity 为可选参数。
1 | make([]T, length, capacity) |
这里 len 是数组的长度并且也是切片的初始长度。
7.类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
Go 语言类型转换基本格式如下:
1 | type_name(expression) |
type_name 为类型,expression 为表达式。
7.1 数值类型转换
将整型转换为浮点型:
1 | package main |
7.2 字符串类型转换
将一个字符串转换成另一个类型,可以使用以下语法:
1 | var str string = "10" |
以上代码将字符串变量 str 转换为整型变量 num。
注意⚠️:strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误。
若要将整数转换为字符串,可以用strconv.Itoa 函数
7.3 接口类型转换
接口类型转换有两种情况:类型断言和类型转换。
7.3.1 类型断言
类型断言用于将接口类型转换为指定类型,其语法为:
1 | value.(type) |
其中 value 是接口类型的变量,type 或 T 是要转换成的类型。
如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。
例子:
1 | package main |
- 首先,定义了一个接口类型变量 i,并将它赋值为字符串 “Hello, World”;
- 然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str;
- 最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
7.3.2 类型转换
类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
1 | T(value) |
T 是目标接口类型,value 是要转换的值。
在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
1 | package main |
- 首先,定义了一个 Writer 接口和一个实现了该接口的结构体 StringWriter;
- 然后,将 StringWriter 类型的指针赋值给 Writer 接口类型的变量 w;
- 接着,使用类型转换将 w 转换为 StringWriter 类型,并将转换后的值赋值给变量 sw;
- 最后,使用 sw 访问 StringWriter 结构体中的字段 str,并打印出它的值。
8.接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。
格式:
1 | /* 定义接口 */ |
实例:
1 | package main |
在上面的例子中,定义了一个接口 Phone,接口里面有一个方法 call();
然后我们在 main 函数里面定义了一个 Phone 类型变量,并分别为之赋值为 NokiaPhone 和 IPhone。然后调用 call() 方法,输出结果如下:
1 | I am Nokia, I can call you! |
接口类型变量可以存储任何实现了该接口的类型的值。
9.并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
1 | go 函数名( 参数列表 ) |
例如:
1 | go f(x, y, z) |
开启一个新的 goroutine
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
例子:
1 | package main |
执行以上代码,会看到输出的 hello 和 world 是没有固定先后顺序。
因为它们是两个 goroutine 在执行:
1 | world |
9.1 通道
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
1 | ch <- v // 把 v 发送到通道 ch |
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
1 | ch := make(chan int) |
注意⚠️:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
9.2 通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
1 | ch := make(chan int, 100) |
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
- 如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值;
- 如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;
- 如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
9.3 Go 遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
1 | v, ok := <-ch |
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
10.defer
10.1 什么是defer?
defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按defer的逆序进行执行。
简单来说:先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常用于释放资源。
1 | defer function([parameter_list]) // 延迟执行函数 |
10.2 多个defer的执行顺序?
多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出(LIFO)
1 | func func1(){ |
输出结果如下:
1 | main1 |

10.3 defer和return顺序

return
执行的时候,并不是原子性操作,一般是分为两步:- 将结果
x
赋值给了返回值; - 然后执行了
RET
指令;
- 将结果
defer
语句执行的时候,是在赋值变量之后,在RET
指令之前。所以这里注意一下返回值和x的关系。如果x
是一个值类型,这里是进行了拷贝的。
10.4 defer和panic
当函数遇到panic,defer仍然会被执行。Go会先执行所有的defer链表(该函数的所有defer),当所有defer被执行完毕且没有recover时,才会进行panic。
defer 最大的功能是 panic 后依然有效,所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。
可以在defer中进行recover,如果defer中包含recover,则程序将不会再进行panic,这就实现了Go中异常抛出/捕获类似的机制。
10.5 总结
- defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行。
- 多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出。
- defer后面的函数值在入栈的时候就决定了。
- defer 最大的功能是 panic 后依然有效,我们可以在defer中进行recover,如果defer中包含recover,则程序将不会再进行panic,实现try catch机制。
11.select
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。
Go内置了select关键字,可以同时响应多个通道的操作。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。
select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
1 | select { |
- select可以同时监听一个或多个channel,直到其中一个channel ready;
- 如果多个channel同时ready,则随机选择一个执行;
- 可以用于判断管道是否占满。
12.并发安全和锁
举个例子,开启两个goroutine,每个都执行x++5000次,但是因为没有加锁,会导致最后的结果并不是10000:
1 | var x int64 |
12.1 互斥锁
Go语言中使用sync包的Mutex类型来实现互斥锁。 加上互斥锁之后,就能解决上面代码的问题:
1 | var x int64 |
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;
当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
12.2 读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。
- 当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
- 当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
1 | var ( |
⚠️:读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
13.Sync
13.1 sync.WaitGroup
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 计数器 + delta |
(wg *WaitGroup) Done() | 计数器 - 1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。
例如:
- 当我们启动了N 个并发任务时,就将计数器值增加N。
- 每个任务完成时通过调用Done()方法将计数器减1。
- 通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
注意⚠️:sync.WaitGroup是一个结构体,传递的时候要传递指针。
13.2 sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名如下:
1 | func (o *Once) Do(f func()) {} |
注意⚠️:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
13.3 sync.Map
Go语言中内置的map不是并发安全的。
Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。
开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
1 | var m = sync.Map{} |