Go语言学习笔记

Go 语言入门

变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a int // 定义 int 变量 a
var b int = 10 // 定义 int 变量 b 并赋初值
var c, d int // 定义多个 int 变量
var e, f int = 3, 4 // 定义多个 int 型变量
var g, h, i, j = 5, 6, true, "def" // 使用类型推导为多个变量进行赋值
k, l, m := 7, 8.1, false, "def" // 更简短的定义与赋值语句,:= 只能第一次用,往后要用 =
// 并且只能在函数内部使用,函数外部无法使用 :=
var (
aa = 1
bb = 2.2
cc = "define"
dd = true
) // 集中定义多个变量

// 在函数外的变量使用范围是包内部,GO 无全局变量的说法

TIPS 运行 GO 的 main 函数,所在包的包名也要是 main

1
2
3
4
5
6
7
package main  //

import "fmt"

func main() { //
fmt.Println("Hello World")
}

内建变量

  • bool 布尔型
  • string 字符串型
  • (u)int, (u)int8, (u)int16, (u)int32, (u)int64, (u)intptr 数字表示int位数,不加就默认为当前计算机位数,ptr 表示指针,u表示无符号
  • byte 字节型,8位1字节
  • rune Go语言的字符型,32位4个字节
  • float32, float64 浮点型,数字为位数
  • complex64, complex128 复数类型,前者实部和虚部都是 float32,后者两个都是 float64

强制类型转换

Go语言类型转换是强制

常量的定义

与变量的定义有很多相似的地方,不过要用 const 代替 var

定义常量的时候可以指定类型,也可以不指定类型,若不指定类型,就是个位置类型的文本,在使用的时候会进行推断

1
2
3
4
const a, b = 3, 4
var c = int
c = int(math.Sqrt(a * a + b * b))
// math.Sqrt 只能传入浮点数,若 a,b 指定了 int 型则会报错,若不指定则会当成 float

定义枚举类型

Go 语言没有枚举值,只能用 const 来模拟

1
2
3
4
5
6
7
8
9
10
const (
Mon = iota
Tue
Wed
Thr
Fri
Sat
Sun
)
fmt.Println(Mon, Tue, Wed, Thr, Fri, Sat, Sun) // 0, 1, 2, 3, 4, 5, 6

iota 是一个预先定义的标识符,在 const 表达式中(通常在括号内),用来表示当前 const 规范的非类型整数顺序数,索引为 0

条件语句

if

1
2
3
4
5
6
7
const filename = "in.txt"
contents, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}

Go的条件语句的特点

  1. if 条件里可以赋值,且赋值的变量作用域只在这个 if 语句里
  2. 条件语句部分不需要括号,并且可以用 ; 来分隔多个条件

上面的例子可以进一步修改为

1
2
3
4
5
if contents, err := ioutil.ReadFile(filename); err == nil {
fmt.Printf("%s\n", contents)
} else {
fmt.Println(err)
}

switch

1
2
3
4
5
6
7
8
9
10
11
12
func eval(a, b int, op string) int {
var result int
switch op {
case "+": result = a + b
case "-": result = a - b
case "*": result = a * b
case "/": result = a / b
default:
panic("unsupported operator:" + op)
}
return result
}

Go 语言中 switch 的特点:

  1. swtich 会自动 break,除非使用 falllthrough
  2. switch 后可以没有表示,在 case 中写表达式也可以

循环语句

for

去其他语言的 for 循环的区别在于没有 ( ),并且三个部分各自都可以省略

1
2
3
4
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}

Go 语言中没有 while,如果把 for 玄幻的初始化和递增条件省略,就类似于 while

如果把三个部分都省略,那么就相当于死循环

1
2
3
for {
...
}

函数

多个返回值

Go 的函数可以返回多个值

1
2
3
func div(a, b int) (int, int) {
return a / b, a % b
}

