Go 语言基础

一、Go 安装

1
2
3
4
5
6
$ wget https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz
$ tar -zxvf go1.13.6.linux-amd64.tar.gz
$ sudo mv go /usr/local/

$ go version
go version go1.13.6 linux/amd64
  • Go 1.11 版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题
  • 使用 Go Modules 管理依赖后会在项目根目录下生成两个文件 go.modgo.sumgo.mod 中会记录当前模块的模块名以及所有依赖包的版本
  • 在需要使用时才开启 GO111MODULE=on,避免在已有项目中意外引入 Go Modules
1
2
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct
  • 新建 Go Modules 项目:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ mkdir go-test
$ cd go-test
$ go mod init go-test

# 检测该文件夹目录下所有引入的依赖,写入 go.mod 文件
$ go mod tidy
# 将依赖下载至本地
$ go mod download
# 将下载至 GOPATH 下的依赖转移至该项目根目录下的 vendor 文件夹下
$ go mod vendor

$ go mod edit # 手动修改依赖文件
$ go mod graph # 打印依赖图
$ go mod verify # 校验依赖

二、Hello World

  • 新建 main.go
1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello World!")
}
  • 执行
1
2
3
4
$ go run main.go  # go run .

$ go build main.go
$ ./main
  • package main:声明了 main.go 所在的包。Go 语言中使用包来组织代码,一般一个文件夹即一个包,包内可以暴露类型或方法供其他包使用
  • func main:main 函数是整个程序的入口,main 函数所在的包名也必须为 main

三、变量与内置数据类型

1. 变量

  • Go 语言是静态类型的,变量声明时必须明确变量的类型
  • Go 语言的类型在变量后面
1
2
3
4
5
6
var a int  // 如果没有赋值,默认为0
var a int = 1 // 声明时赋值
var a = 1 // 声明时赋值,类型名可以省略不写

a := 1
msg := "Hello World!"

2. 简单类型

  • 空值:nil
  • 整型类型(取决于操作系统): intint8int16int32int64uint8uint16、…
  • 浮点数类型:float32float64
  • 字节类型:byte(等价于 uint8
  • 字符串类型:string
  • 布尔值类型:boolean
1
2
3
4
5
var a int8 = 10
var c1 byte = 'a'
var b float32 = 12.2
var msg = "Hello World"
ok := false

3. 字符串

  • Go 语言中,字符串使用 UTF8 编码。英文每个字符占 1byte,和 ASCII 编码是一样的;中文一般占 3byte。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"reflect"
)

func main() {
str1 := "Golang"
str2 := "Go语言"
fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8
fmt.Println(str1[2], string(str1[2])) // 108 l
fmt.Printf("%d %c\n", str2[2], str2[2]) // 232 è
fmt.Println("len(str2):", len(str2)) // len(str2): 8
}
  • reflect.TypeOf().Kind() 可以知道某个变量的类型
  • 字符串是以 byte 数组 形式保存的,类型是 uint8,打印时需要用 string 进行类型转换,否则打印的是编码值
  • 将 string 转为 rune 数组:
1
2
3
4
5
str2 := "Go语言"
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2])) // 35821 语
fmt.Println("len(runeArr):", len(runeArr)) // len(runeArr): 4
  • 转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文

4. 数组与切片

  • 声明数组:
1
2
3
4
var arr [5]int     // 一维
var arr2 [5][5]int // 二维
var arr3 = [5]int{1, 2, 3, 4, 5}
arr4 := [5]int{1, 2, 3, 4, 5}
  • 使用 [] 索引/修改数组:
1
2
3
4
5
arr := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
arr[i] += 100
}
fmt.Println(arr) // [101 102 103 104 105]
  • 数组的长度不能改变,如果想拼接 2 个数组,或是获取子数组,需要使用切片
  • 切片是数组的抽象:切片使用数组作为底层结构,包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
slice1 := make([]float32, 0)          // 长度为0的切片
slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice2), cap(slice2)) // 3 5

// 添加元素,切片容量可以根据需要自动扩展
slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice2), cap(slice2)) // 7 12
// 子切片 [start, end)
sub1 := slice2[3:] // [1 2 3 4]
sub2 := slice2[:3] // [0 0 0]
sub3 := slice2[1:4] // [0 0 1]
// 合并切片
combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0]
  • 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展
  • sub2... 是切片解构的写法,将切片解构为 N 个独立的元素

5. 字典

  • map 是一种存储键值对的数据结构
1
2
3
4
5
6
7
8
9
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
"Sam": "Male",
"Alice": "Female",
}
// 赋值/修改
m1["Tom"] = 18

6. 指针

  • 指针即某个值的地址,类型定义时使用符号 *。对一个已经存在的变量,使用 & 获取该变量的地址
