学习笔记|Go

本文最后更新于:1 年前

1.准备工作

1.1 GOROOT 和 GOPATH

  • GOROOT:Go SDK的位置。
  • GOPATH:在 goland 中分为全局 GOPATH、项目 GOPATH、模块 GOPATH。

使用原生的 go mod 的方式,不用管这些乱七八糟的 GOPATH,将它当作一个普通且普通的放代码的目录即可。

一个 go 项目的目录是什么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go    # GOPATH
├── bin # Go 构建的编译可执行程序的位置
└── pkg # 编译后包文件会生成.a文件,放置在 $GOPATH/pkg/$GOOS_$GOARCH中。
│ └── mod # 程序寻找第三方库,就在该目录下寻找
│ ├── go.uber.org # 依赖的第三方库的版本信息
│ └── cache # 依赖的第三方库代码实际缓存的位置
├── project1 # 项目一
│ ├── go.mod # 用于依赖管理的 mod 文件:记录了所依赖的第三方库名称及其版本。
│ ├── go.sum # 对于 mod 文件更详细的描述:包含 现在 和 曾经 依赖的第三方库名称、版本、hash 值。
│ └── zaptest # 包
│ │ ├── func.go # 封装的功能一
│ │ └── zaptest.go # 封装的功能二
│ └── entry # 包
│ └── entry.go # 启动程序,调用第三方库和封装的功能

└── project2 # 项目二

1.2 hello world

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello world!")
}

1.3 goimports

在保存代码后,goimports 可自动导入需要的包,格式化代码(甚至将空格转化为 tab

1.4 goland 快捷键

代码编辑

  • command + alt + V,自动生成变量名。
  • command + B,转到声明。
  • command + N,快速生成代码。
  • command + X,剪切、删除当前光标所在行。
  • command + D,复制当前光标所在行。
  • command + shift + Up/Down, 代码向上/下移动。
  • alt + Delete,按单词进行删除。
  • command + Delete,按整行删除
  • shift + enter,向下插入新行,即使光标在当前行的中间。
  • command + shift + U,将选中内容进行大小写转化。

代码格式化

  • command + alt + T,把代码包在一个块内,例如if{…}else{…}。
  • command + alt + L,格式化代码。
  • command + /,单行注释。
  • command + shift + /,进行多行注释。
  • command + “+/-”,可以将当前方法进行展开或折叠。

查找和定位

  • command + F,查找文本。
  • command + R,替换文本。
  • command + shift + F,进行全局查找。

文件相关快捷键

  • command + E,打开最近浏览过的文件。
  • command + shift + E,打开最近更改的文件。
  • command + shift + N,快速生成文件。

1.5 编译命令

1
2
3
go run helloworld.go  # 编译运行单个文件。
go build # 编译整个项目,但不保存可执行文件。
go install # 编译后,将可执行文件保存在 $GOPATH/bin/ 目录下。

2.基础语法

2.1 变量定义

  • var 声明变量类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a int    // 定义一个变量
var b, c int // 定义两个变量
var s string

// 赋初值
var aa int = 1
var bb, cc int = 2, 3
var ss string = "abc"

// 用括号集中定义
var (
a int
s string
)
  • 当赋初值时,不声明变量类型

当不声明类型时,可以不同类型可以一起定义。

1
var b, c, s = 2, 3, "abc"
  • 不使用 var 关键字【推荐

    := 表示定义
    = 表示赋值。
    注:只能在函数内使用这种定义方式。

1
2
3
4
5
6
7
a := 1
b, s := 2, "abc"

// 【注意】
a, b := 1, 2 // 定义 a, b
b, c := 3, 4 // 虽然 b 定义过,因为 c 没定义过,所以 b, c 可以使用 := 定义。
fmt.Println(a, b, c)
  • 包内定义和函数内定义

    • go 没有真正的全局变量,包内定义只能在包内使用。
    • 包内定义推荐使用 var () 集中定义。

2.2 内建变量类型

  • bool、string

  • 无符号整型:int(长度跟随操作系统位数)、int8、int16、int32、int64

  • 指针(长度跟随操作系统位数):unitptr

  • 字符类型(8位,1字节,同 int8):byte

  • 字符类型(32位,4字节,同 int32):rune

    utf-8 中,有的字符需要三个字节表示。

    使用 char 有很多问题,所以 go 没有 char 类型,使用 4 个字节来作为字符类型。

  • 浮点型:float32、float64

  • 复数:complex64、complex128

2.3 类型转化

只有强制类型转换。

1
2
3
4
5
6
7
8
// 参数、返回值均为 float64:func Sqrt(x float64) float64 {}

func triangle() {
a, b := 3, 4
var c int
c = int(math.Sqrt(float64(a*a + b*b))) // 所以此处必须强制转换
fmt.Println(c)
}

2.4 常量定义

不声明类型时,常量的数值可以作为各种类型使用,就相当于文本替换(C++中的宏定义)。

1
2
3
4
5
6
7
8
9
const filename string = "abc.txt"     // 申明类型

const a, b = 3, 4 // 不声明类型,a、b 既可以做 int 也可以做 float

// 集中定义
const (
a, b = 3, 4
filename = "abc.txt"
)

特殊的常量:枚举类型

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
func enums() {
const ( // 定义枚举类型
cpp = 0
java = 1
python = 3
golang = 4
)
fmt.Println(cpp, java, python, golang) // 0 1 3 4
}


---------------------
func enums() {
const (
cpp = iota // 创建自增的枚举,这样后面变量可不赋值,自动 +1
java
python
golang
)
fmt.Println(cpp, java, python, golang) // 0 1 3 4
}


---------------------
func enums() {
const (
b = 1 << (10 * iota) // 利用自增作为种子,构建表达式
kb
mb
gb
tb
)
fmt.Println(b, kb, mb, gb, tb) // 1 1024 1048576 1073741824 1099511627776
}

2.5 条件语句

  • if
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
const fileame = "./abc.txt"
contents, err := ioutil.ReadFile(filename) // 读取文件,返回两个参数,内容和错误信息。
if err != nil { // 判断是否有错误信息
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
}


---------------------
// 定义变量和 if 判断写到一起, 用分号做分割
// 注意:此时 contents 只能在 if 中使用。
func main() {
const filename = "./abc.txt"
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
}
  • switch
    • 默认每个 case 后自动有 break,否则需要使用 fallthrough。
    • panic 函数会终止程序,打印错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func grade(score int) string {
yourGrade := ""
switch {
case score >= 0 && score < 60:
yourGrade = "fail"
case score < 90:
yourGrade = "pass"
case score <= 100:
yourGrade = "super"
default:
panic(fmt.Sprintf("Wrong score: %d", score)) // 同 c++ 的 sprintf,将字符串格式化,当作 panic 的参数。
}
return yourGrade
}

2.6 循环

  • 条件没有括号。
  • 初始条件、结束条件、递增表达式都可以省掉。
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
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println(sum)
}

---------------------
// 省略初始条件
i := 1
for ; i <= 100; i++ {
sum += i
}


