Go语言入门学习

cover

这是一篇关于Go语言的学习总结,会不定期更新~

一、准备工作

讲道理我每次都最讨厌配环境了……出个路径问题等等岔子要折腾好久。

首先是在官网上下Go语言的相关文件,再打开我的电脑高级配置改环境变量和系统变量:

  • GoPath 对应的是工作区域,随便开一个地就行
  • GoRoot 对应下载下来的 Go 相关文件,一般来说它会自动加个 /bin

打开 cmd 输一下 go version 康康版本,再输个 go env 检查一下路径。

我的 VSC 上的 Go 环境挂了,于是下载了 GoLand ,确实香。

二、了解基础知识

1、包、变量和函数

就基本结构而言,由于祖师爷肯汤普森和 C 语言的加成,Go 在某些方面确实与 C 有异曲同工的地方,不过在谷狗大力推行“去C语言化”的环境下,现在的 Go 基本能自举了

首先我们来看一个每一个程序员入门都要写的程序:Hello World

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Printf("hello, world\n")
}

这里的 main 以及 fmt 都是程序调用的包,每个 Go 程序都是由包构成的。

import 是用路径导入 “fmt” 这个包,按照约定,包名与导入路径的最后一个元素一致。本例中有 **fmt.Printf(“hello, world\n”) **这条代码,假如直接用 Printf 是不行的。

这和 C family 的 #include 有些相似,可以把包理解为一个函数库

我们还注意到,Printf 的首字母是大写的,这表明他是一个已导出名,相对的小写则是未导出名,在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。

那么问题又来了,假使我们要一次性导入多个包,该如何做呢?

这种情况下采用“分组”导入形式,代码用圆括号组合了导入,例如:

1
2
3
4
import (
"fmt"
"math"
)
函数

关于函数,和 C 很相似,但是在类型声明时类型在变量名 之后。同样的,函数可以接受多个或不接受参数。当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func add(x, y int) int {//这里x省略了类型
return x + y
}

func main() {
fmt.Println(add(42, 13))
}

但是在 C 语言中,我们返回值只会存在一个,而在 Go 语言中,可以做到 多值返回

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, b := swap("hello", "world")//这里赋值也是两个变量一起赋值
fmt.Println(a, b)
}
命名返回值

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。

返回值的名称应当具有一定的意义,它可以作为文档使用。

没有参数的 return 语句返回已命名的返回值。也就是 直接 返回。

我们看下面例子中 x,y 是提前命名好的,因此在无返回值时就返回他们。

直接返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}

func main() {
fmt.Println(split(17))
}
变量

var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后,var 语句可以出现在包或函数级别。

并且支持类型的自动推导,可以从初始值中获取类型。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

var i, j int

func main() {
var c, python, java = true, "omg", "no!"
fmt.Println(i, j, c, python, java)
}

并且有趣的是,在没有初始值只有类型的情况下,Go 会自动将变量置空,例如上例中最后输出会是 0 0 true omg no! i 和 j 被自动置为 0。

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。
短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

基本类型

Go 的基本类型有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128 //复数

本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

强制类型转换

这个东西其实和 C 一样的,int(x)这样直接转类型。然而在 C 语言中系统会默认转换成等号左边的类型,也就是隐式转换——在 Go 中是无法实现的,必须要用 显示转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"math"
)

func main() {
var x, y int = 3, 4
var f float64 = math.Sqrt(float64(x*x + y*y))
var z uint = uint(f)
//这个例子中,假若我们把 f 的强制类型转换删去变成 var z uint = f,是会发生类型错误的
fmt.Println(x, y, z)
}
类型推导

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。

当右值声明了类型时,新变量的类型与其相同:

1
2
var i int
j := i // j 也是一个 int

不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64complex128 了,这取决于常量的精度:

1
2
3
i := 42           // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
常量

常量的声明与变量类似,只不过是使用 const 关键字。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

const Pi = 3.14

func main() {
const World = "世界"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")

const Truth = true
fmt.Println("Go rules?", Truth)
}

2、流程控制语句:for、if、else、switch 和 defer

关于 for

Go 只有一种循环结构:for 循环。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式:在每次迭代前求值
  • 后置语句:在每次迭代的结尾执行

初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。