此外,还可以给返回值命名,可以增强代码可读性,也可以为编辑器的补全提供帮助。

若指明了返回值名称,则可以对返回值进行直接赋值,最后只需要一个 return 即可(该用法仅用于非常简单的函数,否则影响可读性)

1
2
3
4
5
func div(a, b int) (q, r int) {
q = a / b
r = a % b
return
}

函数式编程

1
2
3
4
5
6
func apply(op func(int, int) int, a, b int) int {
fmt.Printf("Calling %s with (%d, %d)\n",
reflect.ValueOf(op).Pointer(runtime.FuncForPC(p).Name()),
a, b)
return op(a, b)
}

使用

1
2
3
apply(func(a int, b int) int {
return a * b
}, 2, 3)

可变参数列表

Go 语言没有默认参数函数重载,但有可变参数列表

1
2
3
4
5
6
7
func sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}

指针

Go 语言的指针不能进行运算,这也是他简单的地方
Go 语言只有值传递一种方式(与值传递对应的是类似与 C++ 的引用传递,这个才 Go 中是没有的)

1
2
3
func swap(a, b *int) {
*a , *b = *b, *a
}

数组

1
2
3
4
var arr1 [5]int 			// 定义长度为 5 的整型数组,默认值都为 0
arr2 := [3]int{1, 3, 5} // 定义并初始化长度为 3 的整型数组
arr3 := [...]int{1, 3, 4} // 不指定长度初始化一个整型数组
var grid [3][4]int // 定义 3 行 4 列的二维数组

数组的遍历,可以使用 range 关键字,可以获得下标和值

1
2
3
4
arr := [...]int{2, 4, 6, 8, 10}
for i, v := range arr {
fmt.Println(i, v)
}

如果只需要值不需要下标,则可以用 __ 代替

1
for _, v := range arr { ... }

若只要下标,则可以直接写成

1
for i range arr { ... }

Go 中的数组是值类型,也就是说数组传递给函数后,会拷贝数组内容,修改数组的值不会改变原数组的内容(大部分语言的数组都是引用传递,而 Go 则是值传递)。此外[10]int[20]int 是不同类型,也就是说实参数组的长度要与形参数组的长度相一致,否则会报错。

要实现引用传递,则需要用到指针

1
2
3
4
5
func foo(arr *[5]int) { ... }
func main() {
var arr[5]int
foo(&arr)
}

可见使用数组还是相当麻烦的,所以在 Go 中使用的更多的是切片(Slice)

切片

1
2
3
4
5
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(arr[2:6]) // [2 3 4 5]
fmt.Println(arr[:6]) // [0 1 2 3 4 5]
fmt.Println(arr[2:]) // [2 3 4 5 6 7]
fmt.Println(arr[:]) // [0 1 2 3 4 5 6 7]

切片生成的是数组的一个视图,修改切片会改变原数组,在函数中传递数组时,若要方便修改原数组,可以使用切片

1
2
3
4
5
func foo(arr []int) { ... } //  方括号内什么也没有就表示切片
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
foo(arr[:])
}

  1. slice 可以向后扩展,但是不能向前扩展
  2. s[i] 不可以超越 len(s),向后扩展不可以超越底层数组 caps(s)
  3. 向slice中添加元素,如果长度没超过 cap,则会覆盖原数组的内容,若超过cap,系统则会重新分配更大的底层数组
  4. 由于值传递,使用 append 有可能会修改 slice 的 len 或 cap ,因此必须接受 append 的返回值,即 newSlc := append(slc, newValue)

切片的创建方式

1
2
3
s1 := []int{2, 4, 6, 8}		// 定义并初始化切片,[]内什么都不加为切片,加...或数字为数组
s2 := make([]int, 16) // 创建长度为 16 的空切片
s3 := make([]int, 10, 32) // 创建长度为 16 ,底层数组长度为 32 的空切片

切片的操作

切片的复制

1
copy(s1, s2) // 把 s2 复制到 s1 上