---------------------
// 省略初始条件、递增表达式,同其他语言的 while(go 没有 while)
i := 1
for i <= 100 {
sum += i
// ...
}


---------------------
// 省略初始条件、结束条件、递增表达式(死循环)
for {
sum += i
// ...
}

【注意】遍历数组时使用指针,需要保存临时变量。

go 中 for i,v :=range slice 中的 i、v 会保存本次循环的索引和值,直接使用 v 的地址会导致内容变更。

因此循环体内需要临时变量保存当前值。

1
2
3
4
5
6
7
8
9
10
for _, x := range nums {
x := x // 保存临时变量
if a == nil || x > *a {
a, b, c = &x, a, b
} else if *a > x && (b == nil || x > *b) {
b, c = &x, b
} else if b != nil && *b > x && (c == nil || x > *c) {
c = &x
}
}

2.7 函数

  • 函数定义

    函数名写在前面,返回类型在后面

1
2
3
4
5
6
7
8
func function(a, b int, s string) int {
if s == "+" {
return a + b
} else if s == "-" {
return a - b
}
return 0
}
  • 返回值可以返回两个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func function(a, b int) (int, int) {
return a + b, a - b
}

func main() {
max, min := function(1, 2) // 接收两个返回值。
tmp, _ := function(1, 2) // 只接收一个返回值,_ 用作占位符。
fmt.Println(max, min, tmp)
}


---------------------
// 函数体比较长,不推荐的写法
func function(a, b int) (max, min int) { // 返回值中声明变量名
max = a + b
min = a - b
return // 返回时可不写变量名。
}
  • 通常情况,两个返回值,一个返回结果一个返回错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func function(a, b int, s string) (int, error) {
if s == "+" {
return a + b, nil
} else if s == "-" {
return a - b, nil
}
return 0, fmt.Errorf("unsuported operation: %s", s) // 返回错误
}

func main() {
if res, err := function(1, 2, "*"); err != nil { // 判断是否有错
fmt.Println(err)
} else {
fmt.Println(res)
}
}
  • 函数作为参数

    同 C++ 中的回调函数。

1
2
3
4
5
6
7
8
9
// 注意函数做参数的定义方式(函数名称,函数参数,函数返回值)
func function(opt func(float64, float64) float64, a, b float64) float64 {
return opt(a, b)
}

func main() {
fmt.Println(function(math.Pow, 2, 3)) // 2 的 3 次方 = 8
fmt.Println(function(math.Max, 2, 3)) // 2 和 3 中最大 = 3
}
  • 匿名函数
1
2
3
4
5
6
7
8
9
10
11
func function(opt func(float64, float64) float64, a, b float64) float64 {
return opt(a, b)
}

func main() {
fmt.Println(function(
func(f1 float64, f2 float64) float64 { // 使用匿名函数作为 function 的参数,完成减法
return f1 - f2
}, 2, 3),
) // 2 - 3 = -1
}
  • 可变参数列表
    • go 没有默认参数、可选参数、函数重载、操作符号重载等花里胡哨的东西。
    • 只有 可变参数列表,相当于一个数组。
1
2
3
4
5
6
7
8
9
10
11
func sumArgs(values ...int) int {   // values ...int 就是可变参数列表
sum := 0
for i := range values {
sum += values[i]
}
return sum
}

func main() {
fmt.Println(sumArgs(1, 2, 3))
}

2.8 指针

  • 因为类型在后面,定义指针和使用指针是很好区分。
  • 指针不能运算(C 语言中指针的复杂来源于指针 + 1)。
1
2
3
4
var a int = 1
var p *int = &a // 定义指针
*p = 3 // 使用指针
fmt.Println(a) // 3

2.9 参数传递

  • C/C++ 可以值传递,也可以引用传递。
  • java/python 大部分类型是引用传递。
  • go 只有值传递,通过指针可以实现引用传递的效果。

2.10 数组

  • 定义
1
2
3
4
5
6
func main() {
var arr1 [3]int
arr2 := [3]int{1, 2, 3} // 使用 := 就应该赋初值
arr3 := [...]int{1, 2, 3} // 可省略数组个数,但是需要加 ...
var arr4 = [4][5]bool // 定义二维数组
}
  • 求长度

数组、slice、string(特殊的 slice) 求长度均使用 len 函数。

1
n := len(arr)
  • 遍历数组

    推荐使用 range 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
arr2 := [3]int{1, 2, 3}

// 遍历数组
for i := 0; i < len(arr2); i++ {
fmt.Println(arr2[i])
}

// 遍历数组【推荐写法】
for i := range arr2 { // range 函数只获取数组下标
fmt.Println(arr2[i])
}

for i, v := range arr2 { // range 函数获取数组下标和值,r 表示下标,v 表示元素值
fmt.Println(i, v)
}

for _, v := range arr2 { // range 函数只获取数组的值
fmt.Println(v)
}
}
  • 数组是值类型
    • [3]int[4]int 是不同类型,类型不同不能直接调用。
    • 调用 func function(arr [3]int) ,传参会拷贝数组,值传递。(大部分语言的数组传递都是引用传递)
    • 在 go 中,一般不直接使用数组,使用切片。
1
2
3
4
5
6
7
8
9
10
11
// [3]int 和 [4]int 是不同类型,类型不同不能直接调用
func function(arr [3]int) { // 传参会拷贝数组,值传递
for i, v := range arr {
fmt.Println(i, v)
}
}

func main() {
arr := [3]int{1, 2, 3}
function(arr)
}
  • 指向数组的指针作为函数参数【一般不用这种方式,推荐使用切片】
1
2
3
4
5
6
7
8
9
10
11
func function(arrPtr *[3]int) {   // 用 指针传递 作为 引用传递
arrPtr[0] = 100 // 使用 指针 直接取值
for i, v := range arrPtr { // range 可直接遍历 arrPtr 指向的数组
fmt.Println(i, v)
}
}

func main() {
arr := [3]int{1, 2, 3}
function(&arr)
}

2.11 切片

slice:切片

既是切片,也是动态数组。

  • 基本用法
    • arr[2:6] 表示第 2 位到第 5 位,左闭右开。
    • 前后下标可以缺省。
1
2
3
4
5
6
7
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(arr[2:6]) // [2 3 4 5]
fmt.Println(arr[2:]) // [2 3 4 5 6]
fmt.Println(arr[:6]) // [0 1 2 3 4 5]
fmt.Println(arr[:]) // [0 1 2 3 4 5 6]
}
  • 底层原理

    • slice 底层结构是一个结构体,里面包含了指针、元素数量、容量。
    • 对于切片 s,len(s) 可以取出 slice 中的元素数量,cap(s) 可以取出 slice 的容量。

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // slice 中元素的数量
cap int // 容量:从 slice 起始位置到 array 尾部的长度
}
  • 切片做参数
    • go 所有类型都是值类型。slice 传递的也是值,不过这个值是指向数组的指针,可以对数组进行操作。
    • slice 本身是没有数据的,是对底层 array 的一个 view(视图)。
