Skip to content

Go语言趣学指南

======【命令式编程】======

被美化的计算器

  1. Go 并不支持++count 这种见诸 C 和 Java 等语言中的前置增量操作。

  2. rand 包的 Intn()函数,存在“差一错误”(off-by-one error)。

    • rand.Into(10)只会返回一个 0~9 的伪随机数
    • 也就是说要想生成 1 到 10 的数字,需要+1。即:var num = rand.Into(10)+1

循环和分支

  1. 对 Go 来说,true 是唯一的真值,而 false 则是唯一的假值。

  2. Go 只提供了一个相等运算符,并且它不允许直接将文本和数值进行比较。

    • 也就是说,Go 不存在三等号运算符
    • "1"===1 在 Go 中报错,不支持
  3. "apple"和"banana"这两个单词,哪个更大一些?

    • fmt.Println("apple" > "banana") // false
  4. Go 也采用了短路逻辑||满足第一个条件,后面都忽略。

  5. switch 还拥有独特的fallthrough关键字,它可以用于执行下一个分支的代码。

    • 在 C、Java、JavaScript 等语言中,下降是 switch 语句各个分支的默认行为,而 Go 对此采取了更为谨慎的做法,即用户需要显式地使用 fallthrough 关键字才会引发下降
  6. 布尔值是唯一可以用于条件判断的值。

  7. Go 通过 if、switch 和 for 来实现分支判断和重复执行代码。

  • for 示例
go
package main

import (
 "fmt"
 "time"
)

func main() {
 var count = 10
 for count > 0 {
  fmt.Println(count)
  time.Sleep(time.Second)
  count--
 }
}
  • if 示例
go
package main

import (
 "fmt"
 "math/rand"
)

func main() {
 var degrees = 0
 for {
  fmt.Println(degrees)
  degrees++
  if degrees > 360 {
   degrees = 0
   if rand.Intn(2) == 0 {
    break
   }
  }
 }
}
  • switch 示例
go
package main

import (
 "fmt"
)

func main() {
 var word = 'b'
 switch {
 case word == 'a':
  fmt.Println("is a")
 case word == 'b':
  fmt.Println("is b")
 case word == 'c':
  fmt.Println("is c")
 }

 // 简化版
 switch word {
 case 'a':
  fmt.Println("is a")
 case 'b':
  fmt.Println("is b")
 case 'c':
  fmt.Println("is c")
 }
}

变量作用域

  1. Go 的作用域通常会随着大括号{}的出现而开启和结束。

  2. 脱离作用域的变量将不再可见并且无法访问。

  3. 简短声明: var count = 0 等同于 count := 0

    • 简短声明 := 好处:在不支持 var 情况下使用
      • 在 for 循环,循环体变量 i 作用域保留在循环体内
      • 在 if 语句中声明新的变量
      • 和 if 语句一样,简短声明也可以用作 switch 语句的一部分
go
for i := 0; i < 10; i++ {
  fmt.Println(i)
 }
 fmt.Println(i) // undefined: i
go
if num := rand.Intn(3); num == 0 {
  fmt.Println("数字为", num)
 } else if num == 1 {
  fmt.Println("数字为", num)
 } else {
  fmt.Println("数字为", num)
 }
go
switch num := rand.Intn(3); num {
 case 0:
  fmt.Println("数字为", num)
 case 1:
  fmt.Println("数字为", num)
 case 2:
  fmt.Println("数字为", num)
 }

注意 ⚠️

包作用域在声明变量时不允许使用简短声明

go
package main

import (
 "fmt"
 "math/rand"
)

count := 2 // 不支持
// syntax error: non-declaration statement outside function body

func main() {
 fmt.Println(rand.Intn(3))
}
  1. 函数作用域比包作用域的范围狭窄,它始于 func 关键字,并终结于函数声明的右大括号。

  2. switch 的每个 case 都拥有自己独立的作用域,switch 分支的作用域是唯一一种无须使用大括号标识的作用域。

  3. 在 Go 语言中,所有带小数点的数字在默认情况下都会被设置为 float64 类型。

  4. golint工具能够提供代码风格方面的提示。

======【类型】======

实数

即:单精度浮点数、双精度浮点数

  1. Go 语言中,拥有两种浮点类型,默认:双精度浮点数,float64,每个占 8 字节;单精度浮点数,float32,每个占 4 字节(也就是 32 位)。

  2. 在 Go 语言中,每种类型都有相应的默认值,我们将其称为零值(zero value)。

    • float64 和 float32 的零值都为 0.0
  3. 打印浮点类型

格式化宽度精度
%v原样输出原样输出
%f根据实际情况默认保留 6 位小数
%.3f根据实际情况保留 3 位小数
%4.2f4 位,包括小数点保留 2 位小数
go
 PI64 := math.Pi
 fmt.Println(PI64)              // 3.141592653589793
 fmt.Printf("%v\n", PI64)       // 3.141592653589793
 fmt.Printf("%f\n", PI64)       // 3.141593
 fmt.Printf("%.3f\n", PI64)     // 3.142
 fmt.Printf("%4.2f\n", PI64)    // 3.14
  1. 0015.1021 的宽度和精度分别是多少?

    • 宽度为 9,精度为 4,并且使用了零填充,格式化变量为 %09.4f
  2. 浮点数的不精准问题

浮点数也许并不是表示金钱的最佳选择。正确做法是使用整数类型存储美分的数量

go
 total := 1/3 + 1/3 + 1/3
 fmt.Println(1 / 3) // 0
 fmt.Println(total) // 0

 third := 1.0 / 3.0
 fmt.Println(third + third + third) // 1

 piggyBank := 0.1
 piggyBank += 0.2
 fmt.Println(piggyBank) // 0.30000000000000004
  1. 比较浮点数
go
 // 错误示范
 piggyBank := 0.1
 piggyBank += 0.2
 fmt.Println(piggyBank == 0.3) // false

 // 正确示范,根据自己的应用选择一个合适的容差
 fmt.Println(math.Abs(piggyBank-0.3) < 0.0001) // true

整数

  1. 10 种整数类型
  • 常规整型:int8、uint8、int16、uint16、int32、uint32、int64、uint64
  • 特殊整型:int、uint(根据目标硬件选择最合适的位长)

int 不是其他任何类型的别名,int、int32 和 int64 实际上是 3 种不同的类型

  1. 了解类型

Printf 函数提供的格式化变量%T去查看指定变量的类型。

go
year := 2020
fmt.Printf("%T", year) // int
  1. Go 语言中的十六进制数字

要求十六进制数字必须带有 0x 前缀。

  • 使用 Printf 函数打印十六进制数字:使用 %x%X 作为格式化变量
go
var red, green, blue uint = 0, 141, 216
// 十六进制表示
red, green, blue = 0x00, 0x8d, 0xd5
fmt.Printf("color: #%02x%02x%02x;", red, green, blue) // color: #008dd5;
  1. 整数回绕

在 Go 语言中,当超过整数类型的取值范围时,就会出现整数回绕现象。

go
var red uint8 = 255
red++
fmt.Println(red) // 0

var number int8 = 127
number++
fmt.Println(number) // -128
  • 打印二进制位
