Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,好称“大道至简”,是新时代的“C语言”。

Go语言基本语法

变量

go语言语法规定,定义的局部变量若没有被调用会发生编译错误

变量声明格式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//var 变量名 变量类型
	var a int
//批量声明
var(
	a int
	b string
	c []float32
	d func() bool
	e struct {
		x int
		y string
	}
)

未初始化的变量将被默认初始化:

  1. 整型和浮点型变量默认值为0。
  2. 字符串默认值为空字符串。
  3. 布尔型默认值为 false
  4. 函数,指针变量,切片默认值为 nil

go语言拥有多种初始化方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//标准格式
var 变量名 变量类型 = 表达式
//编译器自动推断类型格式
var 变量名 = 表达式
//短变量声明格式
变量名 := 表达式

var a int = 10
var b = 10
b := 10

短变量声明模式只能用于函数体内,该变量名必须是没有定义过的变量,若定义过,将发生编译错误。多个短变量声明和赋值时,至少有一个新声明的变量出现在左侧,那么即便其他变量可能是重复声明的,编译器也不会报错价格。

1
2
3
4
5
var a = 10
a := 20 //重复定义变量a

var a = 10
a, b := 100, 200 //ok

go语言提供了多重赋值功能,可以实现变量交换,需要注意的是,多重赋值时,左值和右值按照从左到右的顺序赋值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//传统写法
var a int = 10
var b int = 20
vat tmp int
tmp = a
a = b
b = tmp

//一种取代中间变量的算法
a = a ^ b
b = b ^ a
a = a ^ b

//go的多重赋值
b, a = a, b

匿名变量_既不占用命名空间,也不会分配内存。

1
2
3
4
5
6
func GetData() (int, int) {
	return 10, 20
}

a, _ = GetData()
_, b = GetData()

数据类型

  1. 基本数据类型:整型,浮点型,复数型,布尔型,字符串,字符(byte,rune)。
  2. 复合数据类型: 数组,切片(slice),映射(map),函数,结构体,通道(channel),接口(interface),指针。
整型类型 字节数 取值范围 说明
int8 1 -128~127 有符号8位整型
uint8 1 0~255 无符号8位整型
int16 2 -32768~32767
uint16 2 0~65535
int32 4
uint32 4
int64 8
uint64 8
int 4/8 取决于平台
uint 4/8 取决于平台
uintptr 4/8 取决于平台 用于存放一个指针
符号类型 字节数 说明
float32 4 32位浮点型
float64 8 64位浮点型
复数类型 字节数 说明
complex64 8 64位的复数型,由float32类型的实部和虚部联合表示
complex128 16 128位的复数类型,由float64类型的实部和虚部联合表示
字符类型 字节数 说明
byte 1 表示utf-8字符串的单个字节的值,uint8的别名类型
rune 4 表示单个unicode字符,int32的别名类型

打印格式通常使用 fmt 包,通用的打印格式有:

  1. %v 值的默认格式表示。
  2. %T 值的类型的Go语法表示。
  3. %t bool类型的true/false。
  4. %b 整型的二进制。

类型转换

Go语言采用数据类型前置加括号的方式进行类型转换,T(表达式)。Go语言中不允许字符串转 int

1
2
3
var a int = 100
b := float64(a)
c := string(a)

指针

指针存储另一个变量的内存地址的变量。一个指针变量可以指向任何一个值的内存地址。go语言中使用取地址符 & 来获取变量的地址,一个变量前使用 &,会返回该变量的内存地址。go语言的指针不能运算

1
2
3
var 指针变量名 *指针类型
var ip *int
var fp *float32

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil指针也称为空指针。

1
2
3
4
5
if(ptr != nil) {}

var ptr [3]*string //指针数组就是元素为指针类型的数组。

var ptr **int //指向指针的指针

常量

常量是一个简单的标示符,在程序运行时,不会被修改。常量中的数据只可以是布尔类型,数字型(整型,浮点型和复数型)和字符串。常量定义和未被使用,不会在编译时报错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const 标示符 [类型] =   //可以省略说明符[type]

const B string = "hello"
const C = "world"
const C, D, E = value1, value2

const (
	a = 10
	b = 11
	c		//常量组中如果不指定类型和初始值,则与上一个非空常量的值相同
	d
)	

iota常量