1
2
3
4
str := "Golang"
var p *string = &str // p 是指向 str 的指针
*p = "Hello"
fmt.Println(str) // Hello 修改了 p,str 的值也发生了改变
  • 指针通常在函数传递参数,或者给某个类型定义新的方法时使用
  • Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func add(num int) {
num += 1
}

func realAdd(num *int) {
*num += 1
}

func main() {
num := 100
add(num)
fmt.Println(num) // 100,num 没有变化

realAdd(&num)
fmt.Println(num) // 101,指针传递,num 被修改
}

四、流程控制(if/for/switch)

1. if else

1
2
3
4
5
6
7
8
9
10
11
12
13
age := 18
if age < 18 {
fmt.Printf("Kid")
} else {
fmt.Printf("Adult")
}

// 可以简写为:
if age := 18; age < 18 {
fmt.Printf("Kid")
} else {
fmt.Printf("Adult")
}

2. switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Gender int8
const (
MALE Gender = 1
FEMALE Gender = 2
)

gender := MALE

switch gender {
case FEMALE:
fmt.Println("female")
case MALE:
fmt.Println("male")
default:
fmt.Println("unknown")
}
// male
  • 使用 type 关键字定义了一个新的类型 Gender
  • 使用 const 定义了 MALE 和 FEMALE 两个常量。Go 语言中没有枚举(enum)的概念,一般可以用常量的方式来模拟枚举
  • Go 语言的 switch 不需要 break。匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough
1
2
3
4
5
6
7
8
9
10
11
12
switch gender {
case FEMALE:
fmt.Println("female")
fallthrough
case MALE:
fmt.Println("male")
fallthrough
default:
fmt.Println("unknown")
}
// male
// unknown

3. for

1
2
3
4
5
6
7
sum := 0
for i := 0; i < 10; i++ {
if sum > 50 {
break
}
sum += i
}
  • 对数组(arr)、切片(slice)、字典(map)使用 for range 遍历:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nums := []int{10, 20, 30, 40}
for i, num := range nums {
fmt.Println(i, num)
}
// 0 10
// 1 20
// 2 30
// 3 40

m2 := map[string]string{
"Sam": "Male",
"Alice": "Female",
}
for key, value := range m2 {
fmt.Println(key, value)
}
// Sam Male
// Alice Female

五、函数

1. 参数与返回值

  • 一个典型的函数定义如下,使用关键字 func,参数可以有多个,返回值也支持有多个
  • 特别地,package main 中的 func main() 约定为可执行程序的入口
1
2
3
func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
// body
}
  • 例如,实现两个数的加法(一个返回值)和除法(多个返回值):
1
2
3
4
5
6
7
8
9
10
11
12
func add(num1 int, num2 int) int {
return num1 + num2
}

func div(num1 int, num2 int) (int, int) {
return num1 / num2, num1 % num2
}
func main() {
quo, rem := div(100, 17)
fmt.Println(quo, rem) // 5 15
fmt.Println(add(100, 17)) // 117
}
  • 可以给返回值命名,简化 return,例如 add 函数可以改写为:
1
2
3
4
func add(num1 int, num2 int) (ans int) {
ans = num1 + num2
return
}

2. 错误处理

  • 函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理
1
2
3
4
5
6
7
8
9
10
11
12
import (
"fmt"
"os"
)

func main() {
_, err := os.Open("filename.txt")
if err != nil {
fmt.Println(err)
}
}
// open filename.txt: no such file or directory
  • 可以通过 errors.New 返回自定义的错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"errors"
"fmt"
)

func hello(name string) error {
if len(name) == 0 {
return errors.New("error: name is null")
}
fmt.Println("Hello ", name)
return nil
}

func main() {
if err := hello(""); err != nil {
fmt.Println(err)
}
}
// error: name is null
  • error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func get(index int) int {
arr := [3]int{2, 3, 4}
return arr[index]
}

func main() {
fmt.Println(get(5))
fmt.Println("finished")
}

$ go run .
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
exit status 2
  • Go 语言提供了类似 try...catch 的机制:deferrecover
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func get(index int) (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Some error happened!", r)
ret = -1
}
}()
arr := [3]int{2, 3, 4}
return arr[index]
}

func main() {
fmt.Println(get(5))
fmt.Println("finished")
}

$ go run .
Some error happened! runtime error: index out of range [5] with length 3
-1
finished
  • defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,即先被 defer 的语句最后被执行
  • 使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer
  • 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0

六、结构体、方法和接口

1. 结构体和方法

  • 结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Student struct {
name string
age int
}

func (stu *Student) hello(person string) string {
return fmt.Sprintf("hello '%s', I am '%s'", person, stu.name)
}