go
var green uint8 = 3
fmt.Printf("%08b\n", green) // 00000011
green++
fmt.Printf("%08b\n", green) // 00000100

大数

  1. big 包

big 包提供了以下 3 种类型。

  • 存储大整数的 big.Int
  • 存储任意精度浮点数的 big.Float
  • 存储诸如 1/3 的分数的 big.Rat
go
bigNum := big.NewInt(299792)
fmt.Println(bigNum) // 299792

bigNum2 := new(big.Int)
bigNum2.SetString("24000000000000000", 10)
fmt.Println(bigNum2) // 24000000000000000
  1. 常量声明可以带类型,但无法用 uint64 类型存储像 24 艾这样的巨大值
  • uint64 类型溢出
go
const bigNum uint64 = 24000000000000000000
fmt.Println(bigNum)
// cannot use 24000000000000000000 (untyped int constant) as uint64 value in constant declaration (overflows)
  • 正常
go
const bigNum = 24000000000000000000
// 能存储,但不能打印,打印还是会报溢出
fmt.Println(bigNum)
// cannot use bigNum (untyped int constant 24000000000000000000) as int value in argument to fmt.Println (overflows)
  1. Go 语言不会为常量推断类型,而是直接将其标识为无类型(untyped)

  2. 无类型常量可以存储非常大的值,并且所有数值型字面量都是无类型常量

  3. 无类型常量在被用作函数参数的时候,必须转换为有类型变量

多语言文本

  1. Go 语言会把 双引号 包围的字面值推断为 string 类型