切片元素的删除

1
2
3
4
5
6
7
s = append(s[:3], [4:]) // 删除中间元素(第三个)

front := s[0] // 取得头元素
s = s[1:] // 删除头元素

tail := s[len(s) - 1] // 取得尾元素
s = s[:len(s)-1] // 删除尾元素

Map

定义形式如下

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
map[K]V
map[K1]map[K2]V // 复合map

// example
m := map[string]string {
"name" : "Inno",
"id" : "1234",
"gender" : "male",
}
fmt.Println(m) // map[gender:male id:1234 name:Inno]

// 另一种定义 map 的方式
m2 = make(map[string]string) // m2 == empty map
var m3 = map[string]string // m3 == nil

// map 的遍历(不保证顺序)
for k, v := range m {
fmt.Println(k, v)
}

// 获取元素
name := m["name"]
fmt.Println(name)

// 判断元素是否存在
if age, ok := m["age"]; ok {
fmt.Println("age:", age)
} else {
fmt.Println("There is not a key called age!")
// 若元素不存在,获得的值也不是 nil 而是一个 zeroValue
}

// 删除 key
delete(m, "gender")

// 获取长度
len(m)
  • map 使用哈希表,必须可以比较相等
  • 除了 slice、map、function 的内建类型都可以作为key
  • 自建类型(Struct 类型)也可以作为 key,前提是没有 slice、map、function

面向对象

Go 语言面向对象仅支持封装,不支持继承和多态;Go语言没有class,只有 struct

结构的创建和简单使用

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

import "fmt"

type treeNode struct {
value int
left, right *treeNode
}

// 工厂方法
func createTreeNode(value int) *treeNode {
return &treeNode{value: value} // 返回的是局部变量的地址!!
}

func main() {
var root treeNode

// Go struct 有很多构造方法,所以可以不需要所谓的构造函数

root = treeNode{value: 3}
root.left = &treeNode{}
root.right = &treeNode{5, nil, nil}
root.right.left = new(treeNode)
root.left.right = createTreeNode(2) // 使用工厂方法

// 定义在 Slice 中可以省略反复写 treeNode
nodes := []treeNode {
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes)
}

Q 在工厂方法中返回的局部变量的地址,该局部变量是创建在堆上还是在栈上?

A 因为 Go 语言有垃圾回收机制,具体是在堆上还是在栈上,由编译器来决定。若是函数中的局部变量,那么建在栈上;若将局部变量的地址返回给外部调用,那么这个局部变量就会键在堆上,并参与垃圾回收,当不再使用时,就会被回收掉。所以退出函数后,局部变量就会被销毁,这是不一定的。

为结构定义方法

1
2
3
func (node treeNode) print() {
fmt.Print(node.value)
}
  • (node treeNode) 显示定义和命名方法接收者,随便叫什么都可以

因为在Go语言中都是值传递,那么如果要在函数内修改值的话,值传递是无法生效的,因此可以使用指针作为方法接收者

1
2
3
func (node *treeNode) setValue(value int) {
node.value = value
}
  • 只有使用指针才可以改变结构内容
  • nil 指针也可以调用方法

值接收者 VS 指针接收者

  1. 要改变内容时必须使用指针接收者
  2. 结构过大时也考虑使用指针接收者(因为值接收者是对值的拷贝,结果过大则消耗越大)
  3. 一致性:如果有指针接收者,最好都是指针接收者(一个建议,保持一致有利于阅读以及代码维护)
  4. 值接收者 是 Go 语言特有的
  5. 值/指针接收者均可接收值/指针

封装

名字一般使用CamelCase,首字母大写为 public,首字母小写为 private。

封装是对包而言的,每个目录可以有很多个包,但只能有一个main包,main 包包含可执行入口。

为结构定义的方法必须在同一个包内,但可以是不同文件。