iota 特殊常量值,是一个系统定义的可以被编译器修改的常量值。可以理解为常量组中的常量计数器,只要有一个常量,则 iota 就加1,即可以被用作枚举值

1
2
3
4
5
6
const(
	a = iota	//第一个iota等于0
	b = iota
	c = iota
	d	//和上一个一样是iota,自增1
)

类型别名与类型定义

1
2
3
4
5
6
7
8
//type 新的类型名 类型
//type 类型别名 = 类型

type byte uint8
type rune int32

type byte = uint8
type rune = int32

运算符

算术运算符

+ - * / % ++ --

关系运算符

== != > < >= <=

逻辑运算符

&& || !

位运算符

位运算符 说明
&
|
^
« 左移运算符,高位移出,低位补0。
» 右移运算符,低位移出,高位补符号位,即正数补0,负数补1。

赋值运算符 == += -= *= /= %= <<= >>== &= ^= |=

其他运算符 & *

运算符优先级

  1. ^!
  2. */%«»&&^
  3. +-|^
  4. ==!=«=>=>
  5. <-
  6. &&
  7. ||

流程控制

if条件判断语句

在go语言中,左括号必须在if或else的同一行。在if之后,条件语句之前,可以添加变量初始化语句,使用";"进行分割。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
if 布尔表达式 {
	/* 布尔表达为 true 时执行 */
}

if 布尔表达式 {

} else {

}

if 布尔表达式 {

} else if {


} else {

}

if statement; condition {

}

switch分支语句

Go语言中的switch默认给每个case自带break,因此匹配成功后不会向下执行其他的case分支,而是跳出整个switch。可以添加fallthrough强制执行后面的case分支。fallthrough必须放在case分支的最后一行。

case后的值不能重复,但可以有多个值,这些值之间用,隔开,同时测试多个符合条件的值。

switch后的表达式可以省略,默认是switch true

switch类型转换

switch语句还可以被用于 type switch(类型转换)来判断某个interface变量中实际存储的变量类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
func main() {
	var x interface{}
	switch i := x.(type) {
		case nil:
			fmt.Printf(" x 的类型 :%T",i)
		case int:
			fmt.Printf(" x 是 int 型")
		case float64:
			fmt.Printf(" x 是 float64 型")
		case func(int) float64:
			fmt.Printf(" x 是 func(int) 型")
		case bool, string:
			fmt.Printf(" x 是 bool 或 string 型")
		default:
			fmt.Printf("未知型")
	}
}

for循环语句

for是go语言中唯一的循环语句,go没有while,do…while循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
for 初始语句init; 条件表达式condition; 结束语句post {

}

for 循环条件condition {

}


for {

}

for key, value := range oldMap {
	newMap[key] = value
}

循环控制语句

break continue goto

1
2
LABEL: statement
goto LABEL

函数

go语言的函数可以返回多个值,返回值可以是返回数据的数据类型,也可以是变量名+变量类型的组合。return后的数据,要保持和声明的返回值类型,数量,顺序一致。

1
2
3
4
5
6
7
8
func 函数名 (参数列表) (返回参数列表) {
	//
}

func funcName (param type1, param type2...) (output1 type1, output2 type2..){
	//
	return value1, value2...
}

在参数列表中,如果相邻变量是同类型,则可以将类型省略。go语言支持可变参数,在函数体中变参是一个切片。一个函数最多有一个可变参数。若参数列表中还有其他类型参数,则可变参数写在所有参数最后。

go语言支持递归函数,注意防止栈溢出。

1
2
3
func add(a, b int) {}

func myfunc(arg ...int) {}

在go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。可以通过关键字type来自定义类型。

 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
package main
import "fmt"

type processFunc func(int) bool	//声明一个函数类型

func main() {
	slice := []int{1,2,3,4,5,7}
	fmt.Println("slice = ",slice)
	odd := filter(slice, isOdd)
	fmt.Println("odd num: ",odd)
	even := filter(slice, isEven)
	fmt.Println("even num: ",even)
}

func isEven(integer int) bool {
	if integer % 2 == 0 {
		return ture
	}
	return false
}

func isOdd(integer int) bool {
	if integer % 2 == 0 {
		return false
	}
	return true
}

func filter(slice []int, f processFunc) []int {
	var result []int
	for _, value := range slice {
		if f(value) {
			result = append(result, value)
		}
	}
	return result
}