1
2
3
4
5
6
7
8
9
10
func function(arr []int) {   // [] 表示传递的是 slice,[...] 表示传递的是数组
arr[0] = 100
fmt.Println(arr) // [100 3 4 5]
}

func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
function(arr[2:6])
fmt.Println(arr) // [0 1 100 3 4 5 6]
}
  • slice 的拷贝
    • s1、s2 都是 arr 的 view。
1
2
3
4
5
6
7
8
9
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
s1 := arr[1:6]
fmt.Println(s1) // [1 2 3 4 5]
s2 := s1[1:4]
fmt.Println(s2) // [2 3 4]
s2 = s2[1:]
fmt.Println(s2) // [3 4]
}
  • slice 的扩展
    • slice 可以向后扩展,不能向前扩展。
    • s[i] 不能超过 len(s),向后扩展不能超过 cap(s)
1
2
3
4
5
6
7
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
s1 := arr[1:4]
fmt.Println(s1) // [1 2 3]
s2 := s1[2:4]
fmt.Println(s2) // [3 4]
}

  • slice 的添加元素

    • append(slice, elems) 由于值传递的关系,必须接受 append() 的返回值。

      一般 s2 = append(s1, elems)s1 = append(s1, elems)

    • elems 是可变参数,可以加多个。append(slice, elem1, elem2, elem3)

    • slice 的容量足够时,
      • 会通过指针更改原数组的值。
    • 当 slice 的容量不够时,
      • slice 的指针会指向一个新建的数组,并将原数组中的数据拷贝到新数组中。
      • 扩容原则(类似 STL 中的 vector):新数组是原数组容量的两倍,当原数组长度大于 1024 时,将每次按照不小于 25% 的涨幅扩容。
    • 扩容后的 slice 不再指向原数组,无法更改原数组的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
arr := [...]int{0, 1, 2}
s1 := arr[1:2] // [1]


// ======slice 的容量够用======
s1 = append(s1, 20) // [1 20]
fmt.Printf("s1 = %d, len(s1) = %d, cap(s1) = %d\n", s1, len(s1), cap(s1))
// s1 = [1 20], len(s1) = 2, cap(s1) = 2


// ======slice 的容量不够用======
s1 = append(s1, 3, 4)
// 此时,s1 = [1 20 3 4], len(s1) = 4, cap(s1) = 4

s1 = append(s1, 5)
// 此时,s1 = [1 20 3 4 5], len(s1) = 5, cap(s1) = 8


// ======扩容后的 s1 不再指向原数组,无法更改原数组的值======
s1[0] = 999
fmt.Println(arr) // [0 1 20]
}
  • slice 作动态数组(定义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// 定义空 slice
// 此时 s == nil, len(s) == 0,cap(s) == 0
var s []int

// 初始化 slice 的值
// 此时 s1 == [1 2 3],len(s) == 3,cap(s) == 3
s1 := []int{1, 2, 3}

// 定义空 slice 并设置元素输量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 3
s2 := make([]int, 3)

// 定义空 slice 并设置容量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 16
s3 := make([]int, 3, 16)
}
  • slice 作动态数组(拷贝)
    • 利用 copy() 拷贝时,是拷贝元素值,不是指向数组的指针。(类似于 C++ 的深拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s1 := []int{0, 1, 2, 3}
s2 := make([]int, 8, 16)
copy(s2, s1)
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1 2 3 0 0 0 0]

// ======= 当 len(s2) 小于 len(s1) 时,只能拷贝前一部分 =======
s1 := []int{0, 1, 2, 3}
s2 := make([]int, 2, 3)
copy(s2, s1)
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1]
}
  • slice 作动态数组(删除某元素)
    • 使用 append(slice, elems) 函数。
      • s1 = append(s1[:2], s1[3:]...)s1[:2] 中添加 s1[3:] 的所有元素。
      • elems 是可变参数,使用 s1[3:]... 可以省去将 s1[3:] 的所有元素都列举出来。
    • 删除头尾极其方便。
1
2
3
4
5
6
7
8
9
10
11
// 删除中间 2 这元素

func main() {
s1 := []int{0, 1, 2, 3}
s1 = append(s1[:2], s1[3:]...)
fmt.Println(s1) // [0 1 3]
}

// 删除头尾元素比较方便
s1 = s1[1:]
s1 = s1[:len(s1) - 1]

2.12 map

  • 创建

    • 使用 make 建立的是空的 map(empty map),使用 var 定义的是 nil,但是二者可以互相运算,无影响。

      But:nil map 不能直接增加元素,需要用 make 函数创建一个非 nil map。

    • 上述二者打印出来就是 map[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}


// 创建 empty map
m0 := map[int]bool{} // m0 == empty map
m1 := make(map[int]bool) // m1 == empty map

var m2 map[string]string // m2 == nil // nil map 不能直接赋值

fmt.Println(m, m1, m2) // map[cpp:85 golang:100 java:40 python:60] map[] map[]
}

补充:

  • 声明的是 nil map。
  • nil map 不能直接赋值。
  • 但是 nil map 可以取值,返回类型的默认值。
1
2
3
4
5
6
7
8
// 声明一个 nil map
var m2 map[string]string

// m2["app"] = "APP" // error:nil map 不能直接赋值
value, ok := m2["http"] // 正确:nil map 可以返回默认值。value = “”, ok = false

m2 = make(map[string]string) // 创建一个非 nil map,可以直接赋值
m2["app"] = "APP" // 正确。
  • 遍历元素
    • 使用 range 遍历 key 或 value 或 key-value 对。
    • hash map,没有顺序(每次打印的顺序不一样)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

for k := range m { // 只遍历 key
fmt.Println(k)
}

for _, v := range m { // 只遍历 value
fmt.Println(v)
}

for k, v := range m { // 遍历 key-value 对
fmt.Println(k, v)
}
}
  • 增加元素
1
2
m1 := make(map[string]int)
m1["add"] = 1
  • 获取元素
    • 当 key 不存在时,获得 value 类型的初始值。
      • int 初始值是 0,string 初始值是空串 "",bool 初始值是 false。
    • 获取元素时,可两个返回值:value、ok。
      • 存在 ok == true
      • 不存在 ok == false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

// 获取存在的元素
value1 := m["cpp"]
fmt.Println(value1) // 85

// 获取不存在的元素
value2, ok := m["app"]
fmt.Println(value2, ok) // 0, false
}
  • 删除元素
1
2
3
4
5
6
7
8
9
10
11
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

delete(m, "java")
fmt.Println(m) // map[cpp:85 golang:100 python:60]
}
  • map 中的 key 的类型

    • map 使用哈希表,必须可以比较相等。
    • 除了 slice、map、function 的内建类型都可以作为 key。
    • 当 struct 不包括上述类型,也可以作为 key。
  • 复合 map

    map 的 value 是个 map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {

// m 的 key 是 string 类型,value 是 map[string]int 类型。
m := map[string]map[string]int{
"China": {
"cpp": 80,
"java": 20,
},
"USA": {
"cpp": 0,
"java": 0,
},
"England": {},
}
fmt.Println(m) // map[China:map[cpp:80 java:20] England:map[] USA:map[cpp:0 java:0]]
}