扩充类型

  1. 使用别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Queue []int //Queue 本质是一个int切片
    // 扩展 []int,使其具有队列的功能
    /* 入队 */
    func (q *Queue) Push(v int) { // 设计修改内容的要使用指针接收者
    *q = append(*q, v)
    }
    /* 出队 */
    func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head
    }
  1. 使用组合

利用组合为之前的 treeNode对象扩展一个遍历方法

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
// 定义新的结构体,使用组合的方式扩展类型
type myTreeNode struct {
node *treeNode
}

func (myNode *myTreeNode) postOrder() {
if myNode == nil && myNode.node == nil {
return
}
left := myTreeNode{myNode.node.left}
right := myTreeNode{myNode.node.right}

left.postOrder()
right.postOrder()
myNode.node.print()
}

func main() {
var root treeNode
// 定义二叉树
// ...
myRoot := myTreeNode{&root}
myRoot.postOrder()
fmt.Println()
}

接口

接口的定义

使用者定义接口

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"
)

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

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

func main() {
var r Retriever
// 实现者待实现
fmt.Println(download(r))
}

实现接口

在接口所在目录下新建目录,创建新文件用来实现接口方法,当前目录结构为

1
2
3
4
LearnGo>innofang.io>innofang>interfaces
>real
|- main.go // 接口实现所在位置
|- main.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
25
26
27
28
29
package real

import (
"time"
"net/http"
"net/http/httputil"
)

type Retriever struct {
UserAgent string
TimeOut time.Duration
}

// 只要实现了接口方法,就被认为实现了接口
func (r Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}

result, err := httputil.DumpResponse(resp, true)
resp.Body.Close() // 使用完 response 需要关闭

if err != nil {
panic(err)
}

return string(result)
}

使用接口实现

1
2
3
4
5
func main() {
var r Retriever
r = real.Retriever{} // 使用接口 包名.结构体名
fmt.Println(download(r))
}

接口里面有什么

接口变量里面的内容有两种情况:

  1. 实现者的类型和实现者的值
  2. 实现者的类型和实现者的指针,其中指针指向实现者

具体是哪个,可以自由选择,因此在使用接口变量的时候,不要使用接口的地址,因为接口内部包含有指针

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接收者实现只能以指针方式使用;值接收者都可

若接口方法实现时使用了指针接收者,那么在定义该接口实习时,需要取接口地址

1
2
3
4
// 修改 real 包下的 Get 方法实现,使用指针接收者
func (r *Retriever) Get(url string) string {
...
}

那么在定义该接口实现者时,就需要使用取地址符

1
2
3
4
5
func main() {
var r Retriever
r = &real.Retriever{}
fmt.Println(download(r))
}

查看接口变量

再定义一个接口实现者

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

type Retriever struct {
Contents string
}

// 只要实现了接口方法,就被认为实现了接口
func (r Retriever) Get(url string) string {
return r.Contents
}

查看接口变量有两种方式

Type Switch

1
2
3
4
5
6
switch v := r.(type) { // 获取类型
case mock.Retriever:
fmt.Println("Contents: ", v.Contents)
case *real.Retriever:
fmt.Println("UserAgent: ", v.UserAgent)
}

Type Assertion

1
2
3
4
5
6
7
8
9
10
11
12
realRetriever := r.(*real.Retriever) // 通过
fmt.Println(realRetriever.TimeOut)

//mockRetriever := r.(mock.Retriever) // 错误,因为 r 在之前定义的是 real.Retriever
//fmt.Println(mockRetriever.Contents) // 因此无法转换

// 更优雅的转换方式
if mockRetriever, ok := r.(mock.Retriever); ok {
fmt.Println(mockRetriever.Contents)
} else {
fmt.Println("not a mock")
}

表示任何类型interface{}

1
2
3
4
5
6
7
8
9
10
11
type Quue []interface{} // 该slice就可以接收任何类型的值

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

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

接口的组合