一旦条件表达式的布尔值为 false,循环迭代就会终止。

值得注意的就是 Go 中 for 没有使用圆括号,而 “{ }”必须 的!(没有就报错)

同样,for 语句可以删到只剩中间一条语句,分号无所谓,实际上去掉分号就变成了 while 语句了。

关于 if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

麻了,怎么在一些奇奇怪怪的方面“大道至简”……

for 一样, if 语句可以在条件表达式前执行一个简单的语句。

该语句声明的变量作用域仅在 if 之内。

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

import (
"fmt"
"math"
)

func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}

func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
if 和 else

if 的简短语句中声明的变量同样可以在任何对应的 else 块中使用。也就是说,局部变量的作用域会拓展至 else 语句中。不过有一个很怪的点就是 else 使用时必须和 if 结尾的 “}” 相连(毕竟 Go 判断语句结尾其中一个根据就是换行)

同样 else if 也完全可以使用。

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

import (
"fmt"
"math"
)

func Sqrt(x float64) (z float64) {
z=1.0
var k float64
for {
z-=(z*z-x)/(2*z)
if math.Abs(k-z)<0.0000000001{
return
}else{
k=z
}
}
}

func main() {
fmt.Println(Sqrt(2))
fmt.Println(math.Sqrt(2))
}

上面是一个利用牛顿法结合 for 和 if 语句编写的找平方根函数。

switch

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

与 C 不同的是,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。而且 case 无需为常量,且不需要是整数。

switch 的 case 语句从上到下顺次执行,直到 匹配成功时停止

例如,

1
2
3
4
switch i {
case 0:
case f():
}

i==0f 不会被调用。

没有条件的 switch 同 **switch true**一样。

这种形式能将一长串 if-then-else 写得更加清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
defer

defer 语句会将函数推迟到 外层函数返回之后 执行。

推迟调用的函数其参数会 立即求值,但直到外层函数返回前该函数都不会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
fmt.Println("counting")

for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

fmt.Println("done")
}

上面的代码中我们很容易看出循环中的语句会延后到 main 函数结束,然而输出的数字却呈 9、8、7、6……的倒序排列,为什么呢?

这是因为推迟的函数调用会被 压入一个栈 中。当外层函数返回时,被推迟的函数会按照 后进先出 的顺序调用。

3、更多类型:struct、slice 和映射

指针

Go 拥有指针。指针保存了值的内存地址。

类型 *T 是指向 T 类型值的指针。其零值为 nil

1
var p *int

& 操作符会生成一个指向其操作数的指针。

1
2
i := 42
p = &i

* 操作符表示指针指向的底层值。

1
2
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。

与 C 不同,Go 没有指针运算

结构体

是一组字段,例如:

1
2
3
4
type Vertex struct {
X int
Y int
}

但是,如果改为 var X int 就会报错,这是因为 var 是声明变量,而 struct 中是不存在变量的,只有类型

与 C 类似的,使用 . 来访问字段。(指针也一样)

结构体指针:如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

结构体文法:

  • 使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)
  • & 求址符可以返回一个指针
1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
数组

类型 [n]T 表示拥有 nT 类型的值的数组。

表达式

1
var a [10]int

会将变量 a 声明为拥有 10 个整数的数组。

数组的长度是其类型的一部分,因此数组 不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var a [2]string//使用字符串数组
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

primes := [6]int{2, 3, 5, 7, 11, 13}
//同样,可以在声明数组的同时进行初始化
fmt.Println(primes)
}
切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

1
a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

1
a[1:4]

注意:

切片中并 不包含任何数据,他可以理解为数组的引用,更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。

更改切片中元素时,对应初始下标位置会随着切片位置改变而变动

切片文法:类似于 C 中的变长数组。

当使用 []bool{true, true, false} 时,会自动创建一个同类型的等长数组并构建一个引用它的切片,就效果而言和变长数组差不多

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

import "fmt"

func main() {
q := []int{2, 3, 5, 7, 11, 13}
fmt.Println(q)

r := []bool{true, false, true, true, false, true}
fmt.Println(r)

s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}//这一段实际上是创建了一个结构体数组
fmt.Println(s)
}

切片的默认行为

在进行切片时,你可以利用它的默认行为来忽略上下界。

切片下界的默认值为 0,上界则是该切片的长度。