匿名函数

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
30
31
32
func(参数列表) (返回参数列表) {

}

package main
import "fmt"
func main() {
	func(data int) {
		fmt.Println("hello", data)
	}(100)
}

func main() {
	f := func(data string) {
		fmt.Println(data)
	}
	f("hello world")
}

func main() {
	arr := []float{1,9,16,25,30}
	visit(arr, func(v float64){
		v = math.Sqrt(v)
		fmt.Printf("%.2f \n", v)
	})
}

func visit(list []float64, f func(float64)) {
	for _, value := range list {
		f(value)
	}
}

闭包

闭包(Closure)是词法闭包(Lexical Closure)的简称。闭包是由函数和其相关的引用环境组合而成的实体。在实现深约束时,需要创建一个能显示表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。函数 + 引用环境 = 闭包

闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定来,不会在执行时发生变化,所以函数是一个实例。

闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。闭包在某些编程语言中被称为Lambda表达式。

函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有"记忆性”。函数是编译器静态的概念,而闭包是运行期动态的概念。对象是附有行为的数据,而闭包是附有数据的行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
import "fmt"
func main(){
	for i := 0; i < 5; i++ {
		fmt.Printf("i=%d \t",i)
		fmt.Println(add2(i))
	}
}

func add2(x int) int {
	sum := 0
	sum += x
	return sum
}
 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() {
	pos := adder()
	for i := 0; i < 10; i++ {
		fmt.Printf("i=%d \t", i)
		fmt.Printf(pos(i))
	}
	
	for i := 0; i < 10; i++ {
		fmt.Printf("i=%d \t", i)
		fmt.Printf(pos(i))
	}
}

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		fmt.Printf("sum1=%d \t", sum)
		sum += x
		fmt.Printf("sum2=%d \t", sum)
		return sum
	}
}

闭包捕获来和它在同一作用域的其他常量和变量,所以当闭包在任何地方被调用,闭包都可以使用这些常量或变量。只要闭包还在使用这些变量,这些变量就依然存在,不关心这些变量是否已经超出作用域。

go语言中所有的传参都是值传递,都是一个副本。副本的内容有的是值类型,有的是引用类型。

Go语言容器

数组

数组是相同类型的一组数据构成的长度固定的序列,其中数据类型包含了基本数据类型,复合数据类型和自定义类型。

1
2
3
4
5
var 变量名 [数组长度] 数据类型

var nmus = [5]int{1,2,3,4,5}

var nums = [...]int{1,2,3,4,5}

数组的长度是数组的一个内置常量,通过将数组作为参数传递给 len() 函数,可以获得数组的长度。

1
var arrayName [x][y] variable_type

go语言中数组并非引用类型,而是值类型。将数组作为函数参数传递,是通过值传递,原始数组保持不变。

切片

切片是可变长度的序列,序列中每个元素都是相同类型。切片的数据结构可以理解为一个结构体,这个结构体包含三个元素:

  1. 指针,指向数组中切片指定的开始位置。
  2. 长度,即切片的长度。
  3. 容量,也就是切片开始的位置到数组的最后位置的长度。

声明一个未指定长度的数组来定义切片。切片不需要说明长度,采用该声明方式且未初始化的切片为空切片。默认长度为nil,且长度为0。

1
var identifier [] type

使用 make() 函数来创建切片。其中 capacity 为可选参数: make([]T, length, capacity)

1
2
3
var slice []type = make([]type, len)

slice := make([]type, len)

s := arr[startIndex:endIndex]将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片,长度为endIndex-startIndex。缺省endIndex时,表示一直到arr的最后一个元素。缺省startIndex时,表示从arr的第一个元素开始。

1
2
3
4
s := [] int {1,2,3}

arr := [5] int {1,2,3,4,5}
s := arr[:]

切片的长度是切片中元素的数量。切片的长度可以通过 len() 方法获取,切片的容量可以通过 cap() 函数获取。数组计算cap()与len()的结果相同。

切片没有自己的数据,它是底层数组的一个引用。对切片所做的任何修改都将反应在底层数组中。数组是值类型,切片是引用类型。

函数 append() 用于向切片中追加新元素。可以向切片里追加一个或者多个元素,也可以追加一个切片。append() 会改变切片所引用的数组的内容,会影响到引用同一个数组的其他切片。当容量不够时,会新建一个内存地址来存储元素。

