什么是Go?
Go(也叫Golang)是由Google开发的一款开源的编程语言。它是一款静态编译型语言。Go支持并发编程, 即它允许多个进程同时运行, 这是通过使用通道、协程等实现。Go有垃圾回收机制,它自己实现内存管理并且允许函数的延迟执行。
如何下载以及安装Go
到https://golang.org/dl/下载你操作系统对应的二进制文件。(由于网络原因, 国内下载请前往Go语言中文网, 梯子除外)
第一个Go程序
创建一个名为studyGo的文件夹, 你将会在这个文件夹内创建我们的go程序, Go文件以 .go 后缀名创建, 你可以使用下面的语法运行Go程序。
1 | $ go run main.go |
注: go run的go文件所在的package必须为package main, 并且必须包含main()函数, 否则无法编译通过
创建一个名为 first.go 的文件, 添加以下代码到文件里并保存。
1 | package main |
在终端导航到这个文件夹中, 使用下面的命令运行程序。
1 | $ go run first.go |
你可以看到输出打印:
1 | Hello World! This is my first Go program |
对于上面的程序:
package main - 每个go程序都应该以这个包名开始。Go允许我们使用其他go程序中的package, 所以支持代码重用。go程序的执行开始于main package。
import fmt - 导入fmt包, 这个package实现了I/O功能。
func main() - 这是go程序开始执行的入口, main函数应该总是出现在main包中, 在main()下面,你可以在{}中写代码。
fmt.Println() - 通过fmt包中的Println函数可以将文本打印到屏幕上, 并且自动换行。
数据类型
类型(数据类型)表示存储在变量中的值类型, 函数返回的值类型等等。
Go中有三种基本类型。
数值类型 - 表示包括整型、浮点型和复数。各种数值类型有:
关键字 | 类型 |
---|---|
int8 | 8位有符号整型 |
int16 | 16位有符号整型 |
int32 | 32位有符号整型 |
int64 | 64位有符号整型 |
uint8 | 8位无符号整型 |
uint16 | 16位无符号整型 |
uint32 | 32位无符号整型 |
uint64 | 64位无符号整型 |
float32 | 32位浮点型 |
float64 | 64位浮点型 |
complex64 | 实部和虚部都由float32组成 |
complex128 | 实部和虚部都由float64组成 |
字符串类型
表示由字符组成的序列。你可以在字符串上进行拼接、截取子串等一系列操作。
布尔类型
表示true或者false
变量
变量指存储值的一块内存地址, 在下面的语法中, 类型参数表示可以存储在内存中的值类型。
变量可以使用下面的语法声明:
1 | var <variableName> <type> |
一旦你声明了一个变量, 你可以赋给它任何该类型的值。在声明过程中,你也可以通过下面的语法给一个初始值。
1 | var <variableName> <type> = <initinal value> |
如果你声明的时候赋予了初始值, Go内部会自动根据值类型确定变量的类型, 所以在声明时你可以通过下面的语法忽略类型:
1 | var <variableName> = <value> |
同样, 你可以通过下面的语法声明多个变量:
1 | var <variableName1>, <variableName2> = <value1>, <value2> |
下面的例子有一些关于变量声明的例子:
1 | package main |
上面的输出为:
1 | x: 3 |
Go同样提供一个简单的方法声明变量并赋值, 该方法将忽略var
关键字:
1 | <变量名> := <值> |
注意: 使用:=
替代=
, 你不能使用:=
给一个已经声明过的变量赋值。
创建一个名为assign.go
的文件, 并写入下列代码:
1 | package main |
执行go run assign.go
, 观察运行结果:
1 | ./assign.go:7:4: no new variables on left side of := |
不带初始值的变量声明将会给变量赋值该类型的默认值, 数值类型默认值为0, 布尔型为false, 字符串类型为空字符串。
常量
常量是一旦被赋值了便不可更改的变量, Go中的常量通过关键字const
声明
创建constant.go
文件, 写入下面的代码:
1 | package main |
执行go run constant.go
查看结果:
1 | .constant.go:7:4: cannot assign to b |
循环
循环用来执行基于某个条件的重复语句块。大多数编程语言提供三种循环-for
, while
, do while
。但是Go只支持for循环。
for
循环的语法是:
1 | for 初始表达式; 终止条件表达式; 迭代表达式 { |
初始表达式第一个被执行, 并且只执行一次
终止条件表达式在每次执行循环代码块前都会先进行一次判断, 如果结果为true
, 则执行循环内的代码块。
迭代表达式从第一次循环结束开始, 以后每次循环结束都会执行该迭代语句, 改变迭代变量的值, 直到终止条件表达式的值为false。
复制下面的代码到go文件, 并执行。 程序将会循环打印输出1到5。
1 | package main |
输出为:
1 | 1 |
Go语言中经常会用到无限循环用于持续接收数据之类的场景:
1 | for { |
if else
条件语句if else的语法为:
1 | if condition{ |
if后面可以不用跟着else, 也可以使用链式的if else语句。下面的程序将会解释更多关于if else的使用方法。
下面的程序将会检查整型变量x, 如果小于10, 程序将会打印x is less than 10
1 | package main |
这里如果x的值大于10, if中的语句将不会被执行。
下面的程序添加了else, 如果if条件不满足, 则会执行else中的语句。
1 | package main |
上面的程序将会输出:
1 | x is greater than or equals 10 |
下面是带有多个if else(链式if else)的程序, 执行下面的例子, 程序将会检查x
是否小于10或者在10-90之间或者大于90。
1 | package main |
程序将会一次检查每个条件, 直到找到满足的条件, 然后执行该分支下面的代码块。上面的程序将会输出:
1 | x is greater than 90 |
switch
switch
是Go中的另一个条件语句, 与其他编程语言中一样, switch
语句将会计算表达式的值, 然后与每个case
分支中的值进行比较, 如果比较结果为true
, 则会执行对应的代码块。如果没有匹配的case
分支, 则什么也不执行, 这种情况下, 可以在switch
语句中添加default
分支, 当没有满足条件的case
分支时, 程序会执行default
分支中的代码块。关于switch
语句的语法为:
1 | switch 表达式 { |
注: 表达式必须是可计算的
还有另外一种写法是, 省略switch
后面的表达式, 在每个case
语句后面添加表达式:
1 | switch { |
这种情况下, switch
语句将会判断每个分支, 直到找到找到case
中的条件表达式为true时, 然后执行对应的代码块, 如果没有结果为true
的case
分支, 则会执行default
分支。
注: 每个case
中的表达式必须是可计算的, 并且计算结果为true或者false
执行下面的代码:
1 | package main |
程序将会输出:
1 | Sum is 3 |
将a和b的值改为3, 程序将会输出:
1 | Printing default |
第二种写法:
1 | package main |
程序输出:
1 | a is 2 |
如果多个case
的处理相同, 则可以将case
条件语句放在同一个case
中, 用,
分隔。 如果不放在一起, 则可以将两个case
挨着放, 然后在上面的case
分支中通过关键字fallthrough
让程序执行下面挨着的case
代码块。
1 | package main |
程序输出:
1 | b == 1 |
数组
数组表示长度固定, 元素数据类型相同的一组数据序列。在数组中, 不能同时包含整型元素和字符元素, 数组一旦定义好了则不能改变其长度以及对应的元素类型。
声明数组的语法如下:
1 | var arrayName [size]type |
对于声明好的数组, 每个元素可以通过下面的语法赋值:
1 | var arr [10]int |
数组下表从0
开始到size - 1
在数组声明的同时可以进行赋值:
1 | arrayName := [size]type{v_0, v_1, ..., v_size-1} |
数组声明的时候也可以省略size而使用赋值的方式替代, Go会自动识别数组的size, 语法如下:
1 | arrayName := [...]int{1, 2, 3, 4} //size为4 |
注: Go中对...
的应用主要有三种情形:
1. 数组声明中, 用于表示该数组的长度由花括号中元素个数决定, 而不是通过显示的方式指定
2. 用在函数或者方法的形参中, 用于说明该参数为指定类型的不定个数参数(参数值个数>=0), func test(params …int), 表示params包含int型的一个或者多个参数值
3. 用在append(array1, array2…)中, 表示将array2中的所有元素append到array1中
通过调用Go内置函数len()
可以获取数组的size:
1 | arraySize := len(arrayName) |
通过下面的代码更好的理解数组:
1 | package main |
上面程序的输出:
1 | Two |
切片
切片是数组的一部分, 切片底层指向一个数组, 像数组一样, 切片元素的类型也是固定的, 切片元素也可以通过切片名和下标来访问, 与数组不同的是, 切片长度可以改变。
切片实际上是一个指向底层数组元素的指针, 也就是说如果你改变了切片元素的值, 底层数组的值也会被改变。
注: 实际使用中, 如果多个切片共用一个底层数组, 需要注意值的改变对切片之间带来的影响。
切片创建的语法如下:
1 | var sliceName []type = arrayName[start:end] |
上面的语法将会使用数组arrayName
下标start
到end-1
的元素创建一个名为sliceName
, 元素类型为type
的切片(实际中可以省略type
)
执行下面的代码, 程序将会从数组创建一个切片并打印, 同时更改了切片元素的值, 数组的值也会跟着改变:
1 | package main |
输出:
1 | Array after creation: [one two three four five] |
Go中有一些可以应用在切片上的内置函数:
len(sliceName) - 返回切片长度
append(sliceName, v1, v2) - 往切片追加元素v1, v2
append(slice1, slice2…) - 将slice2中的元素追加到slice1中
执行下面的代码:
1 | package main |
输出:
1 | sliceA: [2 3] |
函数
函数表示执行特定任务的一个代码块。函数声明指定函数名, 返回值类型以及传入的参数(形参)。函数定义指定了函数要执行的代码块, 函数声明的语法如下:
1 | func funcName(param1 type, param2 type) returnType { |
形参和返回类型是可选的, 根据实际情况做选择。Go语言支持函数有多个返回值。下面的代码中函数接收两个参数并且计算加法和减法, 并且返回两个值。
1 | package main |
输出为:
1 | Sum 25 |
package
Go语言中的package是用来组织代码结构的, 在一个大型项目中, 将所有代码写在一个文件里是不可行的, Go语言允许我们在不同的package下组织我们的代码。package的应用增加了代码的可读性以及复用性。Go的可执行程序应该包含main package和程序的执行入口main函数, 导入package的语法如下:
1 | import packageName |
下面的例子将讨论如何创建和使用package
创建一个名为
package_example.go
的文件并且添加如下代码:1
2
3
4
5
6
7
8
9
10package main
import "fmt"
import "calculation" //待创建的包名
func main() {
x,y := 15,10
//calculation包中将包含函数Do_add()
sum := calculation.DoAdd(x,y)
fmt.Println("Sum",sum)
}上面的程序中
fmt
包是Go提供给我们的主要用于I/O格式化输出功能的, 还包含了calculation
包, 该包中包含Do_add()函数, 并且该函数在main
包被调用:sum := calculation.DoAdd(x,y)
在$GOPATH目录下的src目录中创建一个名为
calculation
的文件夹, 在该文件夹下创建名为calculation.go
的文件, 并写入代码:1
2
3
4
5
6package calculation
func DoAdd(num1 int, num2 int) int {
sum := num1 + num2
return sum
}回到
package_example.go
目录中, 执行go run package_example.go
, 程序将会输出:Sum 25
对于使用go module的项目, 则需要在go.mod
文件中require
该package, 值得注意的是, 对于自己的本地项目, 不仅需要在go.mod
文件中require
, 而且需要将该packagereplace
为本地路径。
注意: DoAdd()函数的首字母必须大写, 因为Go语言中的对于想让其他package可以访问的函数、变量、结构体字段等, 其命名必须以大写字母开头, 上面的代码中如果DoAdd()开头为小写: doAdd(), 则其他package无法访问, 上面的程序将会编译出错:
1 | cannot refer to unexported name calculation.calc.. |
defer
defer
语句是用来延迟函数执行的, 在defer
中的代码块将会被延迟到包含该defer
语句的函数返回前执行, 即: defer
将会在包含它的函数返回前最后执行。
1 | package main |
输出为:
1 | Inside the main() |
这里sample()
的执行被延迟到main()函数体执行完且在return返回之前执行
当存在多个defer
语句时, Go将所有的defer
放进调用栈中, 当主函数调用完成后, defer
调用栈按照先进后出(LIFO)的顺序执行各个defer
, 如下:
1 | package main |
输出为:
1 | 4 |
指针
在介绍指针前, 先看一下&
操作符, &
操作符被用来获取变量的地址, 即&a
将会获取到存储变量a
的内存地址。
下面的程序将会展示一个变量的值和它的地址:
1 | package main |
输出为:
1 | Address: 0xc000078008 |
指针变量存储另一个变量的内存地址, 定义指针的语法如下:
1 | var pName *type |
*
操作符表示该变量是一个指针, 看下面代码:
1 | package main |
输出为:
1 | Address of a: 0x416020 |
结构体
结构体是开发者自己定义的数据类型, 结构体可以包含一个或者多个相同或者不同类型的字段。当你想将多个数据存储在一起的时候可以使用结构体。考虑员工信息, 一般包含姓名、年龄和地址, 你可以通过两种方式存储:
- 通过三个数组, 一个数组存储姓名, 一个数组存储年龄, 另一个存储地址, 每个数组中下标相同的元素表示同一个员工的信息
- 声明一个包含姓名、年龄、地址三个字段的结构体, 通过结构体的数组来表示员工信息, 数组的每个元素表示一个员工信息
第一种方法并不高效, 在这种场景冲, 结构体更加高效。
声明结构体的语法如下:
1 | type structName struct { |
上面的员工信息结构体可以声明为:
1 | type emp struct { |
现在可以通过结构体创建一个存储员工信息的变量:
1 | var empName emp |
对结构体中每个字段的赋值语法如下:
1 | empName.name = "John" |
也可以在声明变量的时候同时赋初值:
1 | empData := emp{"Raj", "Building-1, Delhi", 25} |
在上面的声明过程中, 必须保证字段的顺序与结构体声明的顺序一致。
执行下面的代码:
1 | package main |
输出:
1 | John |
方法(不是函数)
方法是带有接收者的函数, 语法为:
1 | func (variable variableType)methodName(parammeter1 parammeter1Type) (returnValue1 returnValue1Type){ |
上面的方法等价于
1 | func methodName(variable variableType, parammeter1 parammeter1Type) (returnValue1 returnValue1Type){ |
将上面的员工相关的函数变为方法
1 | package main |
Go语言不是面向对象的编程语言, 没有class的概念, 方法调用给人一种在面向对象编程时调用class的方法的感觉。
并发
Go支持任务的并发执行, 意味着Go可以同时执行多个任务。这与并行的概念不同, 在并行中, 一个任务被分成多个更小的子任务并且并行的被执行, 但是在并发中, 多个任务是同时被执行的, 在Go中并发通过通道和协程实现。
协程
协程是一个可以与其他函数同时执行的函数。通常当一个函数被调用的时候, 控制权将转移到该函数中, 一旦被调用函数执行完毕, 控制权将返还给调用函数, 调用函数继续执行, 在调用函数继续执行剩下的代码前它将等待被调用函数执行完毕。
但是在协程使用中, 调用函数将不用等待被调用函数执行完毕, 它将会继续执行后面的代码, 在一个程序中可以有多个协程。
在Go程序中, main协程(主协程)执行完它包含的所有的代码后将会退出, 并不会等待其他协程执行完毕再退出。
开启一个协程是使用关键字go
后面紧跟着函数调用:
1 | go add(x, y) |
通过下面的代卖你将更好的理解协程:
1 | package main |
输出:
1 | In main |
这里main协程甚至在display协程开始执行前就已经执行完毕了, display协程是通过下面的语法调用的:
1 | go funcName(parammeter list) |
在上面的代码中, 因为在display协程执行前, main函数就已经执行完毕了, 所以打印内容中并没有display中打印的内容。
现在对上面的代码进行修改, 在main的循环中每次循环增加2秒的延时, 在display的循环中每次循环增加1秒的延时:
1 | package main |
输出为:
1 | In display |
通道
通道是函数彼此通信的一种方式。它可以看作是一个中间区域, 一个协程往这个中间区域放数据, 另一个协程从中获取数据。
注: 通道只能传输同一种类型的数据
通道的声明语法为:
1 | channelName := make(chan dataType) |
例如:
1 | ch := make(chan int) |
向通道发送数据的语法为:
1 | chanVariable <- data |
例如:
1 | ch <- x |
从通道接收数据的语法为:
1 | data := <- chanVariable |
例如:
1 | data := <- ch |
在上面的例子中, main函数并不会等待协程的执行, 但是这是在没有使用通道的情形下, 如果一个协程发送数据到通道里, main函数将会在通道接收操作那里等待, 直到接收到数据。
在下面的例子中, 观察一下使用和不实用通道的区别:
1 | package main |
输出为:
1 | Inside main() |
main函数在协程执行前已经退出, 所以并没有打印display()中的输出内容。
现在更改上面的代码使之加入通道:
1 | package main |
输出为:
1 | Inside display() |
这里main将会阻塞在x := <-ch
, 直到在display中往通道发送了数据。
通过关闭通道, 往通道发送数据的发送着可以告知接收者不会再有数据被发送了, 这主要用在当你在一个循环中发送数据到通道中的时候。一个通道可以通过调用close()
内置函数关闭通道:
1 | close(chName) |
接收者在接收数据的时候可以通过可选的变量来判断通道是否关闭了:
1 | variableName, status := <- ch |
如果状态为true, 则表示通道未关闭且数据有效, 如果状态未false, 则表示通道已关闭。
通道同样可以用在协程之间的通信, 有发送数据的通道, 也有接收数据的通道。
1 | package main |
输出为:
1 | Read data |
注: 关于通道的更多使用, 请参考Go语言中的通道和Go语言如何优雅的关闭通道
select
select
可以看作是用于通道的switch
语句。这里的case
语句必须是通道操作, 通常情况下, 每个case
将会尝试从通道中读取数据, 当任何一个case
语句对应的通道准备好之后, 该case
语句将会被执行, 如果存在多个case
语句, select
将会随机选择一个执行。与普通switch
一样, 当没有case
语句可以执行的时候, default
分支将会被默认执行。
1 | package main |
程序输出:
1 | from data2() |
为上面程序中的select
添加default
分支, 因为data1()
和data2()
都有至少2秒的延时, 对于select
来说, 因为case
中的通道都没有数据(未准备好), 所以default
分支将会被执行:
1 | package main |
程序输出:
1 | Default case executed |
mutex
mutex包含在sync
包中, 根据包名可以看出, mutex
是Go中用于控制互斥的锁, 即互斥锁。当你不想让一个资源同时被多个子协程访问时, 便可以通过互斥锁来实现。互斥锁有两个方法: Lock
和Unlock
, 在Lock
和Unlock
中的代码块将会被唯一的执行, 即在同一时刻只有有一个任务执行该段代码块。
下面的例子将会对循环的执行次数进行计数, 在例子中我们期望循环10次, 开启了三个协程, 总的执行次数应该是30, 总的执行次数被存放在一个全局变量中。
没有互斥锁:
1 | package main |
输出:
1 | Count after i=1 Count: 11 |
每次执行的结果可能不同, 但都不是预期的值
在上面的程序中, 对于count
值的变更有三个步骤:
- 将值拷贝给
temp
变量 - 对
temp
进行加1操作 - 将
temp
的值存回count
因为存在上面的三个步骤, 并且同时是三个协程在访问并更改count
变量, 所以存在互斥: 可能值被协程1改变了, 但此时协程2持有的仍然是协程1改变之前的旧值, 此时协程2将会对协程1更改的值进行覆盖。
下面加入互斥锁的实现:
1 | package main |
程序输出:
1 | Count after i=3 Count: 21 |
这里的输出符合我们的预期, 因为在全局变量的访问与写入中我们加入了互斥锁, 防止在同一时刻该段代码被多次执行
错误处理
错误表示出现了不符合预期的异常情况, 比如: 关闭一个未打开的文件, 打开一个不存在的文件等等。函数通常将错误当作最后一个返回参数返回。
1 | package main |
输出:
1 | open /invalid.txt: no such file or directory |
自定义错误
对于Go程序开发者而言, 可以通过调用errors包中的New()
函数来自定义错误内容。
1 | package main |
输出:
1 | custom error message:File name is wrong |
文件读取
文件用来存放数据, Go支持从文件中读取数据。
在当前目录下用下面的内容创建一个名为data.txt
的文件:
1 | Line one |
运行下面的程序, 将文件的内容作为输出打印:
1 | package main |
data, err := ioutil.ReadFile("data.txt")
读取文件中的数据并且返回一个字节序列, 在输出打印时将字节序列转换为string输出。
输出:
1 | Contents of file: Line one |
文件写入
查看下面的代码:
1 | package main |
上面将会在当前目录下创建一个名为file1.txt
的文件, 并且向文件中写入: Write Line one
, 如果file1.txt
已经存在, 则该文件内容将会被覆盖。
注: 如果文件打开成功, 在操作完毕后需要调用Close()
方法来关闭该文件。