对于数组

1
var a [10]int

来说,以下切片是等价的:

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

注:切片可以再切成更小的切片。

切片的长度与容量:切片长度就是它 包含元素的个数

切片的容量是从它的第一个元素开始数,到 其底层数组 元素末尾的个数。

可以通过 len(s) 获取长度,cap(s) 获取容量。

nil 切片:

切片的零值是 nil

nil 切片的长度和容量为 0 且 没有底层数组

用 make 创建切片:

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

1
a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

1
2
3
4
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4

切片可包含 任何类型,甚至可以切片套娃。

向切片追加元素:

Go 为我们提供了一个快捷追加切片元素的方法:使用内置的 append 函数,其函数原型为:

1
func append(s []T, vs ...T) []T

其中,第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该 切片的末尾

1
2
3
4
5
6
func main() {
var s []int// 添加一个空切片
s = append(s, 0)
s = append(s, 1)// 这个切片会按需增长
s = append(s, 2, 3, 4)// 可以一次性添加多个元素
}

最后就应该是 [] –> [0] –> [0,1] –> [0,1,2,3,4]

Range

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

一般是这么用的:

1
2
3
for i,v := range (数组名){

}

i 对应下标,v 对应元素副本(值)

可以将下标或值赋予 _ 来忽略它。

1
2
for i, _ := range pow
for _, value := range pow

若你只需要索引,忽略第二个变量即可。

1
for i := range pow
映射

和对应的 组成。相较于 C++ 以及 Java,Go 的映射使用时不需要任何的库。

映射将键映射到值。

映射的零值为 nilnil 映射既没有键,也不能添加键。

对于建立映射,Go 主要提供两种方法:

1、内置的 make 函数:

1
2
// 创建一个映射,键的类型是 string,值的类型是 int
myMap := make(map[string]int)

make 函数会返回给定类型的映射,并将其初始化备用。

2、字面量声明映射(常用):

1
2
3
// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
myMap := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

关于这里官方文档有个示例:

1
2
3
4
5
6
7
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},

很怪的是 Vertex 屁股后面的大括弧里最后一个数尾巴上都带了一个 ,

我把它删掉后直接报错了。。。

原来 Go 语言中 如果一个多行切片,数组或映射文字分行表达,那么每个元素后面都要加上一个 ,

若顶级类型只是一个 类型名,你可以在文法的元素中 省略 它。

比如上例中省略 Vertex 是完全科学的。

映射的查找与修改:

查找方式有两种:

1、引入代表存在性的布尔值:

1
elem, ok = m[key]

如果elem ok 还未声明,你可以使用短变量声明:

1
elem, ok := m[key]

2、直接检测对应值是否为零值:

1
elem = m[key]

但是要注意,这种方法仅仅适用于键值对中不存在零值的情况。

如何修改与删除?

在映射 m 中插入或修改元素:

1
m[key] = elem

获取元素:

1
elem = m[key]

删除元素:

1
delete(m, key)
练习:映射

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败。

你会发现 strings.Fields 很有帮助。

网上查了一下,这个 strings.Fields 函数是将 string 字符串中被空隔开(比如空格和制表符等)的单字拆分出来放进一个数组中,因此 它的返回值一定是一个数组

所以再用上我们之前学过的 range 循环并让对应映射位数值 ++

函数值

函数也是值。它们可以像其它值一样传递。

函数值可以用作函数的参数或返回值。

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

import (
"fmt"
"math"
)

func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))//ans:13

fmt.Println(compute(hypot))//ans:5
fmt.Println(compute(math.Pow))//ans:81
}

从上面的例子可以看出:在函数 compute 中,fn 被赋予了函数类型,因此 compute 函数中的参数是一个调用了两个浮点数返回值也是浮点数的函数。

函数的闭包

简而言之,闭包函数就是一个 返回值为函数 的函数,闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起

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

import "fmt"

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

比如在上例中,出现了多个 adder 返回的闭包,如果不使用闭包,就需要很多的全局变量。而使用闭包后,每个 adder 返回的闭包都与对应的 sum 值相绑定,不会出现混淆。

三、方法和接口

方法

不同于 C++ 以及 JAVA 这类面对对象编程语言存在类这种方便的东西,Go语言没有类。