函数 copy() 会复制切片元素,将源切片中的元素复制到目标切片中,返回复制元素的个数。复制前后的切片不存在联系。

map

map是引用类型,map 是由hash表实现的,所以对 map 的读取顺序不固定。map 是无序的,不能通过index获取,而必须通过key获取。map 长度不是固定的,和切片一样可以扩展。内置的 len()函数同样适用于map,返回map拥有的键值对的数量,但 map 不能通过 cap()函数计算容量。

1
2
3
var 变量名 map[key类型]value类型  //未初始化的map的默认值是nil

变量名 := make(map[key类型]value类型)  //该声明方式不初始化map,map也不等于nil

value, ok := map[key]获取key/value是否存在。

delete(map, key)函数用于删除集合中的某个元素,删除函数不返回任何值。

结构体

类型名是标识结构体的名称,在同一个包内不能重复。同类型的成员属性可以写在一行。结构体只定义了一种内存布局,只有当结构体实例化时,才分配内存。

1
2
3
4
5
6
type 类型名 struct {
  成员属性1 类型1
  成员属性2 类型2
  成员属性3, 成员属性4 类型3
  ...
}

内置函数 new() 对结构体进行实例化后形成结构体指针。结构体是值类型。值类型是深拷贝,为新对象分配内存,引用类型是浅拷贝,只是复制了对象的指针。

结构体的语法糖

语法糖(Syntactic Sugar)指计算机中添加某种语法,对语言的功能没有影响,但是更方便程序员使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Emp struct {
  name string
  age int8
  sex byte
}

func main() {
  emp := make(Emp)
  (*emp).name = "hello"
  (*emp).age = 30
  (*emp).sex = 1
  //语法糖写法
  emp.name = "world"
  emp.age = 30
  emp.sex = 1
}

匿名结构体与匿名字段

匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。匿名结构体由结构体定义和键值对初始化两部分组成。

1
2
3
变量名 := struct {
 
}{ }

匿名字段就是在结构体中没有名字的字段,只包含一个没有字段名的类型。同一个类型只有一个匿名字段。结构体采用匿名结构体字段可以模拟继承关系。

结构体嵌套可以模拟聚合关系(一个类作为另一个类的属性)和继承关系(一个类作为另一个类的子类)。

方法

方法有接受者,函数无接受者。接受者类似与thisself。接受者可以是struct和非struct类型,可以是指针类型或非指针类型。若方法的接受者不是指针,实际获取的是一份拷贝。只要接受者不同,方法名可以相同。

1
2
3
func (接受者变量 接受者类型) 方法名(参数列表) (返回值列表) {

}

方法的继承

匿名字段实现了一个方法,那么包含该匿名字段的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
type Human struct { 
  name, phone string
  age int
}

type Student struct{
  Human
  school string
}

type Employee struct {
  Human
  company string
}

func main() {
  s1 := Student{Human{"hello", "123*****"}, "school"}
  e1 := Employee{Human{"world", "133****"}, "company"}
  s1.SayHi()
  e1.SayHi()
}

func (h *Human) SayHi() {
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
}

方法的重写

方法重写指一个包含了匿名字段的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
type Human struct { 
  name, phone string
  age int
}

type Student struct{
  Human
  school string
}

type Employee struct {
  Human
  company string
}

func main() {
  s1 := Student{Human{"hello", "123*****"}, "school"}
  e1 := Employee{Human{"world", "133****"}, "company"}
  s1.SayHi()
  e1.SayHi()
}

func (h *Human) SayHi() {
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
}

func (h *Student) SayHi() {
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
}

func (h *Employee) SayHi() {
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
}

接口

在go语言中,接口是一组方法的签名,接口指定了类型应该具有的方法,类型决定了如何实现这些方法。当某个类型为接口中的所有方法提供了具体的实现,这个类型就被称为实现了该接口。go语言的类型都是隐式实现接口的,任何定义了接口中所有方法的类型都被称为隐式的实现了该接口。

1
2
3
4
5
6
type 接口名字 interface {
  方法1 ([参数列表]) [返回值]
  方法2 ([参数列表]) [返回值]
  ...
  方法3 ([参数列表]) [返回值]
}
 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
type Phone interface {
  call()
}

type AndroidPhone struct {

}

type IPhone struct {

}

func (a AndroidPhone) call() {
  fmt.Printf("I am Android Phone\n")
}