2.13 rune

  • 字符串存在的问题:索引和汉字数量无法对上,意味着无法通过索引取到中文汉字。
    • s 有 5 个汉字。
    • len(s) = 15,len 只能获得字节长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
s := "民族有希望"

// 存在问题:索引和汉字数量无法对上
for i, ch := range s {
fmt.Printf("(%d, %c) ", i, ch) // (0, 民) (3, 族) (6, 有) (9, 希) (12, 望)
// fmt.Printf("%c", s[2]) // 无法取到想要的汉字
}
fmt.Println()

// 解决问题:使用 rune 对字符串进行类型转换
for i, ch := range []rune(s) {
fmt.Printf("(%d, %c) ", i, ch) // (0, 民) (1, 族) (2, 有) (3, 希) (4, 望)
}
}
  • 出现问题的原因:
    • string 本质上是字节数组,索引的长度大小是字节,而 utf-8 编码是 3 个字节储存一个汉字,所以汉字数量和索引自然对不上。
    • 之所以转 rune 数组可以解决问题,是因为 rune 是 4 字节储存一个字符/汉字,索引的长度大小是 rune。
  • 十六进制打印验证一下

前置知识点:

  • rune 是 4 个字节储存 1 个字符,不使用 UTF-8 编码方式,使用 unicode 字符集一一对照即可。

  • string 使用的是 UTF-8 编码方式。

  • unicode 只是字符集,UTF-8 是 unicode 的实现方式之一,详见阮一峰:字符编码笔记

如果明白了UTF-8、unicode,应该可以很轻松理解以下两个事实:

  • 在 UTF-8 编码模式下,一个汉字需要 3 个字节表示。
  • 使用 4 个字节储存 1 个汉字,使用 unicode 字符集一一对照时,其实只有后两个字节有数据,前两个字节数据均为 0。

再深入一点,都是使用 unicode 字符集,UTF-8 为什么需要更长的字节?

  • UTF-8 编码是变长的编码方式,所以需要用额外的字符标记,这是 3 个字节表示 1 个字符,而不是 3 个字节表示是 3 个字符。
  • 当规定编码方式为 4 个字节表示 1 个字符,就不需要使用额外的字符来标记多少字节储存一个字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
s := "民族有希望"

// s 本质上就是字节数组,为了将其按 Byte 为单位打印出来,需要将 s 转化为字节数组。
// 每个 ch 是 byte,一共循环 15 次,取出 15 个 byte。
// UTF—8 编码中,3 个字节表示 1 个汉字,所以一共表示 5 个汉字。
for _, ch := range []byte(s) {
fmt.Printf("%X ", ch) // E6 B0 91 E6 97 8F E6 9C 89 E5 B8 8C E6 9C 9B
}
fmt.Println()

// 对于 string,索引 i 还是按照 byte 为单位,但是打印是会按照字符来打印。
// 每个 ch 是 rune,一共循环 5 次,取出 5 个 rune,表示 5 个汉字。
// rune 使用 unicode 符号集,只需 2 个字节可表示一个汉字,所以 rune 的 4 字节,只有后面 2 个字节有数据,故只打印出后面两个字节
for i, ch := range s {
fmt.Printf("(%d, %X) ", i, ch) // (0, 6C11) (3, 65CF) (6, 6709) (9, 5E0C) (12, 671B)
}
fmt.Println()
}
  • 获取 string 中的字符数量。

    • 问题:len(s) 只能获得字节长度,无法获取字符数量。
    • 理解:将 string 转化为 rune,取 rune 的数量。
    • 函数:utf8.RuneCount(string)
  • 在 byte 数组中,获取 rune 字符

    • 理解:将 Byte 数组中的二进制,用 UTF-8 编码,转化为字符,以 rune 类型返回字符,size 表示字符含有几个字节。
    • 函数:ch, size := utf8.DecodeRune(bytes)

2.14 string

PS:反引号可以多行字符串,还能写入引号。

1
2
3
s := `frist line
second line
"33"`
  • string 和 rune 数组、byte 数组的关系。

    • string 本质上是 []byte,但是 string 的类型是 string,[]byte 的类型是 []uint8。
    • string 和 []byte 可以互相转化。
    • string 和 []rune 可以互相转化,但 rune 使用 4 个字节储存 string 的一个字符/汉字。
    • []rune 不可以和 []byte 互相转化。
  • string 拼接:直接 + 字符串(双引号)即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // string 与 string 拼接
    s := "abc"
    s += "123"

    // string 与 []rune 拼接
    r := []rune("abc")
    s += string(r)

    // string 与 []byte 拼接
    b := []byte("abc")
    s += string(b)

    // string 与 字符 拼接
    c := 'd'
    s += string(c)
    // fmt.Printf("%s", s) // abc123abcabcd
  • []byte、[]rune 拼接不能使用 +,使用 append

    1
    2
    3
    4
    5
    6
    7
    8
    9
    r1, r2 := []rune("abc"), []rune("def")
    r1 = append(r1, r2...)
    // fmt.Printf("%c", r1) // [a b c d e f]


    r1, r2 := []byte("abc"), []byte("def")
    r1 = append(r1, r2...)
    fmt.Printf("%c", r1) // [a b c d e f]
    fmt.Printf("%s", r1) // [a b c d e f]
  • string 和 rune 数组遍历

    string 和数组遍历一样。

    for i, value := range s 或者 for i := 0; i < len(s); i++

    1
    2
    3
    4
    5
    6
    7
    // 二维数组的使用方式
    func destCity(paths [][]string) {
    for _, path := range paths {
    path[0] = 'A'
    fmt.Println(path)
    }
    }
  • string[i] 是 “c” or ‘c’?如何判断 s[i] == ‘-‘

    • string[0] 是 uint8 类型。

    • 判断方式: s[i] == 'c'

      注意:上述判断方式只能与 uint8(byte)类型比较,与 uint32(rune)类型比较时,需要强制转换为 uint8(byte)类型。

  • ch := 'a' ch 是 uint32,也就是 rune 类型。

  • string 切片

    string 可以切片;切片可用 + 拼接。

  • 字符串打印

    rune 、[]rune、byte、[]byte 均为整型,必需先将其转换为string才能用 Println 打印出来,否则打印出来的是一个整数。

2.15 strings.Builder

string 类型也是只读且不可变的。因此,这种拼接字符串的方式会导致大量的string创建、销毁和内存分配。Golang 1.10 之后,推出了 strings.Builder 用于拼接字符串。

实现文件:strings/builder.go