不过没关系,我们仍然可以通过为 结构体类型定义方法 将函数和它对应的变量相连接。

我们这里会使用一个叫 接收者 的参数,它位于 func 关键字和方法名之间。

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

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

上例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

:除了有个接收者以外,方法就是函数 的一种,功能都一样

可以为非结构体类型声明方法,但是!!!接收者类型必须和方法声明在 同一个包 里面!!!

换句话说,在其它包里的内建类型(int、float 等)是不能够直接拿来作为接收者类型的,需要再一次声明一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"math"
)

type MyFloat float64//将包内 MyFloat 将其它包的 float64 替换了,效果一样

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

指针接收者:你可以为指针接收者声明方法。

这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(此外,T 不能是像 *int 这样的指针。)

采用指针才能使用方法 更改指针接收者指向的值。如果不采用指针,那么方法使用的仅仅是 参数对应的副本,而不会对其值有任何影响

选择值或指针作为接收者:

使用指针接收者的原因有二:

  • 首先,方法能够 修改 其接收者指向的值。
  • 其次,这样可以 避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会 更加高效

接口

接口是一组 仅包含方法名、参数、返回值的未具体实现的 方法的集合。

接口是一种 类型

在 C++ 等面对对象编程中,很多时候为了高效便捷,会引入一个多态的概念(多态是同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作),而接口就是一个非常能体现多态这一特点的类型。

当我们编写出了不同类型的变量和他们对应的方法,如果这些方法调用过程都非常相似,那么就可以用接口来进行简化。普通函数的缺陷在于他们需要了解参数类型,倘若类型不同是不能互通的。而接口不关心其类型,唯一需要知道的是通过对应方法能做什么。

接口的定义格式:
1
2
3
4
5
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

实现接口指的是什么?就是让一个对象能实现接口中所有的方法,那么这个对象就实现了接口。接口就是一个 需要实现的方法列表

值接收者 VS 指针接收者

当使用值接收者的方法时,对应接口是 可以处理值或者对应指针 的,因为Go语言中有对指针类型变量求值的语法糖,指针内部会自动求值

而如果使用指针接收者,对应接口是 不能处理普通值类型的变量的!只能使用指针类型。

一个类型实现多个接口

接口间是相互独立的,只需要把要使用的接口与类型本身对接就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type dog struct {
name string
}

// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}

func main() {
var x Sayer
var y Mover

var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}

多个类型实现一个接口

除了让不同类型都实现同一个接口,还可以让多个类实现一个接口,接口的方法可以通过在类型中 嵌入 其他类型或者结构体来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
接口嵌套

接口间也可以形成包含关系,创造出可以使用不同方法的新接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Sayer 接口
type Sayer interface {
say()
}

// Mover 接口
type Mover interface {
move()
}

// 接口嵌套
type animal interface {
Sayer
Mover
}
空接口

定义:

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

1
var x interface{}//空接口可以这样直接声明

应用:

  • 作为函数的参数,这样一来函数就可以 接受任意类型的参数
  • 作为 map 的值,这样一来映射中可以 保存任何类型的值
1
var x = make(map[string]interface{})
接口值

一个接口的值(简称接口值)是由 一个具体类型具体类型的值 两部分组成的。这两部分分别称为接口的 动态类型动态值

想判断空接口包含的值时,可以使用类型断言:

1
2
x.(T)// x:表示空接口的变量
// T:断言 x 可能的类型

这样会返回两个值,一个是转化为 T 后的变量,第二个则是 bool 值,若为 true 则表示断言成功,为 false 则表示断言失败。

1
t,ok:=x.(T)

x 保存了一个 T,那么 t 将会是其底层值,而 oktrue

否则,ok 将为 falset 将为 T 类型的零值,程序并不会产生恐慌。

判定一个值的类型还可以使用类型选择:

类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}

类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type

此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 TS 的情况下,变量 v 会分别按 TS 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 vi 的接口类型和值相同。

Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一。

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

Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

意思就是你只需要针对某一类型实现这个方法,就可以直接在 fmt 的很多函数中直接用(比如 Println ,很多 IDE 都会显示函数是否存在接口)。

错误处理

Go 程序使用 error 值来表示错误状态。

同样,error 类型是一个内建接口:

1
2
3
type error interface {
Error() string
}

所以函数在返回值时也默认会返回一个 err 值。一般可以这样获取 err 值:

1
t,err:=math.Sqrt(x)// err 对应错误值,当其值为 nil 时则没有错误

不过既然是接口,当然可以进行一定程度上的自定义,参考之前我们使用接口的经历,先建立类型并让这个类型实现 Error 方法,那么这个时候该类型就可以直接传给 error 接口了:

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

import (
"fmt"
"time"
)

type MyError struct {
When time.Time
What string
}

func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}

func run() error { //将 MyError 直接返回到 error 接口了
return &MyError{
time.Now(),
" ",
}
}

func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}

当然,还有更加简单粗暴快速的方法——那就是使用 errors.New 函数,它在 “errors” 包内,使用时直接调用就可以创建并返回一个错误值了

1
2
3
4
5
6
func Sqrt(f float64) (float64, error) {
if f < 0 {
return -1, errors.New("math: square root of negative number")
}
return math.Sqrt(f), nil
} //完全不需要将特殊类型的方法与接口对接

Reader

io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。

Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。

io.Reader 接口有一个 Read 方法:

1
func (T) Read(b []byte) (n int, err error)

Read 用数据 填充给定的字节切片 并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。

这里有些要补充的是:给定的字节切片填充时是从头开始覆盖,比如原本一个长为4的切片 [1,2,5,7] 填入 2,4 之后之会将前两个元素覆盖而后续不变:[2,4,5,7]

图像

image 包定义了 Image 接口:

1
2
3
4
5
6
7
package image

type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}

注意: Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明。

(请参阅文档了解全部信息。)

color.Colorcolor.Model 类型也是接口,但是通常因为直接使用预定义的实现 image.RGBAimage.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。

并发

并行与并发

并行:同一个时刻,有多条指令在多个处理器上同时执行

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过cpu时间片轮转使多个进程快速交替的执行。

Go 程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

1
go f(x, y, z)

会启动一个新的 Go 程并执行

1
f(x, y, z)

f, x, yz 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法

信道/通道(channel)

顾名思义,就是个带有信息类型的管道,可以通过它用信道操作符 <- 来发送或者接收值。

1
2
ch <- v    // 将 v 发送至信道 ch。
i := <-ch // 从 ch 接收值并赋予 i,完成了一次信息的传递

“箭头”就是数据流的方向。

和映射与切片一样,信道在使用前必须自己创建:

1
ch := make(chan int) //使用 make 函数

默认情况下,发送接收 操作在 另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。(发送方和接收方都可以是多个)

关于传入及传出的死锁问题:

在 main 函数下声明的 channel 处于 main goroutine 下运行,它想要接收的一定是 其它 goroutine 传入的数据!如果其它 goroutine 都执行完了还没有传入数据,那么就会陷入僵局。这种情况下,等待数据的 main goroutine 只能自杀并且报错给用户。这类错误可以用 select 来防止

信道缓冲

make 函数在声明信道时可以直接将缓冲长度作为第二个参数初始化一个带缓冲的信道。

1
ch := make(chan int, 100)

发送数据可能出现堵塞:

缓冲区为空:接收方堵塞

缓冲区为满:发送方堵塞

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}

还有一点:传入缓冲的数据是 先进先出

range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

1
v, ok := <-ch

之后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

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

import (
"fmt"
)

func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}

关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入

Channel 是可以控制读写权限的 具体如下:

1
2
3
go func(c chan int) { //读写均可的channel c } (a)
go func(c <- chan int) { //只读的Channel } (a)
go func(c chan <- int) { //只写的Channel } (a)
select

是 Go 语言中的一种结构,与 switch 十分相似,每个 case 必须是一个通信操作,要么是发送要么是接收。

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。(为了防止出现死锁的情况,一般会加上 default)

1
2
3
4
5
6
7
8
9
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
// 你可以定义任意数量的 case
default : // 可选
statement(s);
}
  • 每个 case 都必须是一个通信

  • 所有 channel 表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通信可以进行,它就执行,其他被忽略。

  • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
sync.Mutex

我们已经看到信道非常适合在各个 Go 程间进行通信。

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutual exclusion) ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

草,居然花了这么长时间,果然摸鱼的力量无比强大