func (a IPhone) call() {
  fmt.Printf("I am IPhone\n")
}

func main() {
  var phone Phone //定义接口类型
  phone = new(AndroidPhone)
  phone.call()
  phone = AdnroidPhone{}
  phone.call()
  phone = new(IPhone)
  phone.call()
  phone = IPhone{}
  phone.call()
}

duck typing

go语言没有implementsextends关键字,这类编程语言叫做duck typing编程语言。duck typing是描述事物的外部行为而非内部结构。使用duck typing的编程语言往往被归为"动态语言"或"解释型语言”。

go语言采取的方式为:

  1. 结构体类型T不需要显示地声明它实现了接口I。只要类型T实现了接口I规定的所有方法,它就自动的实现了接口I。
  2. 将结构体类型的变量显示或隐式地转换为接口I类型的变量i。可以在编译时检查参数的合法性。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type SayHello interface {
  SayHello() string
}

type Duck struct {
  name string
}

type Person struct {
  name string
}

func (d Duck) SayHello() string {
  return d.name + "ga ga ga !"
}

func (p Person) SayHello() string {
  return p.name + "hello!"
}

func main() {

}

多态

go语言的多态是在接口的帮助下实现的-定义接口类型,创建实现该接口的结构体对象。

空接口

空接口没有任何方法。任意类型都可以实现该接口。空接口表示任意数据类型。例如,定义一个map,key是string,value是任意数据。定义一个切片,其中存储任意类型的数据。

1
2
3
map := make(map[string]interface{})

slice := make([]interface{}, 0, 10)

接口对象转型

1
2
3
instance, ok := 接口对象.(实际类型)

接口对象.(type)

包(package)

异常处理

error

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

error 本质是一个接口类型,包含一个 Error() 方法,错误值存储在变量中,通过函数返回。

1
2
3
4
res, err := Sqrt(-100)
if err != nil {

}

结构体只要实现了Error() string这种格式的方法,就代表实现了该错误接口,返回值为错误的具体描述。go语言errors包对外提供了可供用户自定义的方法,errors包下的New()函数返回error对象,errors.New()函数创建新的错误。

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

func New(text string) error {
  return &errorString{text}
}

type errorString struct {
  s string
}

func (e *errorString) Error() string {
  return e.s
}

func Errorf(format string, a ...interface{}) error {
  return errors.New(Sprintf(format, a...))
}

自定义错误

  1. 定义一个结构体,表示自定义错误的类型。
  2. 让自定义错误类型实现error接口:Error() string
  3. 定义一个返回error的函数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
import (
  "time"
  "fmt"
)

type MyError struct {
  When time.time
  What string
}

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

defer

defer用于延迟一个函数或者方法的执行,defer语句只能出现在函数或方法的内部。在函数中可以存在多个defer语句,defer语句会按照逆序执行。

defer语句常被用来处理成对操作,例如:打开-关闭,链接-断开,加锁-释放锁。

延迟函数的参数在执行延迟语句时被调用,而不是执行实际的函数被调用时执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
  str := "hello world!"
  fmt.Printf("原始字符串:\n %s\n", str)
  fmt.Println("反转后的字符串: ")
  ReverseString(str)
}
func ReverseString(str string) {
  for _, v := range []rune(str) {
   defer fmt.Printf("%c", v)
  }
}

panic与recover

panic()内建函数,中断程序的执行。recover()仅在延迟函数中有效。

并发

  1. 并发(Concurrency)在微观层面上,任务不会同时进行。
  2. 并行(Parallelism)多个任务一定是同时运行的。

Goroutine

Go语言通过go关键字来启动一个goroutine

  1. go的执行是非阻塞的,不会等待。
  2. go后面的函数的返回值会被忽略。
  3. 调度器不保证多个goroutine的执行次序。
  4. 没有父子goroutine的概念,所有的goroutine是平等地被调用和执行。
  5. go程序在执行时会单独为main函数创建一个goroutine,遇到其他go关键字时在创建其他的goroutine
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
  "runtime"
  "time"
)

func main() {
  go func() {
    sum := 0
    for i := 0; i < 10000; i++ {
      sum += 1
    }
    println(sum)
    time.Sleep(1 * time.Second)
  }()
  
  println("NumGoroutine=", runtime.NumGoroutine())

  time.Sleep(5 * time.Second)
}

select

sync

反射