假设有另一个结构体 Poster,现在要把含有 Get 方法的 Retriever 和含有 Post 方法的 Poster 进行组合

1
2
3
4
5
func RetrieverPoster interface {
Retriever // 把结构体名包含进来就可以了
Poster
// 也可以定义其他方法
}

现在对于实现者来说,他可以自己实现 Post 方法和 Get 方法了

使用者不管定义者如何定义,它只管实现它要的方法,不需要关心使用的是什么接口

只要是一个类型就可以实现接口,这也是 Go 语言灵活的地方

defer 调用

何时使用 defer 调用

  • Open/Close
  • Lock/Unlock
  • PrintHeader/PrintFooter

被 defer 修饰的语句会确保在函数结束时发生,无论这个程序是否被提前终止,如果有多个 defer 会有一个栈来存储语句,采用先进后出的顺序执行语句

错误处理

1
2
3
4
5
6
7
8
9
file, err := os.Open("abc.txt")
if err != nil {
// If there is an error, it will be of type *PathError.
if pathError, ok := err.(*os.PathError); !ok {
panic(err) // 如果是自定义 error 就会直接 panic
} else {
fmt.Println("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err)
}
}

panic

  • 停止当前函数执行
  • 一直向上返回,执行每一层的 defer
  • 如果没有遇见 recover,程序退出

recover

  • 仅在 defer 调用中使用
  • 获取 panic 的值
  • 如果无法处理,可重新 0panic
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 (
"errors"
"fmt"
)

func tryRecover() {
defer func() {
r := recover()
if err, ok := r.(error); ok {
// 如果是 error, 则会进行处理
fmt.Println("Error occured:", err)
} else {
// 如果不是 error,比如直接 `panic(123)` ,则不处理
panic(fmt.Sprint("I don't know what to do: %v", r))
}
}()
panic(errors.New("this is an error"))
}

func main() {
tryRecover()
}

表格驱动测试

  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败
  • Go 语言的语法使得我们更易实践表格驱动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Test(t *testing.T) {
tests := []struct {
a, b, c int32
}{
{1, 2, 3},
{0, 2, 2},
{0, 0, 0},
{-1, 1, 0},
{math.MaxInt32, 1, math.MinInt32},
}

for _, test := range tests {
if actual := add(test.a, test.b); actual != test.c {
// 测试失败的说明
...
}
}
}

代码覆盖率

在测试文件所在目录下执行

1
2
go test -coverprofile=c.out // 运行测试代码
go tool cover -html=c.out // 展示覆盖率

性能测试

1
2
3
4
5
6
7
8
9
10
11
func Becnchmark(b *testing.B) {
testCase := ...
ans := ...

for i := 0; i < b.N; i++ { // 具体运行多少遍不用管,b.N 由系统决定
actual := testedFunc(testCase)
if actual != ans {
b.Errorf("...", ...)
}
}
}

命令行在测试文件所在目录下执行

1
go test -bench .

性能调优

在测试文件所在目录下执行

1
go test -bench . -cpuprofile cpu.out

生成了二进制文件 cpu.out,接着使用命令

1
go tool pprof cpu.out

进入交互式命令行,可以使用 help 查看,最简单的方式是输入 web 查看二进制文件。内容上,看到的方框越大,说明耗时越久,优化方框大的部分可以使程序性能更有。

Goroutine

协程

  • 轻量级“线程”
  • 非抢占式多任务处理,由协程序主动交出控制权
  • 编译器/解释器/虚拟机层面的多任务
  • 多个协程可能在一个或多个线程上运行

因为是非抢占式的 ,若不切换协程,则会一直执行一个协程(io操作会自动切换协程)。因此对于自定义协程,需要注意有没有手动交出控制权

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i]++
runtime.Gosched() // 手动交出控制权
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}