func main() {
stu := &Student{
name: "Tom",
}
msg := stu.hello("Jack")
fmt.Println(msg) // hello 'Jack', I am 'Tom'

// 使用 new 实例化
stu2 := new(Student)
fmt.Println(stu2.hello("Alice")) // hello 'Alice', I am ''
}
  • 创建 struct 实例时,没有显性赋值的变量将被赋予默认值
  • 实现方法与实现函数的区别在于:方法需要加上该方法对应的实例名及其类型

2. 接口

  • 接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口
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
34
type Person interface {
getName() string
}

type Student struct {
name string
age int
}

func (stu *Student) getName() string {
return stu.name
}

type Worker struct {
name string
gender string
}

func (w *Worker) getName() string {
return w.name
}

func main() {
var p Person = &Student{
name: "Tom",
age: 18,
}

fmt.Println(p.getName()) // Tom

// 接口转为实例
stu := p.(*Student)
fmt.Println(stu.getName()) // Tom
}
  • Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可
  • 实例可以强制类型转换为接口,接口也可以强制类型转换为实例
  • 如果删除 (*Student).getName(),编译时会报错:*Student does not implement Person (missing getName method)
  • 如果删除 (*Worker).getName(),程序并不会报错,因为并没有在 main 函数中使用
  • 一般可以使用下面的方法进行检测,来确保某个类型实现了某个接口的所有方法
1
2
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil)
  • 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法

3. 空接口

  • 如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型
1
2
3
4
5
6
7
func main() {
m := make(map[string]interface{})
m["name"] = "Tom"
m["age"] = 18
m["scores"] = [3]int{98, 99, 85}
fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]]
}

七、并发编程

  • Go 语言提供了 syncchannel 两种方式支持协程(goroutine)的并发

1. sync

  • 例如希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束
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
import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second) // 模拟耗时操作
wg.Done()
}

func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go download("a.com/" + string(i+'0'))
}
wg.Wait()
fmt.Println("Done!")
}

$ time go run .
start to download a.com/2
start to download a.com/0
start to download a.com/1
Done!

real 0m1.563s
  • wg.Add(1):为 wg 添加一个计数。wg.Done(),减去一个计数
  • go download():启动新的协程并发执行 download 函数
  • wg.Wait():等待所有的协程执行结束

7.2 channel

  • 使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息
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
var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道

func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second)
ch <- url // 将 url 发送给信道
}

func main() {
for i := 0; i < 3; i++ {
go download("a.com/" + string(i+'0'))
}
for i := 0; i < 3; i++ {
msg := <-ch // 阻塞等待信道返回消息
fmt.Println("finish", msg)
}
fmt.Println("Done!")
}

$ time go run .
start to download a.com/2
start to download a.com/0
start to download a.com/1
finish a.com/2
finish a.com/1
finish a.com/0
Done!

real 0m1.528s

八、单元测试

  • 例如希望测试 package main 下 calc.go 中的函数,要只需要新建 calc_test.go 文件,在 calc_test.go 中新建测试用例即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// calc.go
package main

func add(num1 int, num2 int) int {
return num1 + num2
}

// calc_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
if ans := add(1, 2); ans != 3 {
t.Error("add(1, 2) should be equal to 3")
}
}

$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.040s
  • 运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加 -v 参数

九、包和模块

1. Package

  • 一般一个文件夹是一个 package,同一个 package 的内部变量、类型、方法等定义可以相互看到
  • Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见
  • 例如新建一个文件 calc.go,与 main.go 平级,分别定义 add 和 main 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// calc.go
package main

func add(num1 int, num2 int) int {
return num1 + num2
}

// main.go
package main

import "fmt"

func main() {
fmt.Println(add(3, 5)) // 8
}
  • 运行 go run main.go,会报错 ./main.go:6:14: undefined: add,因为仅编译 main.go 一个文件
  • 运行 go run main.go calc.gogo run .

2. Modules

  • Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制
  • Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务
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
34
35
36
37
38
39
40
41
42
43
44
45
46
$ go mod init example
go: creating new go.mod: module example # 此时在当前文件夹下生成了 go.mod 文件

$ vi main.go
package main

import (
"fmt"

"rsc.io/quote"
)

func main() {
fmt.Println(quote.Hello())
}

$ go mod tidy # 此时会自动触发第三方包 rsc.io/quote 的下载,具体的版本信息也记录在了 go.mod 中
$ go run .
Hello, world.

$ mkdir calc
$ vi calc/calc.go
package calc

func Add(num1 int, num2 int) int {
return num1 + num2
}

# 在 package main 中使用 package cal 中的 Add 函数
$ vi main.go
package main

import (
"fmt"
"example/calc"

"rsc.io/quote"
)

func main() {
fmt.Println(quote.Hello())
fmt.Println(calc.Add(10, 3))
}
$ go run .
Hello, world.
13

参考