go
// 3种作用等同
peace := "peace"
var peace = "peace"
var peace string = "peace"
  1. 使用反引号(`)包围的字符串被称为原始字符串字面量

  2. 原始字符串字面量可以在代码里面跨越多个文本行

  3. 类型别名

  • Go 语言提供了 rune(符文)类型用于表示单个统一码代码点,该类型是 int32 类型的别名。

  • Go 语言还提供了 uint8 类型的别名 byte

  • 从 Go 1.9 开始,用户也可以自行声明类型别名,如:

go
type byte = uint8
type rune = int32
  1. 字符
  • 打印字符:在 Printf 中使用格式化变量 %c

  • 通过使用别名 rune 表明数字代表字符而不是数字

  • 声明一个字符变量且没有指定类型,Go 将推断该变量的类型为 rune

go
// grade := 'A'
// var grade = 'A'
var grade rune = 'A'

fmt.Printf("%v is a %[1]T\n", grade) // 65 is a int32

var star rune = '*'
fmt.Printf("%c %[1]v\n", star) // * 42
var smile rune = '😊'
fmt.Printf("%c %[1]v\n", smile) // 😊 128522

虽然 rune 类型代表的是一个字符,但它实际存储的仍然是数字值

  1. 字符串赋值、读取单个字符
  • 同一个变量,可以赋值不同字符串,但无法对字符串本身进行修改
  • 可以独立访问字符串中单个字符,但是不能修改这些字符
go
peace := "peace"
peace = "hello"
fmt.Println(peace) // hello

char := peace[1]
fmt.Printf("%c\n", char) // e

// 错误示范
peace[1] = 'o'
// cannot assign to peace[1] (value of type byte)
  1. 小写字母转大写字母
go
c := 'z'
c = c - 'a' + 'A'
fmt.Printf("%c\n", c) // Z
  1. Go 跟很多编程语言不同的一点在于,Go 允许函数返回多个值

  2. 将字符串解码为符文

在 utf8 包中提供以下函数:

  • RuneCountInString 函数:能够以符文而不是以字节为单位返回字符串的长度
  • DecodeRuneInString 函数:能够解码字符申的首个字符并返回解码后的符文占用的字节数量。
  1. 关键字 range 不仅可以迭代各种不同的收集器,还可以解码 UTF-8 编码的字符串
go
spanish := "Español"
for i, c := range spanish {
fmt.Printf("key %d,char %c\n", i, c)
/*
key 0,char E
key 1,char s
key 2,char p
key 3,char a
key 4,char ñ
key 6,char o
key 7,char l
*/
}
  • 不需要获取索引,使用空白标识符_(下划线)来省略
go
spanish := "Español"
for _, c := range spanish {
 fmt.Printf("%c ", c) // E s p a ñ o l
}
  1. 字符串使用 UTF-8 可变长度编码,每个字符需要占用 1 ~ 4 字节内存空间

类型转换

  1. 在 Go 语言中,类型不能混合使用
    • 尝试拼接数值和字符串,Go 编译器会报:无效操作
    • 尝试拼接数值和浮点类型,Go 编译器会报:无效操作

Go 不会对你的意图做任何假设,你必须通过显式的类型转换来解决这个问题

go
countdown := "abc" + 10 + "def"
fmt.Println(countdown)
// invalid operation
  1. 数字类型转换
  • 整型转浮点型
go
age := 30
marsAge := float64(age)
fmt.Printf("%v %[1]T\n", marsAge) // 30 float64
  • 浮点型转整型

【注意】浮点数小数点之后的数字将直接被 截断 而不会做任何舍入

go
marsNumber := 30.8
number := int(marsNumber)
fmt.Printf("%v %[1]T\n", number) // 30 int
  1. math 包提供的最小常量和最大常量
go
var num int32 = 16161616
if num < math.MinInt16 || num > math.MaxInt16 {
    // 处理超出范围的值
}
  1. 字符串转换:strconv.Itoa 函数和 fmt.Sprintf 函数
  • 使用 strconv 包的 Itoa 函数转换
go
countdown := 10
str := strconv.Itoa(countdown) + "秒后,系统关机!"
fmt.Println(str) // 10秒后,系统关机!
  • 使用 fmt 包的 Sprintf 函数转换
go
countdown := 10
str := fmt.Sprintf("%v秒后,系统关机!", countdown)
fmt.Println(str) // 10秒后,系统关机!
  1. 静态类型

变量一旦被声明,它就有了类型并且无法改变它的类型。这种机制被称为 静态类型

  1. 转换布尔值
  • 通过 Sprintf 函数将布尔值变量转换成文本
go
launch := false
launchText := fmt.Sprintf("%v", launch)
fmt.Println(launchText) // false
  • 将字符串转换为布尔值
go
yesNo := "no"
launch := (yesNo == "yes")
fmt.Println(launch) // false

注意

在 Go 语言中,没有数字 0 和空字符串""来表示 false,没有数字 1 和非空字符串表示 true

因此,下述都是非法的:
string(false)、int(false)、bool(1)、bool("yes"),Go 编译器都会报告错误

======【构建块】======

函数

  1. 函数声明

在 Go 中,以 大写字母开头 的函数、变量以及其他标识符都会被导出并对其他包可用,反之则不然

  • 如果多个形参拥有相同的类型,那么我们只需要把这个类型写出来一次即可:
go
func Unix(sec int64, nsec int64) Time
func Unix(sec, nsec int64) Time
  • 多个返回值
go
// 多个返回值,需要括号包围起来
func Atoi(s string) (i int, err error)
// 返回值去掉名字,只保留类型
func Atoi(s string) (int, error)
  1. 函数声明中的省略号...代表什么意思?

函数声明中带有省略号...意味着该函数是一个可变参数函数,它可以接受任意多个实参

方法

  1. 通过方法为类型添加行为

每个方法和函数都可以接受多个形参,但一个方法必须并且只能有一个接收者

跟调用其他包中的函数一样,调用方法也需要用到 点记号

一等函数

  1. 匿名函数也就是没有名字的函数,再 Go 中也被称为函数字面量。
    • 因为函数字面量需要保留外部作用域的变量引用,所以函数字面量都是闭包的
go
package main

import "fmt"

var f = func() {
 fmt.Println("Hello World")
}

func main() {
 f()
}
  • 闭包保留的事周围变量的引用而不是副本值,所以修改被闭包捕获的变量可能会导致调用匿名函数的结果发生变化
go
package main

import "fmt"

var k = 294.0

func main() {
 sensor := func() float64 {
  return k
 }

 fmt.Println(sensor()) // 294
 k++
 fmt.Println(sensor()) // 295
}
  1. 闭包提供了哪些普通函数不具备的特性?

闭包能够保留外部作用域的变量引用

======【收集器】======

劳苦功高的数组

  1. 使用复合字面量初始化数组

Go 语言的复合字面量语法允许我们在单个步骤里面完成声明数组和初始化数组这两项工作

go
array := [5]string{"a", "b", "c", "d", "e"}
fmt.Println(array) // [a b c d e]
  • 使用省略号...让 Go 编译器计算数组元素的数量
go
array := [...]string{
 "a",
 "b",
 "c",
 "d",
 "e",
}
fmt.Println(array)      // [a b c d e]
fmt.Println(len(array)) // 5
  1. 迭代数组
  • 根据下标访问
go
array := [...]string{"a", "b", "c"}
for i := 0; i < len(array); i++ {
 fmt.Println(array[i])
}
  • 使用关键字 range 迭代数组
go
array := [...]string{"a", "b", "c"}
for i, item := range array {
 fmt.Println(i, item)
}
  1. 数组被复制

无论是将数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本

go
array := [...]string{"a", "b", "c"}
newArray := array
array[1] = "o"
fmt.Println(array)    // [a o c]
fmt.Println(newArray) // [a b c]
  1. 尝试传递长度不相符的数组作为参数将导致 Go 编译器报错

切片:指向数组的窗口

切片的定义

每个切片在内部都会被表示为一个包含 3 个元素的结构,这 3 个元素分别是指向数组的指针、切片的容量以及切片的长度。

  1. 切分数组
  • 通过切分数组创建切片需要用到 半开区间 (包括左边开始索引,不包括结束索引)

  • 除可以创建数组的切片之外,还可以创建切片的切片

go
array := [...]string{"a", "b", "c", "d", "e", "f", "g"}
aArray := array[2:5]
bArray := aArray[0:2]
cArray := aArray[1:2]
fmt.Println(aArray, bArray, cArray) // [c d e] [c d] [d]
  • 切片的默认索引
go
array := [...]string{"a", "b", "c", "d", "e", "f", "g"}
aArray := array[2:5]
bArray := aArray[:2]                // 省略开始索引,则使用起始位置
cArray := aArray[2:]                // 省略结束索引,则使用数组长度
fmt.Println(aArray, bArray, cArray) // [c d e] [c d] [d]
  • 同时省略起始和结束索引
go
dArray := aArray[:]
fmt.Println(dArray) // [c d e]
  • 切片的索引不能是负数
  1. 切分字符串
  • 切分字符串将创建另一个字符串
  • 切分字符串时,索引代表的是字节号码而非符文号码
go
name := "hello world"
subName := name[5:]
name = "my time"
fmt.Println(name, subName) // my time  world
  1. Go 语言的许多函数都倾向于使用切片而不是数组作为输入

    • Go 语言的使用者很少会直接使用数组,它们更愿意使用更为通用的切片,特别是在向函数传递实参的时候。
  2. 声明一个字符串切片,只需要使用 []string 作为类型即可

go
dwarfs := []string{"a", "b", "c", "d", "e"}
fmt.Println(dwarfs)        // [a b c d e]
fmt.Printf("%T\n", dwarfs) // []string
  1. 在 Go 语言中,当你将切片(slice)传递给函数时,实际上是在进行 “传址” 操作。这是因为切片在 Go 中是一个 引用类型 ,它包含指向底层数组的指针、长度和容量信息。
    • 切片是指向数组的窗口和视图
    • 切片在赋值或者传递至函数时,将与新变量共享相同的底层数据
go
package main

import (
 "fmt"
 "strings"
)

func handleSlice(data []string) {
 for i, v := range data {
  data[i] = strings.TrimSpace(v)
 }
}

func main() {
 dwarfs := []string{"a ", " b", " c "}
 handleSlice(dwarfs)
 fmt.Println(strings.Join(dwarfs, "")) // abc
}

更大的切片

  1. append 函数
  • 和 Println 一样,append 也是一个可变参数函数,因此我们可以一次向切片追加多个元素
go
dwarfs := []string{"a", "b", "c"}
dwarfs = append(dwarfs, "d", "e")
fmt.Println(dwarfs) // [a b c d e]
  • 三索引切片操作
go
planets := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
terrestrial := planets[0:4:4]
fmt.Println(len(terrestrial), cap(terrestrial)) // 4 4
worlds := append(terrestrial, "k")
fmt.Println(worlds)  // [a b c d k]
fmt.Println(planets) // [a b c d e f g h i j]
  • 不使用三索引切片操作
go
planets := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
terrestrial := planets[0:4]
fmt.Println(len(terrestrial), cap(terrestrial)) // 4 10
worlds := append(terrestrial, "k")
fmt.Println(worlds)  // [a b c d k]
fmt.Println(planets) // [a b c d k f g h i j]
  1. 使用 make函数 对切片实行预分配

当切片的容量不足以执行 append 操作时,Go 必须创建新数组并复制旧数组中的内容。

go
dwarfs := make([]string, 0, 10)
fmt.Println(len(dwarfs), cap(dwarfs)) // 0 10
dwarfs = append(dwarfs, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
fmt.Println(dwarfs)                   // [a b c d e f g h i j]
fmt.Println(len(dwarfs), cap(dwarfs)) // 10 10
dwarfs = append(dwarfs, "k")
fmt.Println(dwarfs)                   // [a b c d e f g h i j k]
fmt.Println(len(dwarfs), cap(dwarfs)) // 11 20
  1. 声明可变参数函数
go
package main

import "fmt"

func handleData(prefix string, worlds ...string) []string {
 newWorlds := make([]string, len(worlds))
 for i := range worlds {
  newWorlds[i] = prefix + " " + worlds[i]
 }
 return newWorlds
}

func main() {
 twoWorlds := handleData("new", "abc", "xyz")
 fmt.Println(twoWorlds) // [new abc new xyz]
}
  • 通过省略号可以 展开 切片中的多个元素,并将它们用作传递给函数的多个实参
go
planets := []string{"a", "b", "c"}
twoWorlds := handleData("new", planets...)
fmt.Println(twoWorlds) // [new a new b new c]

无所不能的映射

  1. 声明映射

映射的键几乎可以是任何类型。

在使用 Go 语言的映射时,我们必须为映射的键和值指定类型

举例: map[string]int

其中:string 为键的类型,int 为值的类型

go
temperature := map[string]int{
 "Earth": 15,
 "Mars":  -65,
}
temp := temperature["Earth"]
fmt.Println(temp) // 15
  • 如果程序访问的键并不存在于映射中,那么 Go 语言将根据值的类型返回相应的零值作为结果
go
fmt.Println(temperature["other"]) // 0
  • Go 语言提供了“逗号与 ok”语法

区分键存在于映射中,以及本身值为 0(此处拿 int 类型举例)

go
if moon, ok := temperature["moon"]; ok {
 fmt.Println(moon)
} else {
 fmt.Println("No moon") // No moon
}

第二个变量,不局限于 ok,可以自由地任意名字命名,如:moon, found

  1. 映射共享相同的底层数据,修改任意一个都将导致另一个发生变化
go
temperature := map[string]int{
 "Earth": 15,
 "Mars":  -65,
}

twoTemperature := temperature

temperature["Earth"] = 30
fmt.Println(temperature)    // map[Earth:30 Mars:-65]
fmt.Println(twoTemperature) // map[Earth:30 Mars:-65]

delete(twoTemperature, "Earth")
fmt.Println(twoTemperature) // map[Mars:-65]
fmt.Println(temperature)    // map[Mars:-65]
  1. 使用 make 函数对映射实行预分配
go
temperature := make(map[string]int, 8)
fmt.Println(temperature, len(temperature)) // map[] 0
  1. range 在每次迭代映射时提供的将不再是索引和值,而是键和值

  2. Go 在迭代映射时并不保证键的顺序,因此,同样的映射在进行多次迭代时可能会产生不同的输出

  3. 使用映射和切片实现数据分组

  • 以每 10℃ 为一组对温度实行分组
go
temperatures := []float64{
 -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

groups := make(map[float64][]float64)

for _, t := range temperatures {
 g := math.Trunc(t/10) * 10
 groups[g] = append(groups[g], t)
}

for g, temperatures := range groups {
 fmt.Printf("%v : %v\n", g, temperatures)
}
/*
 30 : [32]
 -30 : [-31 -33]
 -20 : [-28 -29 -23 -29 -28]
*/
  1. 将映射用作集合

集合保证其中的每个元素只会出现一次。

Go 语言没有直接提供集合收集器

  • 通过映射改良成集合
go
temperatures := []float64{
 -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

set := make(map[float64]bool)

for _, t := range temperatures {
 set[t] = true
}

fmt.Println(set) // map[-33:true -31:true -29:true -28:true -23:true 32:true]

if set[-28.0] {
 fmt.Println("存在-28.0") // 输出:存在-28.0
}
  • 对集合转成切片排序
go
unique := make([]float64, 0, len(set))
for t := range set {
 unique = append(unique, t)
}

sort.Float64s(unique)
fmt.Println(unique) // [-33 -31 -29 -28 -23 32]

======【状态与行为】======

结构

  1. 声明结构

在结构中,访问字段的值或者为字段赋值都需要用到 点标记法

go
package main

import "fmt"

var curiosity struct {
 lat float64
 lon float64
}

func main() {
 curiosity.lat = -4.5895
 curiosity.lon = 137.4417

 fmt.Println(curiosity.lon, curiosity.lat) // 137.4417 -4.5895
 fmt.Println(curiosity)                    // {-4.5895 137.4417}
}
  1. 通过类型复用结构
go
package main

import "fmt"

type curiosity struct {
 lat float64
 lon float64
}

func main() {
 var spirit curiosity
 spirit.lat = -14.5684
 spirit.lon = 175.472636

 var spirit2 curiosity
 spirit2.lat = -1.9462
 spirit2.lon = 354.4734

 fmt.Println(spirit, spirit2) // {-14.5684 175.472636} {-1.9462 354.4734}
}
  1. 通过复合字面量初始化结构

初始化可以按任何顺序给定字段赋值,没有赋值的会被初始化为类型对应的零值

go
package main

import "fmt"

type location struct {
 lat, lon float64
}

func main() {
 myLocation := location{lon: 354.4734, lat: -1.9462}
 fmt.Println(myLocation) // {-1.9462 354.4734}
}
  1. 只使用值进行初始化的复合字面量
go
package main

import "fmt"

type location struct {
 lat, lon float64
}

func main() {
 myLocation := location{-1.9462, 354.4734}
 fmt.Println(myLocation.lat) // -1.9462
 fmt.Println(myLocation.lon) // 354.4734
}
  1. 打印出结构的字段
go
package main

import "fmt"

type location struct {
 lat, lon float64
}

func main() {
 myLocation := location{-1.9462, 354.4734}
 fmt.Printf("%v\n", myLocation)  // {-1.9462 354.4734}
 fmt.Printf("%+v\n", myLocation) // {lat:-1.9462 lon:354.4734}
}
  1. 结构被复制

两个结构发生的变化不会对对方产生任何影响

go
myLocation := location{-1.9462, 354.4734}
otherLocation := myLocation

otherLocation.lon += 0.0106

fmt.Println(myLocation)    // {-1.9462 354.4734}
fmt.Println(otherLocation) // {-1.9462 354.48400000000004}
  1. 由结构组成的切片

[]struct 用于表示由结构组成的切片,它的独特之处在于,切片包含的每个值都是一个结构而不是像 float64 这样的基本类型

go
package main

import "fmt"

type location struct {
 name string
 lat  float64
 lon  float64
}

func main() {
 locations := []location{
  {name: "abc", lat: 1, lon: 1},
  {name: "edf", lat: 2, lon: 2},
  {name: "ghi", lat: 3, lon: 3},
 }
 fmt.Println(locations) // [{abc 1 1} {edf 2 2} {ghi 3 3}]
}
  1. 将结构编码为 json
  • 来自 json 包的 Marshal 函数将指定数据编码为 JSON 格式,并以字节形式返回编码后的 JSON 数据

  • 结构中字段必须以大写字母开头(Marshal 函数只会对结构中被导出的字段实施编码)

换句话说,如果字段都是以小写字母开头,那么编码的结果将会是{}

go
package main

import (
 "encoding/json"
 "fmt"
 "os"
)

func main() {
 type location struct {
  Lat, Lon float64
 }

 data := location{1, 1}
 bytes, err := json.Marshal(data)
 exitOnError(err)
 fmt.Println(string(bytes)) // {"Lat":1,"Lon":1}
}

func exitOnError(err error) {
 if err != nil {
  fmt.Println(err)
  os.Exit(1)
 }
}
  1. 使用结构标签定制 JSON

Go 要求结构中的字段必须是大写字母开头,并且多个单词字段名称必须使用类似 CaselCase 这样的驼峰形命名惯例

为了按照我们的意愿修改字段名称,可以给结构中的字段打标签(tag)

go
type location struct {
  Lat float64 `json:"lat"`
  Lon float64 `json:"lon"`
 }

 data := location{1, 1}
 bytes, err := json.Marshal(data)
 exitOnError(err)
 fmt.Println(string(bytes)) // {"lat":1,"lon":1}

上面之所以使用``而不是使用"",目的是省下使用反斜杠转移""的功夫而已。
双引号写法:"json:\"lat\""

Go 没有类

Go 语言跟传统编程语言不一样,它既不支持类和对象,也不支持继承

不过,Go 语言提供了结构和方法,通过组合两者就可以实现面向对象设计的相关概念

  1. 构造函数
  • Go 语言并没有为构造器提供特殊的语言特性,而是选择了名称格式为 newType 或者 NewType 的函数用于构造指定类型的值,至于函数名首字母的大小写则由函数是否需要导出以供其他包使用来决定。

  • 一些对外提供使用的包,将构造函数命名为 New 函数,更简洁。例如:errors 包就是这样,errors.New()

  • 在 Go 语言中,构造函数和其他函数一样只是普通的函数。

组合与转发

  1. 合并结构
go
package main

import "fmt"

type report struct {
 sol         int
 temperature temperature
 location    location
}

type temperature struct {
 high, low celsius
}

type location struct {
 lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
 return (t.high + t.low) / 2.0
}

// 方法转发(report转发至temperature方法)
func (r report) average() celsius {
 return r.temperature.average()
}

func main() {
 bradbury := location{-4.5895, 137.4417}
 t := temperature{high: -1.0, low: -78.0}
 report := report{sol: 15, temperature: t, location: bradbury}

 fmt.Printf("%+v\n", report)
 // {sol:15 temperature:{high:-1 low:-78} location:{lat:-4.5895 long:137.4417}}
 fmt.Printf("%v\n", report.temperature.high) // -1

 fmt.Printf("average: %v\n", t.average())                  // average: -39.5
 fmt.Printf("average: %v\n", report.temperature.average()) // average: -39.5

 fmt.Printf("average: %v\n", report.average()) // average: -39.5
}
  1. 实现自动的转发方法
go
package main

import "fmt"

type report struct {
 sol int
 temperature
 location
}

type temperature struct {
 high, low celsius
}

type location struct {
 lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
 return (t.high + t.low) / 2.0
}

func (r report) average() celsius {
 return r.temperature.average()
}

func main() {
 report := report{
  sol:         15,
  temperature: temperature{high: -1.0, low: -78.0},
  location:    location{-4.5895, 137.4417},
 }

 fmt.Printf("average: %v\n", report.average())             // average: -39.5℃
 fmt.Printf("average: %v\n", report.temperature.average()) // average: -39.5℃
 fmt.Printf("%v\n", report.high)                           // -1℃
 report.high = 32
 fmt.Printf("%v\n", report.temperature.high) // 32℃
}
  1. 命名冲突
go
package main

import "fmt"

type sol int

type report struct {
 sol
 temperature
 location
}

type temperature struct {
 high, low float64
}

type location struct {
 lat, long float64
}

func (s sol) days(s2 sol) int {
 days := int(s2 - s)
 if days < 0 {
  days = -days
 }
 return days
}

func (l location) days(l2 location) int {
 return 5
}

/*
注释此方法,report.days(1446)会报“不明确的引用 'days'”
report类型的同名方法优先级高于嵌入类型的其他同名方法。
*/
func (r report) days(s2 sol) int {
 return r.sol.days(s2)
}


func main() {
 report := report{sol: 15}

 fmt.Println(report.sol.days(1446)) // 1431
 fmt.Println(report.days(1446))     // 1431
}
  1. Go 并没有提供委托或者继承

接口

  • 接口类型通过一组方法来指定所需的行为
  • 任何包中的新代码或者已有代码都可以隐式地满足接口
  • 结构可以通过嵌入满足接口的类型来满足接口
  1. 接口类型
go
package main

import (
 "fmt"
 "strings"
)

var t interface {
 talk() string
}

type martian struct {
}

func (m martian) talk() string {
 return "martian"
}

type laser int

func (l laser) talk() string {
 return strings.Repeat("pew ", int(l))
}

func main() {
 t = martian{}
 fmt.Println(t.talk()) // martian

 t = laser(3)
 fmt.Println(t.talk()) // pew pew pew
}

接口通过 多态 让变量 t 具备了“多种形态”

  1. 为了便于复用,我们通常会把接口声明为类型并为其命名。按照惯例,接口类型的名称常常会以 -er 作为后缀
go
type talker interface {
 talk() string
}
  1. Go 语言允许在实现代码的过程中随时创建新的接口。任何代码都可以实现接口,包括那些已经存在的代码

  2. Go 语言的接口都是隐式满足的

  • 隐式满足接口有什么好处?

你声明的接口可以由其他人编写的代码来满足,这种做法能够让代码变得更加灵活。

  1. 满足接口

Go 标准库导出了很多只有单个方法的接口,人们可以在自己的代码中实现它们。

======【深入 GO 语言】======

关于指针的二三事

  1. 指针

指针是指向另一变量的地址的变量

指针是间接访问的一种形式

Go 的指针采用了历史悠久并且广为人知的 C 语言指针语法

  • 变量会将它们的值存储在计算机的随机访问存储器里面,而值的存储位置则是该变量的内存地址。

地址操作符(&):通过使用&表示的地址操作符,我们可以得到指定变量的内存地址

go
answer := 30
fmt.Println(&answer) // 0x1400011a018

解引用操作符(*):提供内存地址指向的值

go
address := &answer
fmt.Println(*address) // 30

注意

C 语言中的内存地址可以通过诸如 address++这样的指针运算进行操作,但 Go 语言不允许这种不安全的操作

  1. 指针类型

*int 中的星号表示这是一种指针类型。

go
canada := "canada"

var home *string
fmt.Printf("%T\n", home) // *string

home = &canada
fmt.Printf(*home) // canada
  1. 指针的作用就是指向

把解引用的结果赋值给另一个变量将产生一个字符串副本。

  1. 指向结构的指针
go
package main

import "fmt"

type person struct {
 name, superpower string
 age              int
}

func main() {
 timmy := &person{
  name: "Timmy",
  age:  10,
 }
 fmt.Println(timmy) // &{Timmy  10}
 fmt.Println((*timmy).name) // Timmy
}

在访问字段时对结构进行解引用并不是必需的

go
timmy.superpower = "flying"
fmt.Printf("%+v\n", timmy) // &{name:Timmy superpower:flying age:10}

语法 timmy.superpower 和 (*timmy).superpower 有何区别?

因为 Go 会为字段自动实施指针解引用,所以上述两个语句在功能上没有任何区别,不过由于 timmy.superpower 更易读,因此它更可取一些。

  1. 指向数组的指针

Go 也为数组提供了自动的解引用特性。

go
timmy := &[3]string{"Timmy1", "Timmy2", "Timmy3"}
fmt.Println(timmy[0])   // Timmy1
fmt.Println(timmy[1:2]) // [Timmy2]
  1. 切片和映射的复合字面量前面也可以放置地址操作符(&),但 Go 语言并没有为它们提供自动的解引用特性

  2. 将指针用作形参

go
package main

import "fmt"

type person struct {
 name, superpower string
 age              int
}

func birthday(p *person) {
 p.age++
}

func main() {
 timmy := person{
  name:       "Timmy",
  superpower: "image",
  age:        18,
 }
 birthday(&timmy)
 fmt.Println(timmy) // {Timmy image 19}
}
  1. 指针接收者
  • 使用指针执行方法调用
go
package main

import "fmt"

type person struct {
 name string
 age  int
}

func (p *person) birthday() {
 p.age++
}

func main() {
 timmy := &person{
  name: "Timmy",
  age:  18,
 }
 timmy.birthday()
 fmt.Printf("%+v\n", timmy) // &{name:Timmy age:19}
}
  • 无须指针执行方法调用
go
package main

import "fmt"

type person struct {
 name string
 age  int
}

func (p *person) birthday() {
 p.age++
}

func main() {
 timmy := person{
  name: "Timmy",
  age:  18,
 }
 timmy.birthday()
 fmt.Printf("%+v\n", timmy) // {name:Timmy age:19}
}

无论调用方法的变量是否为指针,birthday 方法都必须使用 指针 作为 接收者,否则 age 字段将无法实现自增

  1. 内部指针

内部指针即是指向结构内部字段的指针。这一点可以通过在结构字段的前面放置地址操作符来完成,如&player.stats

  • levelUp 函数对 stats 结构进行修改,所以它需要将形参设置为指针类型
go
package main

import "fmt"

type stats struct {
 level             int
 endurance, health int
}

func levelUp(s *stats) {
 s.level++
 s.endurance = 18 + (14 * s.level)
 s.health = 5 * s.endurance
}

func main() {
 myStats := stats{
  level:     1,
  endurance: 18,
  health:    5,
 }
 levelUp(&myStats)
 fmt.Println(myStats) // {2 46 230}
}
  • Go 语言的地址操作符不仅可以获取结构的内存地址,还可以获取结构中指定字段的内存地址
go
package main

import "fmt"

type stats struct {
 level             int
 endurance, health int
}

func levelUp(s *stats) {
 s.level++
 s.endurance = 18 + (14 * s.level)
 s.health = 5 * s.endurance
}

type character struct {
 name  string
 stats stats
}

func main() {
 player := character{name: "san"}
 levelUp(&player.stats)
 fmt.Printf("%+v\n", player.stats) // {level:1 endurance:32 health:160}
}
  1. 修改数组

虽然我们更倾向于使用切片而不是数组,但数组也适用于一些不需要修改长度的场景

go
package main

import "fmt"

func reset(board *[8][8]rune) {
 board[0][0] = 'r'
}

func main() {
 var board [8][8]rune
 reset(&board)

 fmt.Printf("%c\n", board[0][0]) // r
}

总结

函数或者方法通过指针可以对传入的数组进行修改,这一点在不使用指针的情况下是无法做到的

  1. 隐式指针
  • 映射是隐式指针
go
func demolish(planet *map[string]string) // 多余的指针
  • 切片指向数组

切片在指向数组元素的时候使用了 指针

  • 修改切片
go
package main

import "fmt"

func update(planets *[]string) {
 *planets = (*planets)[0:5]
}

func main() {
 planets := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
 update(&planets)

 fmt.Println(planets) // [a b c d e]
}
  1. 指针和接口
go
package main

import (
 "fmt"
 "strings"
)

type talker interface {
 talker() string
}

func shout(t talker) {
 louder := strings.ToUpper(t.talker())
 fmt.Println(louder)
}

type martian struct {
}

func (m martian) talker() string {
 return "nick nick"
}

func main() {
 shout(martian{})  // NICK NICK
 shout(&martian{}) // NICK NICK
}
  1. 明智地使用指针

指针虽然有用,但是也会增加额外的复杂性。毕竟如果值可能会在多个地方发生变化,那么追踪代码就会变得更为困难。

关于 nil 的纷纷扰扰

在 Go 语言中,nil 是一个 零值

如果一个指针没有明确的指向,那么它的值就是 nil。除了指针,nil标识符还是 切片映射接口零值

  1. 保护你的方法

因为值为 nil 的接收者和值为 nil 的参数在行为上并无区别,所以 Go 语言即使在接收者的值为 nil 的情况下也会继续调用方法

go
package main

import "fmt"

type person struct {
 age int
}

func (p *person) birthday() {
 if p == nil {
  return
 }
 p.age++
}

func main() {
 var nobody *person
 fmt.Println(nobody) // <nil>

 nobody.birthday()
}
  1. nil 函数值

当变量被声明为函数类型时,它的默认值为 nil

go
package main

import "fmt"

func main() {
 var fn func(a, b int) int
 fmt.Println(fn == nil) // true
}
  1. nil 切片

如果切片在声明之后没有使用复合字面量或者内置的 make 函数进行初始化,那么它的值将为 nil。

go
package main

import "fmt"

func main() {
 var soup []string
 fmt.Println(soup == nil) // true

 for _, item := range soup {
  fmt.Println(item)
 }

 fmt.Println(len(soup)) // 0

 soup = append(soup, "soup1", "soup2", "soup3")
 fmt.Println(soup) // [soup1 soup2 soup3]
}
  • 从 nil 开始
go
package main

import "fmt"

func mirepoix(reuslt []string) []string {
 return append(reuslt, "one", "two", "three", "four", "five")
}

func main() {
 soup := mirepoix(nil)
 fmt.Println(soup) // [one two three four five]
}
  1. nil 映射

跟切片的情况一样,如果映射在声明之后没有使用复合字面量或者内置的 make 函数进行初始化,那么它的值将会是默认的 nil

nil 映射可以执行读取操作但是不能执行写入操作

go
package main

import "fmt"

func main() {
 var soup map[string]int
 fmt.Println(soup == nil) // true

 measurement, ok := soup["measurement"]
 if ok {
  fmt.Println(measurement)
 }

 for key, item := range soup {
  fmt.Println(key, item)
 }
}
  1. nil 接口

声明为接口类型的变量在未被赋值时的零值为 nil

go
var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil) // <nil> <nil> true
  • Go 认定接口类型的变量只有在类型和值都为 nil 时才等于 nil
go
var p *int
v := p
fmt.Printf("%T %v %v\n", v, v, v == nil) // *int <nil> true
  • 检视接口变量的内部表示

格式化变量 %#v 能够同时查看变量的类型和值

go
var p *int
v := p
fmt.Printf("%#v\n", v) // (*int)(nil)

孰能无过

  1. 处理错误
go
package main

import (
 "fmt"
 "io/ioutil"
 "os"
)

func main() {
 files, err := ioutil.ReadDir("./") // 'ReadDir' is deprecated
 if err != nil {
  fmt.Println(err)
  os.Exit(1)
 }

 for _, file := range files {
  fmt.Println(file.Name())
  // .idea
  // main.go
 }
}
  1. 关键字 defer

如果一个函数在内部使用 defer 延迟了某些操作,那么 Go 语言将保证这些被延迟的操作会在函数返回之前触发

  • defer 可以延迟任何函数或者方法
go
package main

import (
 "fmt"
 "os"
)

func proverbs(name string) error {
 f, err := os.Create(name)
 if err != nil {
  return err
 }
 defer f.Close()

 _, err = fmt.Fprintln(f, "Errors are values")
 if err != nil {
  return err
 }

 _, err = fmt.Fprintln(f, "Done.")
 return err
}

func main() {
 proverbs("./file.log")
}
  1. 创造性的错误处理
go
package main

import (
 "fmt"
 "io"
 "os"
)

type safeWriter struct {
 w   io.Writer
 err error
}

func (sw *safeWriter) writeln(s string) {
 if sw.err != nil {
  return
 }
 _, sw.err = fmt.Fprintln(sw.w, s)
}

func proverbs(name string) error {
 f, err := os.Create(name)
 if err != nil {
  return err
 }
 defer f.Close()
 sw := safeWriter{w: f}
 sw.writeln("abcd")
 sw.writeln("efgh")
 sw.writeln("ijklmn")
 sw.writeln("ijklmnop")
 return sw.err
}

func main() {
 proverbs("./file.log")
}
  1. 新的错误

errors 包包含一个构造函数,它接受一个代表错误信息的字符串作为参数

go
error.New("out of bounds")
  1. 按需返回错误

许多 Go 包都声明并导出了一些变量,用于表示它们可能会返回的错误

  • 根据惯例,Go 程序将使用带有 Err 前缀的变量来存储错误消息
go
var (
 ErrBounds = errors.New("out of bounds")
 ErrDigit  = errors.New("invalid digit")
)

if !inBounds(row, column) {
 return ErrBounds
}

errors.New 构造函数返回的是指针

  1. 自定义错误类型

与其每次只返回一个错误,更好的做法是,创建自定义类似 Set 方法,让 Set 方法对参数执行多种检查,并一次性返回所有错误

一般定制错误类型,都会使用单词Error作为后缀。但有时候为了简洁,也只使用单词Error,如:url包url.Error一样

  • 定制错误类型可以通过满足 error 接口来实现
  1. 类型断言
  • 类型断言的作用?

它会尝试将值 err 从 error 接口类型转换为具体的 SudokuError 类型

  • 类型断言可以将值从接口类型转换为具体类型或者其他接口类型
  1. Go 语言虽然没有提供异常机制,但它有一种名为 panic 的类似机制

如果一个函数抛出了异常但是却没有人来捕捉它,那么异常将向上传递至该函数的调用者,然后是调用者的调用者,以此类推,直到到达诸如 main 函数之类的调用栈顶部为止。

  1. 如何引发惊恐

传递给 panic 函数的实参可以是任何类型

go
panic("I forgot my towel")

注意

虽然返回错误值通常比使用 panic 更可取,但由于 panic 会在退出程序之前执行所有被延迟的操作,而 os.Exit 不会这么做,因此 panic 比 os.Exit 好一些

  1. 处理惊恐

通过使用关键字 defer,程序可以在函数返回之前执行指定的清理操作

被延迟的操作将在函数返回之前执行,即使在发生惊恐的情况下也是如此。如果某个被延迟的函数调用了 recover,那么惊恐将会停止,而程序则会继续运行。这种恢复机制类似于其他语言中的 catch、except 和 rescue。

go
package main

import "fmt"

func main() {
 defer func() {
  if e := recover(); e != nil {
   fmt.Println(e)
  }
 }()

 panic("I forgot my towel") // I forgot my towel
}

======【并发编程】======

在 Go 语言中,你可以通过 goroutine 并发执行任何代码,并使用 通道(channel) 实现多个 goroutine 之间的通信和协同,从而使得多个并发任务能够直截了当地朝着同一目标前进。

goroutine 和并发

在 Go 中,独立运行的任务被称为goroutine

  • 使用 go 语句可以启动一个新的goroutine,并且这个goroutine将以并发方式运行
  1. 启动 goroutine
go
package main

import (
 "fmt"
 "time"
)

func main() {
 go sleepGopher()
 time.Sleep(4 * time.Second)
}

func sleepGopher() {
 time.Sleep(3 * time.Second)
 fmt.Println("...snore...")
}
  1. 不止一个 goroutine

每次使用关键字 go 都会产生一个新的 goroutine

go
package main

import (
 "fmt"
 "time"
)

func main() {
 for i := 0; i < 5; i++ {
  go sleepGopher(i)
 }
 // 每次执行,不同的顺序输出
 // ... 2  snore...
 // ... 1  snore...
 // ... 4  snore...
 // ... 0  snore...
 // ... 3  snore...
 time.Sleep(4 * time.Second)
}

func sleepGopher(id int) {
 time.Sleep(3 * time.Second)
 fmt.Println("...", id, " snore...")
}
  1. 通道

通道(channel)可以在多个 goroutine 之间安全地传递值。

你只需把对象放到管道里面,它就会飞快地出现在管道的另一端,然后其他人就可以取走这个对象了

  • 创建通道

跟创建映射或切片时的情况一样,创建通道需要用到内置的 make 函数,并且你还需要再创建时为其指定相应的类型

go
c := make(chan int)
  • 通过通道接收值

通过 左箭头操作符(<-) 向它发送值或者从它那里接收值了

go
package main

import (
 "fmt"
 "time"
)

func main() {
 c := make(chan int)
 for i := 0; i < 5; i++ {
  go sleepGopher(i, c)
 }
 for i := 0; i < 5; i++ {
  gopherID := <-c
  fmt.Printf("Gopher ID: %d\n", gopherID)
 }
}

func sleepGopher(id int, c chan int) {
 time.Sleep(3 * time.Second)
 fmt.Println("...", id, " snore...")
 c <- id
}
  1. 使用 select 处理多个通道

我们使用了单个通道来等待多个 goroutine。这种做法在所有 goroutine 都产生相同类型的值时相当好用,但情况并不总是如此。在实际中,程序通常需要等待两种或者多种不同类型的值。

Go 标准库提供了一个非常棒的函数 time.After 来帮助我们实现这一目的。这个函数会返回一个通道,该通道会在经过特定时间之后接收到一个值(发送该值的 goroutine 是 Go 运行时的其中一部分)。

go
package main

import (
 "fmt"
 "math/rand"
 "time"
)

func sleepGopher(id int, c chan int) {
 time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)
 fmt.Println("...", id, " snore...")
 c <- id
}

func main() {
 c := make(chan int)
 for i := 0; i < 5; i++ {
  go sleepGopher(i, c)
 }

 timeout := time.After(2 * time.Second)
 for i := 0; i < 5; i++ {
  select {
  case gopherID := <-c:
   fmt.Println(gopherID, " has finished sleeping")
  case <-timeout:
   fmt.Println("my patience has ran out")
   return
  }
 }
}

提示

select 语句在不包含任何分支的情况下将永远地等待下去。当你启动多个 goroutine 并且打算让它们无限期地运行下去的时候,就可以用这个方法来阻止main 函数返回。

注意

即使程序已经停止等待 goroutine,但只要 main 函数还没返回,仍在运行的 goroutine 就会继续占用内存。所以在情况允许的情况下,我们还是应该尽量结束无用的 goroutine

  1. 跟映射、切片和指针一样,通道的值也可以是 nill,而这个值实际上也是它们默认的零值

对值为 nil 的通道执行发送或接收操作并不会引发惊恐,但是会导致操作永久阻塞

初看上去,值为 nil 的通道似乎没什么用处,但事实恰恰相反。例如,对于一个包含 select 语句的循环,如果我们不希望程序在每次循环的时候都等待 select 语句涉及的所有通道,那么可以先将某些通道设置为 nil,等到待发送的值准备就绪之后,再为通道变量赋予一个非 nil 值并执行实际的发送操作。

速查

  1. time.After 返回的是什么类型的值?
    通道

  2. 对值为 nil 的通道执行发生操作或是接收操作将产生什么后果?
    操作将永远阻塞

  3. select 语句的每个分支可以包含什么?
    一个通道操作

  1. 阻塞和死锁

goroutine 在等待通道的发送或者接收操作的时候,我们就说它被阻塞

goroutine 本身占用的少量内存之外,被阻塞的 goroutine 并不消耗任何资源。goroutine 会静静地停在那里,等待导致它阻塞的事情发生,然后解除阻塞。

当一个或多个 goroutine 因为某些永远无法发生的事情而被阻塞时,我们称这种情况为死锁,而出现死锁的程序通常都会崩溃或者被挂起

go
package main

func main() {
 c := make(chan int)
 <-c
}

速查

  1. 被阻塞的 goroutine 会做什么?
    什么都不做
  1. Go 允许我们在没有值可供发送的情况下通过 close 函数关闭通道,就像这样:close(c)

通道被关闭之后将无法写入任何值,如果尝试写入值将会引发惊恐。

尝试读取已被关闭的通道将会获得一个与通道关型对应的零值

执行以下代码可以获悉通道是否已经被关闭:

go
v, ok := <-c

通过将接收操作的执行结果赋值给两个变量,我们可以根据第二个变量的值来判断此次通道读取操作是否成功。如果该变量的值为 false,那么说明通道已被关闭。

速查

  1. 尝试读取一个已经关闭的通道会得到什么值?
    该通道类型的零值。

  2. 如何才能检测出通道是否已经关闭?
    使用二值赋值语句即可:v, ok := <-c。

小结

  • 通道用于在多个 goroutine 之间传递值。
  • 创建通道需要用到内置的 make 函数,如 make(chan string)。
  • close 函数可以关闭一个通道。
  • range 语句可以从通道中读取所有值,直到通道关闭为止。

并发状态

  1. 互斥锁

“互斥”一词则是“相互排斥”的缩写。goroutine 可以通过互斥锁阻止其他 goroutine 在同一时间进行某些事情,至于事情的具体内容则由程序员指定。

  • 互斥锁具有 LockUnlock 两个方法。

  • 如果有 goroutine 尝试在互斥锁已经锁定的情况下调用 Lock 方法,那么它就需要等到解除锁定之后才能够再次上锁。

  • 和通道不一样,互斥锁并未内置在 Go 语言当中,而是通过 sync 包提供。

go
package main

import "sync"

var mu sync.Mutex

func main() {
 // 我们在使用互斥锁的时候不需要对其实施初始化--它的零值就是一个未上锁的互斥锁。
 mu.Lock()
 defer mu.Unlock() // 在函数返回之前解锁互斥锁
}

注意

defer 语句在函数包含多个 return 语句时特别有用。如果没有 defer,我们就需要在每个返回语句的前面都调用一次 Unlock,而且说不定还会忘了其中的某一个

  1. 统计访问链接次数
go
package main

import (
 "fmt"
 "math/rand"
 "sync"
)

type Visited struct {
 mu      sync.Mutex
 visited map[string]int
}

func (v *Visited) VisitLink(url string) int {
 v.mu.Lock()
 defer v.mu.Unlock()
 count := v.visited[url]
 count++
 v.visited[url] = count
 return count
}

func main() {
 v := Visited{visited: map[string]int{"www.baidu.com": 0}}
 for i := 0; i < 5; i++ {
  data := []string{"www.baidu.com", "www.google.com", "www.youtube.com"}
  randNum := rand.Intn(len(data))
  go v.VisitLink(data[randNum])
 }

 fmt.Printf("%v\n", v.visited)
}
// 每次执行结果不一样,以下执行了五次的结果
// map[www.baidu.com:3 www.youtube.com:1]
// map[www.baidu.com:2 www.youtube.com:2]
// map[www.baidu.com:1 www.google.com:2]
// map[www.baidu.com:2 www.google.com:1 www.youtube.com:2]
// map[www.baidu.com:0]
  1. 互斥锁的隐患

如果一个 goroutine 在锁定互斥锁之后因为某些事情而被阻塞,那么想要取得互斥锁的其他 goroutine 就可能会被耽搁很长一段时间。更严重的是,如果持有互斥锁的 goroutine 因为某些原因而尝试锁定同一个互斥锁,那么就会引发死锁--正在尝试执行加锁操作的 goroutine 将永远无法解除已经被锁定的互斥锁,最终导致 Lock 调用被永久阻塞。

为了保证互斥锁的使用安全,我们必须遵守以下规则。

  • 尽可能地简化互斥锁保护的代码。
  • 对每一份共享状态只使用一个互斥锁。
  1. 长时间运行的工作进程

我们把这种一直存在并且独立运行的 goroutine 称为工作进程(worker)

  • 声明一个没有任何实际用途的工作进程的函数框架
go
package main

func worker() {
	for {
		select {
		// 在此处等待通道
		}
	}
}

func main() {
	// 启动 goroutine 的方法启动这个工作进程
	go worker()
}

事件循环和 goroutine

某些编程语言会使用名为事件循环的中心循环(central loop)来等待事件,并在事件发生时调用相应的已注册函数。Go 通过提供 goroutine 作为核心概念,消除了对中心循环的需求。我们可以把任何工作进程 goroutine 都看作是独立运行的事件循环。

  • 打印数字的工作进程
go
package main

import (
	"fmt"
	"time"
)

func worker() {
	n := 0
	next := time.After(time.Second)
	for {
		select {
		case <-next:
			n++
			fmt.Println(n)
			next = time.After(time.Second)
		}
	}
}

func main() {
	// 启动 goroutine 的方法启动这个工作进程
	go worker()
}

速查

  1. Go 提供了什么来替代事件循环?
    goroutine 中的循环。

  2. 在实现长时间运行的工作进程 goroutine 时,你会使用 Go 中的哪些语句?
    for 语句和 select 语句。

  3. Go 的通道可以发送哪些值?
    通道可以发送任何类型的值。

小结

  • 除非另有声明,否则永远不要在同一时间使用多于一个的 goroutine 访问相同的状态。
  • 使用互斥锁可以确保在同一时间内,只能有一个 goroutine 访问指定的状态。
  • 使用互斥锁可以只为一部分状态提供保护。
  • 应该将加锁之后要做的工作减至最少。
  • 使用长时间运行的 goroutine 可以实现带有 select 循环的工作进程。
  • 可以把工作进程的实现细节隐藏在方法后面。

======【扩展书籍】======

  • 《Go 语言实战》(Go in Practice)
  • 《Go Web 编程》(Go Web Programming)
  • 《Go 语言实战》(Go in Action)
最近更新