goroutine 的定义

  • 任何函数只需要加上 go 就能送给调度器运行
  • 不需要在定义时区分是否是异步函数
  • 调度器在合适的点进行切换
  • 调试时,在命令行使用 -race 来检测数据访问冲突 go run -race goroutine.go

goroutine 可能的切换点

  • I/O, select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()

只是参考,不能保证切换,不能保证在其他地方不切换

通道

指的是 goroutine 之间的通道,可以让 goroutine 之间互相通信。每个通道都有与其相关的类型,即通道允许传输的数据类型。通道的零值为 nil,需要使用内置 make 方法来定义

1
2
3
4
// 声明通道
var 通道名 chan 数据类型
// 创建通道:如果通道为 nil(就是不存在),就需要创建通道
通道名 = make(chan 数据类型)

通道的注意点

Channel 通道在使用的时候,有以下几个注意点:

  1. 用于 goroutine,传递消息
  2. 通道,每个都有相关联的数据类型,nil chan 是无法使用的
  3. 使用通道传递数据:<-
    1. chan <- data,发送数据到通道。向通道中写
    2. data <- chan,从通道中获取数据。从通道中读数据
  4. 阻塞
    1. 发送数据:chan <- data,阻塞,直到另一条 goroutine 读取数据来接触阻塞
    2. 读取数据:data <- chan,阻塞,知道另一条 goroutine 写入数据来接触阻塞
  5. Channel 是同步的,意味着同一时间只有一条 goroutine 来操作

通道是 goroutine 之间的连接,所以通道的发送和接收必须处在不同的 goroutine 中

1
2
data := <- a // 从通道 a 中读取数据
a <- data // 将数据写进通道 a 中

定义方式

1
2
3
4
5
6
7
8
9
10
11
// var c chan int // c == nil
c := make(chan int)
go func() {
for {
n := <- c
fmt.Println(n)
}
}()
c <- 1
c <- 2
time.Sleep(time.Millisecond)

关闭 channel

channel 可以被发送方 close,但是 channel 即使被 close 了,也会接收数据,只不过收到的是 zeroValue

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
func worker(id int, c chan int) {
for {
// 判断发送方是否关闭了 channel

/* // 方法一
n, ok := <-c
if !ok {
break
}
fmt.Printf("worker %d received %c\n", id, n)
*/
// 方法二 利用 range (更好)
for n := range c {
fmt.Printf("worker %d received %c\n", id, n)
}
}
}

// 发送方发数据
func channelClose() {
c := make(chan int, 3)
go worker(0, c)
c <- 'a'
c <- 'b'
c <- 'c'
c <- 'd'
close(c)
time.Sleep(time.Millisecond)
}

Select

select 是 Go 中的一个控制结构,select 语句类似于 switch 语句,但是 select 会随机执行一个可执行的 case。如果没有 case 可运行,若设置了 default 则会执行 default,否则它将阻塞,直到有 case 可执行。语法结构上与 switch 语句很相似

1
2
3
4
5
6
7
8
select {
case communication clause:
statement(s)
case communication clause:
statement(s)
default: // optional
statement(s)
}
  • 每个 case 都必须是一个通信
  • 所有 channel 表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行
  • 若此刻没有 case 语句可以执行时,若有default语句,则执行该语句;否则 select 将阻塞,知道有某个通信可以运行,Go 不会重新对 channel 或值进行求值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
ch1 := make(chan int)
ch2 := make(chan int)

go func() {
time.Sleep(1 * time.Second)
ch1 <- 100
}()

select {
case num1 := <-ch1:
fmt.Println("Read from ch1:", num1)
case num2, ok := <-ch2:
if ok {
fmt.Println("Read from ch2:", num2)
} else {
fmt.Println("ch2 has been closed")
}
case <-time.After(1 * time.Second):
fmt.Println("After 3 seconds")
default:
fmt.Println("default ...")
}
}
文章作者: Inno Fang
文章链接: https://innofang.github.io/2020/02/11/Go语言学习笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来自 Inno's Blog