一、Go 安装
- 下载地址:https://golang.org/dl/
- Linux 安装:
1 | $ wget https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz |
- 从
Go 1.11
版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题 - 使用 Go Modules 管理依赖后会在项目根目录下生成两个文件
go.mod
和go.sum
。go.mod
中会记录当前模块的模块名以及所有依赖包的版本 - 在需要使用时才开启
GO111MODULE=on
,避免在已有项目中意外引入Go Modules
1 | $ go env -w GO111MODULE=on |
- 新建 Go Modules 项目:
1 | $ mkdir go-test |
二、Hello World
- 新建
main.go
1 | package main |
- 执行
1 | $ go run main.go # go run . |
package main
:声明了 main.go 所在的包。Go 语言中使用包来组织代码,一般一个文件夹即一个包,包内可以暴露类型或方法供其他包使用func main
:main 函数是整个程序的入口,main 函数所在的包名也必须为main
三、变量与内置数据类型
1. 变量
- Go 语言是静态类型的,变量声明时必须明确变量的类型
- Go 语言的类型在变量后面
1 | var a int // 如果没有赋值,默认为0 |
2. 简单类型
- 空值:
nil
- 整型类型(取决于操作系统):
int
、int8
、int16
、int32
、int64
、uint8
、uint16
、… - 浮点数类型:
float32
、float64
- 字节类型:
byte
(等价于uint8
) - 字符串类型:
string
- 布尔值类型:
boolean
1 | var a int8 = 10 |
3. 字符串
- Go 语言中,字符串使用
UTF8
编码。英文每个字符占 1byte,和 ASCII 编码是一样的;中文一般占 3byte。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别
1 | package main |
reflect.TypeOf().Kind()
可以知道某个变量的类型- 字符串是以
byte 数组
形式保存的,类型是 uint8,打印时需要用 string 进行类型转换,否则打印的是编码值 - 将 string 转为 rune 数组:
1 | str2 := "Go语言" |
- 转换成
[]rune
类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文
4. 数组与切片
- 声明数组:
1 | var arr [5]int // 一维 |
- 使用
[]
索引/修改数组:
1 | arr := [5]int{1, 2, 3, 4, 5} |
- 数组的长度不能改变,如果想拼接 2 个数组,或是获取子数组,需要使用切片
- 切片是数组的抽象:切片使用数组作为底层结构,包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
1 | slice1 := make([]float32, 0) // 长度为0的切片 |
- 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展
sub2...
是切片解构的写法,将切片解构为 N 个独立的元素
5. 字典
- map 是一种存储键值对的数据结构
1 | // 仅声明 |
6. 指针
- 指针即某个值的地址,类型定义时使用符号
*
。对一个已经存在的变量,使用&
获取该变量的地址
1 | str := "Golang" |
- 指针通常在函数传递参数,或者给某个类型定义新的方法时使用
- Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量
1 | func add(num int) { |
四、流程控制(if/for/switch)
1. if else
1 | age := 18 |
2. switch
1 | type Gender int8 |
- 使用
type
关键字定义了一个新的类型 Gender - 使用
const
定义了 MALE 和 FEMALE 两个常量。Go 语言中没有枚举(enum)的概念,一般可以用常量的方式来模拟枚举 - Go 语言的 switch 不需要 break。匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用
fallthrough
1 | switch gender { |
3. for
1 | sum := 0 |
- 对数组(arr)、切片(slice)、字典(map)使用
for range
遍历:
1 | nums := []int{10, 20, 30, 40} |
五、函数
1. 参数与返回值
- 一个典型的函数定义如下,使用关键字
func
,参数可以有多个,返回值也支持有多个 - 特别地,
package main
中的func main()
约定为可执行程序的入口
1 | func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) { |
- 例如,实现两个数的加法(一个返回值)和除法(多个返回值):
1 | func add(num1 int, num2 int) int { |
- 可以给返回值命名,简化 return,例如 add 函数可以改写为:
1 | func add(num1 int, num2 int) (ans int) { |
2. 错误处理
- 函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理
1 | import ( |
- 可以通过
errors.New
返回自定义的错误:
1 | import ( |
- error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为
panic
1 | func get(index int) int { |
- Go 语言提供了类似
try...catch
的机制:defer
和recover
1 | func get(index int) (ret int) { |
- defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,即先被 defer 的语句最后被执行
- 使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer
- 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0
六、结构体、方法和接口
1. 结构体和方法
- 结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等
1 | type Student struct { |
- 创建 struct 实例时,没有显性赋值的变量将被赋予默认值
- 实现方法与实现函数的区别在于:方法需要加上该方法对应的实例名及其类型
2. 接口
- 接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口
1 | type Person interface { |
- Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可
- 实例可以强制类型转换为接口,接口也可以强制类型转换为实例
- 如果删除
(*Student).getName()
,编译时会报错:*Student does not implement Person (missing getName method)
- 如果删除
(*Worker).getName()
,程序并不会报错,因为并没有在 main 函数中使用 - 一般可以使用下面的方法进行检测,来确保某个类型实现了某个接口的所有方法
1 | var _ Person = (*Student)(nil) |
- 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法
3. 空接口
- 如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型
1 | func main() { |
七、并发编程
- Go 语言提供了
sync
和channel
两种方式支持协程(goroutine)的并发
1. sync
- 例如希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用
sync.WaitGroup
,等待所有并发协程执行结束
1 | import ( |
wg.Add(1)
:为 wg 添加一个计数。wg.Done(),减去一个计数go download()
:启动新的协程并发执行 download 函数wg.Wait()
:等待所有的协程执行结束
7.2 channel
- 使用
channel
信道,可以在协程之间传递消息。阻塞等待并发协程返回消息
1 | var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道 |
八、单元测试
- 例如希望测试 package main 下
calc.go
中的函数,要只需要新建calc_test.go
文件,在calc_test.go
中新建测试用例即可
1 | // calc.go |
- 运行
go test
,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v
参数
九、包和模块
1. Package
- 一般一个文件夹是一个 package,同一个 package 的内部变量、类型、方法等定义可以相互看到
- Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见
- 例如新建一个文件
calc.go
,与main.go
平级,分别定义 add 和 main 方法
1 | // calc.go |
- 运行
go run main.go
,会报错./main.go:6:14: undefined: add
,因为仅编译 main.go 一个文件 - 运行
go run main.go calc.go
或go run .
2. Modules
- Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制
- Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务
1 | $ go mod init example |