1
2
3
4
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)
  • 原理

    1
    2
    3
    4
    type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf []byte
    }
    • 既然 string 在构建过程中会不断的被销毁重建,为了尽量避免这个问题,底层使用一个 buf []byte 来存放字符串的内容。
    • 对于写操作,将新内容 append 到 buf 中即可。
    • buf 容量不够就自动扩容。
  • Grow()

    • 扩容的原理:分配新的更大的 slice。

    • strings.BuilderGrow() 方法是通过 current_capacity * 2 + nn 就是你想要扩充的容量)的方式来对内部的 slice 进行扩容的。

    • 当 n < current_capacity 是不会发生扩容的。

      1
      func (b *Builder) Grow(n int)
  • String()

    • strings.Builder 支持使用 String() 来获取最终的字符串结果。

    • unsafe.Pointer,该类型可以表示任意类型且可寻址的指针值,可以在不同的指针类型之间进行转换。

    • 这里直接将buf []byte转换为 string 类型,无需拷贝 buf,节省内存。

      1
      2
      3
      func (b *Builder) String() string {
      return *(*string)(unsafe.Pointer(&b.buf))
      }
  • 不要拷贝

    • strings.Builder 不推荐被拷贝。当你试图拷贝 strings.Builder 并写入的时候,你的程序就会崩溃。

    • 原因:strings.Builder 拷贝是浅拷贝,拷贝前后两个 builder 指向同一个 buf []byte

    • 拷贝后只支持 len()、string(),不支持更改数据的。除非调用 builder.Reset() 方法重置了 builder,才能写入。

2.15 strings 包

不必过分深究 API,现用现学。参考:字符串操作

strings 包内有诸多字符串操作,常用的有:

  • 分割拼接:Fields,Split,Join
  • 查子串:Contains,Index,Count
  • 转换大小写:ToLower,ToUpper
  • 修建:Trim,TrimRight,TrimLeft
  • 字符串比较:Compare

字符串和基本数据类型之间转换:

  • strconv.Itoa:string 转换为十进制有符号整型,实际上内部调用的 ParseInt(s, 10, 0)

    1
    func ParseInt(s string, base int, bitSize int) (i int64, err error)
    1
    2
    3
    // 例:int 转 string
    a := 0
    s := strconv.Itoa(a)

3.面对对象

  • go 仅支持封装,不支持继承和多态。(面向接口编程)
  • go 没有 class,只有 struct。

3.1 结构体

  • 定义结构体
1
2
3
4
type treeNode struct {
value int
left, right *treeNode
}
  • 创建结构体
    • 无论结构体指针还是结构体,都用 . 来访问成员。
      • C++ 中使用的方式: root.left->right
      • go 中为:root.left.right
    • new 一个变量返回变量地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {

var root treeNode
root = treeNode{value: 3}

root.right = &treeNode{} // right 是指针,需赋值为地址

root.left = &treeNode{5, nil, nil}

root.left.right = new(treeNode)

fmt.Println(root) // {3 0xc0000ac030 0xc0000ac018}
}
  • 创建结构体数组
1
2
3
4
5
6
7
8
 func mian() {
nodes := []treeNode{
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes) // [{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> <nil>}]
}
  • go 没有构造函数
    • 如果需要控制结构体构造,可以写工厂函数。
    • 在 go 中,可以返回局部变量的地址。
      • (C++ 中局部变量分配在栈上,离开函数立即销毁,局部变量的地址不可返回,需要在堆上 new 出变量,并手动释放。)
      • (java 中所有变量都是分配在堆上的,有垃圾回收机制)
      • go 的变量分配在堆上还是栈上,无需使用者关心,由编译器根据代码来决定。
    • 那么,编译器根据代码如何决定,go 的局部变量分配在堆上还是栈上?
      • 如果局部变量没有取地址,并返回出去,将分配在栈上。
      • 如果需要取地址并返回,将分配在堆上。
      • 当该变量不再被使用,就会被垃圾回收机制回收。
1
2
3
4
5
6
7
8
func mian() {
ptr := createNode(10)
fmt.Println(*ptr)
}

func createNode(value int) *treeNode {
return &treeNode{value: value} // 此处返回局部变量的地址,go 中不会有问题。
}

3.2 结构体方法

  • 定义与使用
    • (node treeNode) :go 中叫接受者,相当于 this 指针,标记了哪个结构体在调用结构体方法。
    • 结构体方法其实就是普通函数,使用接收者是为了便于 root.print() 的写法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type treeNode struct {
value int
left, right *treeNode
}

// 结构体方法
func (node treeNode) print() {
fmt.Println(node.value)
}

// 普通函数
func print2(node treeNode) {
fmt.Println(node.value)
}

func main() {
root := treeNode{6, nil,nil}
root.print() // 使用结构体方法
print2(root) // 使用普通方法
}
  • 接收者类型

    • 接收者类型是结构体,该方法无法对结构体变量进行更改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    root := treeNode{6, nil, nil}
    root.setValue(600)
    fmt.Println(root) // {6 <nil> <nil>},root 没有被更改。
    }

    func (node treeNode) setValue(value int) { // 接收者是结构体类型
    node.value = value
    }
    • 接收者类型是结构体指针,该方法可以对结构体变量进行更改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    root := treeNode{6, nil, nil}
    root.setValue(600)
    fmt.Println(root) // {600 <nil> <nil>},root 被更改。
    }

    func (node *treeNode) setValue(value int) { // 接收者是结构体指针类型
    node.value = value
    }
    • 使用值接收者还是指针接收者呢?

      值接收者是 go 特有的,C++ 和 java 都是 this 指针。

      • 建议:最好使用指针接收者。
      • 结构体过大,值传递需要拷贝,推荐使用指针接收者。
      • 要改变结构体内容,必须使用指针接收者。
  • 无论接收者类型是指针还是结构体,都可以接收指针、结构体两种类型。

    • 在 C++ 中,root.get()root_ptr->get()
    • 但在 go 中,root.print()rootPtr.print() 都是用 .
    • 所以 go 中接收者需要同时可以接收这两种类型。本质上都是传递的地址,再根据接收者的需要,取地址还是取结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
root := treeNode{6, nil, nil}
rootPtr := &root

root.print()
rootPtr.print()

root.ptrPrint()
rootPtr.ptrPrint()
}

func (node treeNode) print() {
fmt.Println(node.value)
}

func (node *treeNode) ptrPrint() {
fmt.Println(node.value)
}
  • 结构体指针是nil,也可以调用结构体方法
    • 首先,需要是结构体类型的指针,不可以 nil.happy()
    • 其次,接收者应该是指针类型,否则会报错。(空指针有点特殊)
1
2
3
4
5
6
7
8
func main() {
var ptr *treeNode
ptr.happy()
}

func (node *treeNode) happy() {
fmt.Println("happy")
}

PS:空指针可以调用方法,这能有啥用呢?

A:可以省去在递归是对空指针的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 二叉树的中序遍历
func (node *treeNode) traverse() {
if node == nil {
return
}
node.left.traverse() // 正是因为空指针可以调用方法,所以此处不用判断 node.left 是否为空。
node.print()
node.right.traverse()
}

func (node treeNode) print() {
fmt.Println(node.value)
}

3.3 封装

go 语言,通过函数的命名来进行封装。

  • 函数命名方式:大驼峰、小驼峰。
  • 首字母大写表示 public。
  • 首字母小写表示 private。

3.4 包

  • 每个目录一个包。
  • 包名 main 代表它是一个可独立运行的包,它在编译后会产生可执行文件。所以执行入口 go 文件的包名必须是 main。
  • 为结构定义的方法必须放在同一个包内,但可以是包内的不同的文件。

例如:反转二叉树项目的目录结构。

1
2
3
4
5
6
7
8
9
tree  # 项目目录
├── node.go # 定义 node package tree
├── build_tree.go # 生成二叉树 package tree
├── traversal.go # 反转二叉树 package tree
├── print_tree.go # 打印二叉树 package tree
├── entry_traversal # 启动目录
│   └── entry.go # 启动入口 -- 反转二叉树 package main
└── entry_check # 启动目录
└── entry.go # 启动入口 -- 检查是否反转正确 package main

3.5 扩展已有类型

golang 并没有继承、没有重载,扩展已有类型使用别人的函数,有三种方式:

  • 使用别名

    利用已有的切片,来扩展成队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Queue []int   // 使用别名,将 切片 扩展成 队列

func (q *Queue) Push(value int) {
*q = append(*q, value)
}

func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}

func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
  • 使用组合

    比如,扩展 tree 项目中的 Node 类型,使用别人的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type myTreeNode struct {  // 使用组合的方式实现类型扩展
node *Tree.Node
}

func (myNode *myTreeNode) myNewFunction() { // 用别人的代码实现自己的功能
myNode.node.Traversal()
myNode.node.Print()
}

func main() {
var root myTreeNode
// ...
root.myNewFunction()
}
  • 使用内嵌(embedding)

    • 使用内嵌定义的新类型,是可以直接使用 内嵌类型的变量、函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      type myTreeNode struct {  // myTreeNode 内嵌 *Tree.Node 类型
      *Tree.Node
      }

      func (myNode *myTreeNode) myNewFunction() { // 用别人的代码实现自己的功能
      myNode.Traversal() // 新的类型,可以直接使用 *Tree.Node 类型的功能
      myNode.Print()
      }

      func main() {
      var root myTreeNode
      // ...
      root.myNewFunction()
      }
    • “重载” 了内嵌类型的函数。

      IDE 会标记 shadowed method,其实并不是重载。

      • 这两个 Traversal 并没有任何关系,只是语法糖导致 node 的方法可以直接被 myNode 调用。
      • 父类指针无法指向子类对象。(go 语言通过接口来实现这样的能力。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      type myTreeNode struct {  // myTreeNode 内嵌 *Tree.Node 类型
      *Tree.Node
      }

      func (myNode *myTreeNode) Traversal() { // 和内嵌类型的函数名一样。
      // ...
      }

      func main() {
      var root myTreeNode
      // ...
      root.node.Traversal() // 使用内嵌类型的 Traversal 函数。
      root.Traversal() // 使用新类型的 Traversal 函数。
      }

4.依赖管理

go 的依赖管理有三个发展阶段:GOPATH、GOVENDOR、go mod。

推荐使用 go mod

4.1 GOPATH

  • 默认在 ~/go,使用第三方库依赖时,GOPATH 下的 src 目录下开始寻找。
  • 缺点
    • 所有的项目、依赖的第三方库都放在 GOPATH 下。
    • 不同项目所依赖的第三方库版本可能不一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GOPATH
├── bin
└── src
├── go.uber.org # 依赖的第三方库
│   └── zap
├── golang.org # 依赖的第三方库
│   └── x
├── project1 # 项目一
│   └── hellogo
│   └── hello.go
└── project2 # 项目二
├── entry
│   └── main.go
└── zaptest
└── zaptest.go

4.2 GOVENDOR

  • 第三方依赖管理库:glide、dep、go dep…
  • 理念
    • 项目所依赖的第三方库放在自己的 vendor 的目录下。
    • 使用第三方库依赖时,从项目中的 vendor 目录下寻找。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── bin
│   └── goimports
└── src
├── project1 # 项目一
│   ├── hellogo
│   │   └── hello.go
│   └── vendor # project1 依赖的第三方库
│   └── golang.org
└── project2 # 项目二
├── entry
│   └── main.go
├── zaptest
│   └── zaptest.go
└── vendor # project2 依赖的第三方库
├── golang.org
└── go.uber.org
  • 缺点
    • 需要手动管理第三方库。
    • 即使使用第三方依赖管理库,依然会有种打补丁的感觉。

4.3 go mod

go module

  • 原生的依赖管理。
  • 理念:用户只需关心自己写的源代码即可,无需管理第三方依赖库。
  • 优点:
    • 使用 go.mod 对工程进行管理,只记录依赖的第三方库的名称和版本。
    • 第三方库的实际代码放在 pkg 目录下进行缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go    # GOPATH
├── bin # Go 构建的编译可执行程序的位置
└── pkg # 编译后包文件会生成.a文件,放置在 $GOPATH/pkg/$GOOS_$GOARCH中。
│ └── mod # 程序寻找第三方库,就在该目录下寻找
│ ├── go.uber.org # 依赖的第三方库的版本信息
│ └── cache # 依赖的第三方库代码实际缓存的位置
├── project1 # 项目一
│ ├── go.mod # 用于依赖管理的 mod 文件:记录了所依赖的第三方库名称及其版本。
│ ├── go.sum # 对于 mod 文件更详细的描述:包含 现在 和 曾经 依赖的第三方库名称、版本、hash 值。
│ └── zaptest # 包
│ │ ├── func.go # 封装的功能一
│ │ └── zaptest.go # 封装的功能二
│ └── entry # 包
│ └── entry.go # 启动程序,调用第三方库和封装的功能

└── project2 # 项目二

4.4 相关命令

  • 如果项目中无 go.mod 文件,需要初始化:go mod init

  • 查看当前项目的配置:go env

  • 临时关闭 go mod:export GO111MODULE=off

  • 增加第三方依赖库:go get

  • 更新第三方库版本:添加版本号,例如 go get -u go.uber.org/zap@v1.18.0

  • 去除多余的第三方库:go mod tidy

5.接口

5.1 概念

  • 在其他强类型语言中,

    • 接口本质上是定义接口规范,保证所有子类都有相同的接口实现。
    • 接口就是一个抽象类,多个派生类,可以继承该抽象类,并实现抽象类的方法。
    • 目的:从而实现多态,降低代码的耦合度,提高可扩充性和可维护性。(派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容。)
  • 在 go 中,

    • go 没有继承和多态,使用接口来实现相关的功能。
    • 只要实现了接口的某个方法,就可以使用接口。(不必全部实现接口的所有方法。)
  • go 的接口的语法样例:

1
2
3
4
5
6
7
8
9
10
11
12
type Retriever interface {   // 定义接口类型 Retriever
Get(string) string
}

func getRetriever() Retriever { // 返回接口类型 Retriever
return infra.Retriever{} // 实际上返回的是 infra 的 Retriever
}

func main() {
retriever := getRetriever() // 使用者,使用 Retriever 接口类型
retriever.Get("https://www.aimtao.net")
}

5.2 为什么需要接口

为什么强类型语言需要有接口,而弱类型语言不需要接口呢?

在下面代码中,没有使用接口,会有哪些问题呢?

1
2
3
4
5
6
7
8
func getRetriever() infra.Retriever {
return infra.Retriever{}
}

func main() {
var retriever infra.Retriever = getRetriever()
retriever.Get("https://www.aimtao.net")
}
  • main 函数中,getRetriever() 返回的类型是 infra.Retriever ,也就是说 retriever 的类型就是 infra.Retriever

  • 其他人也实现了一个不同功能的 Retriever 类型,当 main 函数需要使用这个人的 Retriever 时,要做哪些修改呢?

    • 更改 getRetriever 函数定义中的返回值类型、返回值。
    • main 函数中 变量类型。

有了接口后,只需更改 getRetriever 函数定义的返回值即可。

1
2
3
4
5
6
7
8
9
10
11
12
type Retriever interface {  // 接口定义
Get(string) string
}

func getRetriever() Retriever {
return infra.Retriever{} // 只需要更改此处的返回值即可。
}

func main() {
retriever := getRetriever()
retriever.Get("https://www.aimtao.net")
}
  • 通过使用接口,就减少了 main 函数和 infra.Retriever 的耦合性。
  • main 函数就可以在不更改代码的前提下,使用各种各样的 Retriever 类型,从而实现了向后兼容。

5.3 duck typing

  • “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
  • 描述事物的外部行为,而非内部结构。
  • 进一步具体来讲:方法 A 可以接收不同类型的对象,只要该对象实现了方法 A 所用到的方法 B。

go 是不是 duck typing ?

  • go 属于结构化类型系统,类似 duck typing,但不是 duck typing。
    • 为什么类似?go 符合 “描述事物的外部行为,而非内部结构。”
    • 为什么不是?duck typing 要求使用多态,动态绑定,而 go 语言在编译时就绑定了。

python 的 duck typing:

  • download 函数在接收、使用 retriever 时,并不知道 retriever 有没有实现 get 方法。
  • 只有运行时,才知道 retriever 有没有实现 get 方法。
  • 需要使用注释来说明接口。
1
2
3
# 定义 download 函数
def download(retriever):
return retriever.get("https://www.aimtao.net")

C++ 的 duck typing:

  • C++ 使用模板函数来实现 duck typing。
  • 只有编译时,才知道传入的 retriever 有没有实现 get 方法。
  • 需要使用注释来说明接口。
1
2
3
4
template <class R>
string download(const R& retriever) {
return retriever.get("https://www.aimtao.net");
}

Java 中的类似代码:

Java 没有 duck typing,只有类似代码。

  • 因为有 <R extends Retriever> 类型限定,所以传入的参数,必须实现 Retriever 的所有接口。
  • 优点:不需要用注释来说明接口,通过类型来限定。r 没有 get 方法,写代码时就会报错。
  • 缺点:
    • 不是 duck typing,r 只有 get 方法还不行,需要完整地实现 Retriever 接口。
    • Java 不可以多继承,所以如果 r 需要同时使用两个不同类型的 duck typing 接口,Java 是做不到的。
1
2
3
4
<R extends Retriever>
String download(R r) {
return r.get("https://www.aimtao.net");
}

go 的 duck typing:

结合了各语言的优点。

  • 有 python、C++ 的 duck typing 的灵活性。
  • 有 Java 的类型检查。

只要 retriever 实现了 Get 方法,就可以传入 download 函数中,被 download 函数使用。

1
2
3
4
5
6
7
type Retriever interface {  // 接口定义
Get(string) string
}

func download(Retriever retriever) string {
return retriever.Get("https://www.aimtao.net")
}

5.4 接口的定义与实现

在上面的 go 的 duck typing 样例中,

  • retriever 是实现者。
  • download 是使用者。

在传统的面向对象中,接口是由实现者定义的,retriever 实现了某些接口,download 才可以使用某些接口。

在 go 中,接口由使用者定义,download 要使用有 Get 方法的接口,只要实现 Get 方法的对象,都是接口的实现者。

实例:

  • downlod 要使用有 Get 方法的接口。
  • 实现两个有 Get 方法的接口,一个 real、一个 mock,共 download 函数使用。

(1)mock/retriever.go

让 mock.Retriever 实现 Get 方法。

1
2
3
4
5
6
7
8
9
package mock

type Retriever struct {
Contents string
}

func (receiver Retriever) Get(url string) string { // 返回一个固定的假值。
return receiver.Contents
}

(2)real/retriever.go

让 real.Retriever 实现 Get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package real

type Retriever struct {
UserAgent string
TimeOut time.Duration
}

func (r Retriever) Get(url string) string { // 返回利用 http 库获取到的真值
response, err := http.Get(url)
if err != nil {
panic(err)
}

result, err := httputil.DumpResponse(response, true)
response.Body.Close()
if err != nil {
panic(err)
}

return string(result)
}

(3)main.go

分别使用 mock.Retriever 和 real.Retriever。

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
package main

import (
"fmt"
"learngo/retriever/mock"
"learngo/retriever/real"
)

type Retriever interface { // 定义接口。
Get(url string) string
}

func download(r Retriever) string { // 使用接口。
return r.Get("https://www.aimtao.net")
}

func main() {
var r Retriever // 定义接口变量的 r

r = mock.Retriever{Contents: "This is mock message"} // 使用 mock 的 Get
fmt.Println(download(r))

r = real.Retriever{} // 使用 real 的 Get
fmt.Println(download(r))
}

5.5 接口变量的值类型

fmt.Printf("%T, %v\n", r, r)

  • %T 打印变量类型

  • %v 打印变量的值

  • 接口变量 r 储存两个内容:实现者的类型、实现者的指针或实现者的值。

    使用指针的好处:避免使用者的多次值拷贝,节省内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
real/retriever.go 使用指针接收者。
*/

type Retriever struct {
UserAgent string
TimeOut time.Duration
}

// 使用指针接收者
func (r *Retriever) Get(url string) string {
response, err := http.Get(url)
if err != nil {
panic(err)
}

result, err := httputil.DumpResponse(response, true)
response.Body.Close()
if err != nil {
panic(err)
}

return string(result)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
main.go
*/

func main() {
var r Retriever
fmt.Printf("%T, %v\n", r, r) // <nil>, <nil>

r = mock.Retriever{Contents: "This is mock message"}
fmt.Printf("%T, %v\n", r, r) // mock.Retriever, {This is mock message}

// 只能赋值为指针
r = &real.Retriever{
UserAgent: "Mozilla/5.0",
TimeOut: time.Minute,
}
fmt.Printf("%T, %v\n", r, r) // *real.Retriever, &{Mozilla/5.0 1m0s}
}
  • 实现方法是指针接收者的,接口变量使用时该实现时,只可以赋值为指针;
  • 实现方法是值接受者的,接口变量使用时该实现时,既可以赋值为指针,也可以赋值为值。
1
2
3
4
5
6
7
8
9
10
11
func main() {
var r Retriever

// 既可以赋值为指针
r = &mock.Retriever{Contents: "Pointer receiver"}
fmt.Printf("%T, %v\n", r, r) // *mock.Retriever, &{Pointer receiver}

// 也可以赋值为值
r = mock.Retriever{Contents: "Value receiver"}
fmt.Printf("%T, %v\n", r, r) // mock.Retriever, {Value receiver}
}

5.6 查看接口变量

5.6.1 Type Assertion

如果 r 是 mock.Retriever 类型,r.(mock.Retriever) 则表示 mock.Retriever 类型,否则会报错。

1
2
3
4
5
6
7
func inspect(r Retriever) {
if mockRetriever, ok := r.(mock.Retriever); ok {
fmt.Println("Contents: ", mockRetriever.Contents)
} else {
fmt.Println("UserAgent: ", r.(*real.Retriever).UserAgent)
}
}

5.6.2 Type Switch

r.(type) 可以获取到接口变量内的具体 Retriever 变量。

  • r.(type).Contents :正确。
  • r.Contents:错误。r 是接口变量,虽然可以被复制为 mock.Retriever,但并可以直接取 Contents。
1
2
3
4
5
6
7
8
func inspect(r Retriever) {
switch value := r.(type) {
case mock.Retriever:
fmt.Println("Contents: ", value.Contents)
case *real.Retriever:
fmt.Println("UserAgent: ", value.UserAgent)
}
}

5.6.3 任何类型表示法

interface{} 表示任何类型。

以 Queue 文件为例,使用 interface{} 表示 Queue 中可以储存任何类型。

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
package queue

// type Queue []int // 定义 Queue 表示 int 数组类型。
type Queue []interface{} // 定义 Queue 表示 任何类型数组 的类型

func (q *Queue) Push(value interface{}) {
*q = append(*q, value)
}

func (q *Queue) Pop() bool {
len := len(*q)
if len != 0 {
*q = (*q)[1:]
return true
}
return false // 删除之前,队列为空,返回 false
}

func (q *Queue) Front() interface{} {
return (*q)[0]
}

func (q *Queue) Back() interface{} {
len := len(*q)
return (*q)[len-1]
}

func (q *Queue) Size() int {
return len(*q)
}

func (q *Queue) Empty() bool {
len := len(*q)
if len != 0 {
return false
}
return true
}

定义 interface{} 后,如果需要进行类型约束怎么办?

  • 实际上与 r.(type) 一致。

  • 但此类约束,只有运行时,才会报错。比如以下代码:

1
2
3
4
5
func main() {
var a interface{}
a = "0"
fmt.Println(a.(int))
}

5.7 接口的组合

mock.Retriever 实现了 Get / Post 方法,也就表示分别实现了 Retriever 和 Poster 接口。(类似于 C++ 多继承了两个抽象类)。

接口最本质的目的就是:将实现了接口的类的对象,赋值给接口变量,由接口变量调用对象的所实现的接口方法。

那如果接口变量既要调用 Get 方法,又要调用 Post 方法,请问这个接口变量应该是什么类型呢?

答:就是接口组合类型。

s 是一个 mock.Retriever 类型,但既要使用 Get 方法,也要使用 Post 方法,在执行 session(s RetrieverPoster) 函数时,只能用一个组合接口类型变量来接受 s。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
mock/retriever.go
*/

package mock

type Retriever struct {
Contents string
}

func (receiver *Retriever) Get(url string) string {
return receiver.Contents // 返回一个固定的假值。
}

func (receiver *Retriever) Post(url string, form map[string]string) string {
receiver.Contents = form["Contents"]
return "ok"
}
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
/*
main.go
*/

type Retriever interface { // Retriever 接口
Get(url string) string
}

type Poster interface { // Poster 接口
Post(url string, form map[string]string) string
}

type RetrieverPoster interface { // 组合接口:Retriever & Poster
Retriever
Poster
}

func download(r Retriever) string {
return r.Get(url)
}

// s 既可以调用 Post,也可以调用 Get,只需要 mock.Retriever 同时实现了 Get、Post 方法。
func session(s RetrieverPoster) string {
s.Post(url, map[string]string{
"Contents": "I will change contents",
})
return s.Get(url)
}

const url = "https://www.aimtao.net"
const mockContents = "This is mock message"

func main() {
s := &mock.Retriever{Contents: mockContents}
fmt.Println(download(s))
fmt.Println(session(s))
}

5.8 常用的系统接口

5.8.1 Stringer

1
2
3
type Stringer interface {
String() string
}

5.8.2 Reader/Writer

希望把底层一些的 IO 操作封装在 Writer/Reader 里。

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

5.9 函数式编程

函数是一等公民:参数、变量、返回值都可以是函数。

5.10 闭包

python、C++、Java 也支持闭包。

以下面代码为例,sum 是 自由变量,v 是局部变量。

在将 adder() 赋值给 a 后,a 不光拥有一个你们函数,还拥有一个自由变量 sum。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func adder() func(int) int {
sum := 0
return func(v int) int {
sum += v
return sum
}
}

func main() {
a := adder()
for i := 0; i < 5; i++ {
fmt.Println(a(i))
}
}

/*
1
3
6
10
*/

5.10.1 实例:fibonacci

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fibonacci() func() (int, int) {
a, b := 1, 1
return func() (int, int) {
a, b = b, a+b
return a, b
}
}

func main() {
f := fibonacci()
fmt.Println(f()) // 1 2
fmt.Println(f()) // 2 3
fmt.Println(f()) // 3 5
}

5.10.2 实例:函数实现接口

fibonacci 返回的匿名函数,实现了 Read 接口的方法。该方法的作用就是返回 io.Reader 变量。

既然实现了接口,printFileContents 就可以用接口变量 io.Reader 来接收匿名函数。

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
func fibonacci() intGen {
a, b := 1, 1
return func() (int, int) {
a, b = b, a+b
return a, b
}
}

type intGen func() (int, int)

// intGen 匿名函数实现接口的 Read 方法:将值格式化为字符串。
func (g intGen) Read(p []byte) (int, error) {
nextA, nextB := g()
if nextA > 1000 {
return 0, io.EOF
}
s := fmt.Sprintf("%d, %d\n", nextA, nextB)
return strings.NewReader(s).Read(p)
}

// 接收一个 reader,代替我们完成打印动作。
func printFileContents(reader io.Reader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}

func main() {
f := fibonacci()
printFileContents(f)
}

6.资源管理与出错处理

6.1 defer 调用

  • 确保调用在函数结束时发生。

    函数结束时才会自动调用,即使发生 panic 程序中断也会执行 defer 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
func tryDefer() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
panic("mock error")
}

/* 输出如下:
3
2
1
panic: mock error
*/
  • defer 列表为先进后出(栈)。

  • 参数在 defer 语句时计算。

    defer 调用时的变量值,与正常顺序调用的变量值一致。如程序所示,即使打印 “defer start” 时 i 值为 5,但 defer 调用仍然是之前计算好的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func tryDefer() {
for i := 0; i <= 5; i++ {
defer fmt.Println(i)
if i == 5 {
fmt.Println("defer start.")
panic("printed too many.")
}
}
}

/* 输出如下:
defer start.
5
4
3
2
1
0
panic: printed too many.
*/

7.测试

8.goroutine

9.chanel


本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!