golang

go简介

  • GOROOT 是Go语言的安装路径。
  • GOPATH 是一个环境变量,代表了Go语言的工作空间目录。从Go 1.11版本开始,引入了模块管理(Module)系统,通过go mod命令来管理依赖,这使得开发Go应用时不再强制要求将代码放在GOPATH目录下。

  • golang只存在值传递(值的副本(深拷贝)、指针的副本(浅拷贝))

golang的优势:

  • 编译型语言:Go是编译型语言,静态链接,运行时可不依赖库。(1.5版本后支持动态链接)
  • 语言层并发:基于CSP(Communicating Sequential Processes)并发模型,提供了轻量级线程(goroutines)通道(channels)机制,使得并发编程更加高效和简洁。
  • 简单易学:支持内嵌c语言,关键词少,面向对象,跨平台

缺点:

  • 包管理,大部分包都在github上
  • 所有Exception都由Error处理
  • 缺少泛型:在Go 1.18版本之前,Go 1.18引入了泛型。
  • C语言更接近硬件,提供了更多的底层控制,而Go语言则在保证性能的同时,更注重开发效率和代码的安全性。当需要在Go中使用C代码时,通常需要通过cgo进行桥接。

go编译运行

1
2
3
4
go run my.go //编译运行

go build my.go //编译
./my //运行

go生态

  • Web:beego、gin、echo、iris
  • 微服务:go kit、 istio
  • 容器编排:k8s、swarm
  • 服务发现:consul
  • 存储引擎:etcd(k/v存储)、tidb(分布式存储)
  • 静态建站:hugo
  • 中间件:nsq(消息队列)、zinx(tcp长连接框架,轻量级服务器)、Leaf(游戏服务器)、gPRC(RPC框架)、codis(redis集群)
  • 爬虫框架: go query

go路径与导包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//mygo.go
package main
import (
"fmt"
"golangstudy/lib1"
)
func main() {
fmt.Println("Hello from main")
// 调用 lib1 包中的 Hello 函数
lib1.Hello()
}

// lib1/lib1.go
package lib1
import "fmt"
func Hello() { //大写导出
fmt.Println("Hello from lib1")
}
  • 直接导入需要关闭go moudle
1
go env -w GO111MODULE=off
  • 编译器会尝试在$GOROOT/src$GOPATH/是如此、寻找对应包,且路径不需要包含文件名。

  • 在go1.11版本后可使用go modules导包。

1
2
3
4
.
├── lib1
│ └── lib1.go
└── my.go

go 方法名大小写

  • 首字母大写方法被称为导出方法(exported methods),它们可以在包的外部被访问,类似于其他语言中的 public 方法。

  • 首字母小写方法被称为未导出方法(unexported methods),它们只能在定义它们的包内部被访问,类似于其他语言中的 private 方法。

点、匿名和别名导包

1
2
3
4
5
import (
_ "golangstudy/lib1" // 匿名导入,仅调用 lib1 包的 init() 函数,不能调用lib1其他函数
mylib "golangstudy/lib1" // 别名导入,使用 mylib 作为别名
. "golangstudy/lib1" // 点导入,直接使用 lib1 包的导出成员,类似cpp的using namesapce
)

GOMODULES

  • GOPATH
    • bin 存储编译生成的可执行文件。
    • pkg存储编译生成的包文件。
    • src :存储 Go 源代码文件。
    • GOPATH缺点:
      • 无版本控制
      • 无法同步一致第三方版本号
      • 无法指定当前项目引用的第三方版本号

GOMODOULES

  • GO111MODULE:开启go mod
1
go env -w GO111MODULE=on
  • GOPROXY:修改为国内镜像,direct指示符用于指示go回到模块版本的源地址抓取
  • GOSUMBDB:检验拉取的模块数据,通过GOPROXY修改代理解决
  • GOPRIVATE(作为GONOPROXYGONOSUMDB默认值):用于指示私有模块,可通过通配符设置

初始化gomod并为项目命名

1
go mod init [name]
  • 自动下载依赖
1
go mod tidy
  • 手动下载依赖
1
go get [包路径]

GOPATH/pkg/mod:用于存储下载的模块版本,以供本地项目使用。

GOSUM文件

go.sum文件包含以下格式的条目:

1
<module> <version> <hash>
  • <module>:依赖模块的路径。
  • <version>:依赖模块的版本号。
  • <hash>:依赖模块版本文件的哈希值。通常是两个哈希值,一个是校验和(checksum),另一个是用于验证模块内容的哈希。

helloworld

go的包名类似于cpp的命名空间,同一包名的代码将在编译时合并,包名要求:

  • 包名应该是一个小写单词。
  • 包名通常与其所在的目录名称相同。
1
2
3
4
5
6
7
8
9
10
package main //程序的包名与文件名无关

import (
"fmt" //导入包,提供格式化输入输出的函数
)

func main() { //go分号必须在函数名后
fmt.Println("hello world")
}

变量声明与格式化字符串

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 main //程序的包名与文件名无关

import (
"fmt" //导入包,提供格式化输入输出的函数
)

func main() { //go分号必须在函数名后
//方式一,声明变量a,默认初始化为0
var a int

//方式二,初始化变量
var b string = "Randolfluo"

//方式三,省去数据类型
var c = 2 //自动推导为int

//方式四,省去var关键词,但是这种方式不能声明全局变量
d := 3.3

//方式五,初始化多个变量
var (
e int = 3
f int = 4
)
fmt.Printf("a=%d,b=%s,c=%d,d=%f,e=%d,f=%d\n", a, b, c, d, e, f)
fmt.Printf("a=%T,b=%T,c=%T,d=%T,e=%T,f=%T\n", a, b, c, d, e, f)

}

golang常用的格式化字符串

  • %T:输出值的类型。
  • %t:布尔值。
  • %d:整型十进制表示。
  • %x, %X:整型十六进制表示。
  • %f, %F:浮点数。
  • %s:字符串。
  • %q:带双引号的字符串或字符。
  • %p:指针。
  • %v:表示任意类型。

const 与 iota

  • const用于定义常量
  • iota用于标识枚举的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 package main 

import (
"fmt"
)
const (
a, b = iota + 1, iota + 2 //line 0
c, d //line 1
e, f = iota + 2, iota + 2 //line 2
)

func main() {
fmt.Println(a, b, c, d, e, f)
//1 2 2 3 4 4
}

多返回值

  • Go 语言使用多返回值来接收数据和错误。这是 Go 的一个设计哲学,它鼓励开发者显式地处理错误,而不是依赖于异常或全局的错误状态。

  • 在 Go 中,许多标准库函数和方法都会返回两个值:第一个是函数的主要返回结果,第二个是 error 类型,用于指示是否发生了错误。如果没有错误发生,error 值通常是 nil

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

import (
"fmt"
)
//多返回值, 有形参名称
func add_dsc(a int, b int) (add int, dsc int){
//add dsc属于函数的局部变量,默认初始化为0
add = a + b
dsc = a - b
return
}
func main() {
add, dsc := add_dsc(1, 2)
fmt.Println(add, dsc)
return
}

defer

类似于cpp的析构函数,用于返回时释放资源。

  • 其执行顺序类似于栈
  • defer 语句会在 return 之后执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func funcA() int {
defer fmt.Println("A")
return 0
}

func funcB() int {
defer fmt.Println("C")
defer fmt.Println("B")
return funcA()
}

func main() {
funcB()
}
//A
//B
//C

数组&切片slice

数组

  • 长度固定。
  • 类型相同。
  • 在内存中连续分配。
  • 作为值类型,传递时会复制整个数组
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
package main

import (
"fmt"
)

func change_array(arr [10]int) {
arr[0] = 1144514
}

func main() {
//数组声明,初始化为0
var Array1 [10]int
for i := 0; i < len(Array1); i++ {
fmt.Print(Array1[i], " ")
}
fmt.Println()

//数组声明,初始化为1-10
var Array2 [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for idx, val := range Array2 {
fmt.Printf("Array2[%d] = %d\n", idx, val)
}

//值传递方式为深拷贝,函数的修改不会影响原函数
change_array(Array2)
fmt.Printf("Array2[0] = %d\n", Array2[0])

}

切片slice

  • 长度动态。
  • 类型相同。
  • 底层通常由数组支持。
  • 作为引用类型,传递时不会复制底层数组(复制指针)。
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
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
)

func change_slice1ay(slice1 []int) {
slice1[0] = 1144514
}

func main() {

var slice0 []int //声明一个切片,但是未初始化空间
if slice0 == nil {
fmt.Println("slice0 is nil")
} else {
fmt.Println("slice0 is not nil")
}

slice1 := []int{1, 2, 3, 4, 5} //初始化slice1为1-5
//切片传递实际为浅拷贝,传递的是指针,函数的修改会影响原slice1
change_slice1ay(slice1)
fmt.Println(slice1)

slice3 := make([]int, 5, 10) //初始化slice2为长度为5,容量为10
fmt.Println(slice3)

fmt.Println(len(slice3), cap(slice3), slice3)

slice3 = append(slice3, 1, 2, 3, 4, 5, 6) //追加元素,容量不够,扩容
fmt.Println(len(slice3), cap(slice3), slice3)

//直接赋值是将一个切片的地址赋值给另一个地址
slice4 := []int{1, 2, 3, 4, 5}
slice5 := slice4[0:3] //切片
slice5[0] = 100 //修改切片
fmt.Println(slice4)
fmt.Println(slice5)

//通过copy构造一个新切片
slice6 := make([]int, 5, 10)
copy(slice6, slice4)
slice6[0] = 114514
fmt.Println(slice4)
fmt.Println(slice6)

}

map

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
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
)

func changemap(m map[int]string) {
for k, _ := range m {
m[k] = "Randolfluo"
}

}

func main() {
//方式一
var map1 map[int]string //声明空map
map1 = make(map[int]string) //分配空间
map1[0] = "hello"
fmt.Println(map1)

//方式二
map2 := make(map[int]string)
map2[0] = "Randolfluo"
fmt.Println(map2)

//方式三
map3 := map[int]string{
0: "Randolfluo",
1: "Hatsune Miku",
}
fmt.Println(map3)

map3[2] = "114514" //添加
fmt.Println(map3)

delete(map3, 0) //删除
fmt.Println(map3)

map3[2] = "1919810" //修改
fmt.Println(map3)

//map传参属于浅拷贝,引用传递
changemap(map3)
fmt.Println(map3)
}

type

  • 定于的类型可以基于已有的类型,但它被视为一个完全不同的类型。
  • 可以与struct、interface等关键词一起使用定义结构体

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
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
)

type MyInt int //自定义类型

type Person struct {
name string
age int
}

func changePerson1(p Person) {
p.name = "Hatsune Miku"
p.age = 16
}

//传入指针
func changePerson2(p *Person) {
p.name = "Hatsune Miku"
p.age = 16
}

func main() {
var a MyInt = 1
fmt.Printf("a = %T\n", a)
//变量 a 的类型是 main.MyInt,其中 main 是包名,MyInt 是类型名。

//初始化
var p Person
p.age = 20
p.name = "Randolfluo"
fmt.Println(p)

//默认深拷贝
changePerson1(p)
fmt.Println(p)

//可以传递指针进行浅拷贝,函数修改会影响调用者的数据
changePerson2(&p) //传入地址
fmt.Println(p)

}

面向对象

封装

定义一个结构体存储数据(),然后通过公开的方法传入结构体(指针)来访问修改这些字段。

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

import "fmt"

// 大写表示该类其他包可以访问
type Person struct {
//如果首字母为大写,则该属性是公有的,否则为私有
name string
age int
}

// 方法大写表示该方法能被其他包访问如,fmt.Println
// 若不加指针,表示传入参数的副本,修改无效
func (p Person) Get() (name string, age int) {
age = p.age
name = p.name
return
}

// 若加指针,表示传入参数的地址,修改有效
func (p *Person) Set(name string, age int) {
p.name = name
p.age = age
}

func main() {
p := Person{name: "tom", age: 18}
name, age := p.Get() //get方法不修改数据,可以传入副本
fmt.Println(name, age)
p.Set("jack", 20) //set方法修改数据,必须传入地址
name, age = p.Get()
fmt.Println(name, age)
}

继承

组合:组合是一种将一个类型嵌入到另一个类型中的方法,这样内嵌的类型就可以被外部类型使用。

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
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

// 大写表示该类其他包可以访问
type Person struct {
//如果首字母为大写,则该属性是公有的,否则为私有
name string
age int
}

func (p *Person) showinfo() (name string, age int) {
name = p.name
age = p.age
return
}

type Student struct {
Person //继承父类方法
school string
}

// 子类重写父类方法
func (s *Student) showinfo() (name string, age int, school string) {
name = s.name
age = s.age
school = s.school
return
}

func main() {

//写法一
// p := Person{"Randolfluo", 20}
// s := Student{p, "zjxu"}

//写法二
// s := Student{Person{"Randolfluo", 20}, "zjxu"}

//写法三
var s Student
s.name = "Randolfluo"
s.age = 20
s.school = "zjxu"

name, age, school := s.showinfo()
fmt.Println(name, age, school)
}

多态

鸭子类型:一个对象是否符合某个接口,不是由它的类型决定,而是由它实现了哪些方法决定。一个类型只要实现了接口中定义的所有方法,那么这个类型的对象就可以赋值给这个接口类型的变量,而无需显式地声明实现了这个接口。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import "fmt"

// 定义 Animal 接口
type Animal interface {
Sleep()
GetColor() string
Fly()
}

// 实现 Sleep 方法
func (c *Cat) Sleep() {
fmt.Println("Cat sleep")
}

func (b *Bird) Sleep() {
fmt.Println("Bird sleep")
}

// 实现 Fly 方法
func (c *Cat) Fly() {
fmt.Println("Cat can't fly")
}

func (b *Bird) Fly() {
fmt.Println("Bird flies")
}

// 定义 Cat 结构体
type Cat struct {
Color string
}

// GetColor 方法返回 Cat 的颜色
func (c *Cat) GetColor() string {
return c.Color
}

// 定义 Bird 结构体
type Bird struct {
Color string
}

// GetColor 方法返回 Bird 的颜色
func (b *Bird) GetColor() string {
return b.Color
}

// 不能接收指针
func showAnimal(a Animal) {
a.Sleep()
fmt.Println("Animal color:", a.GetColor())
a.Fly()

}

func main() {
// // 创建 Cat 实例并调用方法
// cat := &Cat{Color: "Black"}
// fmt.Println("Cat color:", cat.GetColor())
// cat.Sleep()
// cat.Fly()

// // 创建 Bird 实例并调用方法
// bird := &Bird{Color: "White"}
// fmt.Println("Bird color:", bird.GetColor())
// bird.Sleep()
// bird.Fly()

// 定义一个 Animal 接口类型的变量 an
var an Animal //定义接口指针
an = &Cat{"Black"} //指向cat实例
showAnimal(an)

// 直接创建 Cat 和 Bird 实例
cat := Cat{Color: "Black"}
bird := Bird{Color: "White"}
showAnimal(&cat)
showAnimal(&bird)

}

interface空接口

interface{} 是一个空接口,它表示没有任何方法。由于任何类型都至少实现了零个方法,因此任何类型都实现了空接口 interface{}。这意味着 interface{} 可以被任何类型的值赋值。

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

import "fmt"

func Isstring(a interface{}) {
value, ok := a.(string) //类型断言
if ok {
fmt.Println(value, " is string")
} else {
fmt.Println(a, " is not string")
}
}

func main() {
Isstring(123)
Isstring("Randolfluo")
}

在Go 1.18及更高版本中,推荐使用 any 来代替 interface{},特别是在使用泛型时。

pair

  • 在Go中,接口变量实际上是一个包含两个元素的元组(tuple):一个类型描述符和一个指向具体数据的指针

  • (interface value) = (type, value)

  • 类型断言(type assertion)是用于将接口类型转换为具体类型或另一个接口类型的一个机制。
  • 这里的断言实质是类型转换,当pair类型相同时,可以进行转换
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 main

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

func main() {

// 打开 /dev/tty 设备文件,以读写模式打开
// tty: pair<type:*os.File, value:"/dev/tty"文件描述符>
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
fmt.Println("Error opening /dev/tty:", err)
return // 如果打开失败,打印错误信息并退出
}

// 定义一个 io.Reader 接口类型的变量 r,并将其赋值为 tty
var r io.Reader
//r: pair<type:*os.File, value:"/dev/tty"文件描述符>
r = tty // tty 实现了 io.Reader 接口,因此可以赋值给 r

// 定义一个 io.Writer 接口类型的变量 w,并将其赋值为 tty
var w io.Writer
// 使用 io.Writer 接口的 Write 方法向 tty 写入数据
//w: pair<type:*os.File, value:"/dev/tty"文件描述符>
w = r.(io.Writer) // 类型断言,将 r 转换为 io.Writer 接口

w.Write([]byte("Hello, World!\n"))

// 关闭 tty 文件
err = tty.Close()
if err != nil {
fmt.Println("Error closing /dev/tty:", err)
}
}

reflect

Go语言的反射机制允许程序在运行时检查对象的类型和值

  • 获取类型信息:可以获取任意值的类型(reflect.Type)。

  • 获取值信息:可以获取和设置任意值的值(reflect.Value)。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
"reflect"
)

// Person 定义了一个包含 Name 和 Age 字段的结构体
type Person struct {
Name string // 姓名
Age int // 年龄
}

// String 方法返回 Person 结构体的字符串表示
func (p Person) String() string {
return fmt.Sprintf("Person: %s %d", p.Name, p.Age)
}

func main() {
// 创建一个 Person 实例
user := Person{
Name: "Randolfluo",
Age: 20,
}

// 调用 DoFieldAndMethod 函数来处理 user 实例
DoFieldAndMethod(user)
}

// DoFieldAndMethod 函数使用反射打印输入的字段和方法信息
func DoFieldAndMethod(input interface{}) {

// 获取输入的类型和实际值
inputtype := reflect.TypeOf(input)
inputvalue := reflect.ValueOf(input)

// 打印类型名称和值
fmt.Println("type:", inputtype.Name(), "value:", inputvalue)

// 遍历字段并打印字段名称、类型和值
for i := 0; i < inputtype.NumField(); i++ {
field := inputtype.Field(i)
value := inputvalue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}

// 遍历方法并打印方法名称和签名
for i := 0; i < inputtype.NumMethod(); i++ {
method := inputtype.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}

}

结构体标签

结构体标签可以为结构体的字段添加元数据。标签通常用于编码和解码(如 JSON、XML),验证,数据库映射等。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

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

// Person 结构体,包含 Name 和 Age 字段,并且每个字段都有一个 "info" 标签
type Person struct {
Name string `info:"name"`
Age int `info:"age"`
}

// findTag 函数,使用反射来获取结构体字段的标签信息
func findTag(t interface{}) {
// 获取指针指向的元素类型
tmp := reflect.TypeOf(t).Elem()
// 遍历结构体的字段
for i := 0; i < tmp.NumField(); i++ {
// 获取字段的标签信息
taginfo := tmp.Field(i).Tag.Get("info")
fmt.Printf("%s\n", taginfo)
}
}

// Student 结构体,包含 Name, Age 和 Sex 字段,并且每个字段都有一个 "json" 标签
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Sex []string `json:"sex"`
}

func main() {
// 创建一个 Person 类型的变量 p
var p Person
// 调用 findTag 函数,并传入 p 的指针
findTag(&p) // 传入指针是因为反射需要访问结构体的类型信息,而 t 是 interface{} 类型

// 创建一个 Student 实例,并填充数据
s := Student{
Name: "USA",
Age: 18,
Sex: []string{"male", "female", "tank"},
}

// 将 Student 实例转换为 JSON 字符串
jsonData, err := json.Marshal(s)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(jsonData))

// 创建一个空的 Student 实例 s1
s1 := Student{}

// 将 JSON 字符串转换回 Student 实例
err1 := json.Unmarshal(jsonData, &s1)
if err1 != nil {
fmt.Println("json err:", err1)
}
fmt.Println(s1)
}

Goroutine

  • 协程是轻量级的线程,通常由用户程序通过协程库或语言层面的支持来实现。协程可以在单个线程内实现多任务的并发执行。
  • 即在线程的层面上(用户态),再运行一个调度器进行协程调度
  • 在 Go 中,当主 goroutine(即 main 函数)退出时,所有其他的 goroutine 也会立即退出。
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
39
40
41
package main

import (
"fmt"
"runtime"
"time"
)

// func newTask() {
// i := 0
// for {
// i++
// fmt.Printf("%d ", i)
// time.Sleep(10 * time.Millisecond)
// }
// }

func main() {
defer fmt.Println("main exit")

//go newTask() // 启动一个函数的 goroutine

go func() { // 启动一个匿名函数的 goroutine
i := 0
defer fmt.Println("func exit")
for {
i++
fmt.Printf("%d ", i)
time.Sleep(10 * time.Millisecond)
if i == 500 {
runtime.Goexit() // 退出当前 goroutine
//return // 另一种退出方式
}
}
}()

// 主程序等待 10 秒钟,以便观察 goroutine 的输出
time.Sleep(10 * time.Second)

}

channel

  • channel不需要像文件需要经常关闭,当我们没有数据或显式结束range循环才去关闭channel
  • 关闭channel后无法发送数据,但是可以接收数据
  • 对于nil channel,收发都会被阻塞

channel的基本使用

我们可以将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
25
26
27
28
29
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个整型的通道
c := make(chan int)

// 启动一个匿名函数的 goroutine
go func() {
fmt.Print("Randolfluo\n")
// 向通道发送一个值 0,表示子 goroutine 完成,相当于返回值
time.Sleep(2 * time.Second)
c <- 0
}()

// 从通道接收一个值,阻塞直到接收到值
num := <-c

// 检查返回值
if num == 0 {
fmt.Println("func exit with 0")
} else {
fmt.Println("func exit with others")
}
}

有容量的channel

  • 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
25
26
27
package main

import "fmt"


func channel_test(c chan int) {
defer close(c) // 在函数结束时关闭通道
for i := 0; i < 10; i++ {
c <- i
}
}

func main() {
// 创建一个缓冲容量为 3 的整型通道
c := make(chan int, 3)


go channel_test(c)

fmt.Println("len cap i")
for i := 0; i < 10; i++ {
// 从通道接收数据,并打印通道的当前长度、容量和接收的值
fmt.Println(len(c), " ", cap(c), " ", <-c)
} //每次输出都不同,但是最大容量不会大于cap
}


range & select

  • range方便我们遍历
  • select提供多路复用
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
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"fmt"
"runtime"
"time"
)

func newTask(c, q chan int) {
i := 0
for {
i++
c <- i
time.Sleep(100 * time.Millisecond)

if i == 20 {
close(c) // 关闭通道 c,停止 range 循环
q <- 1 // 向通道 q 发送退出信号
close(q) // 关闭通道 q,表示 不再向q写入数据
runtime.Goexit() // 退出当前 goroutine
}
}
}

func main() {
c := make(chan int) // 创建一个通道 c,用于接收整数
q := make(chan int) // 创建一个通道 q,用于接收退出信号

go newTask(c, q)

label:
for {
select {
case data := <-c:
// 从通道 c 接收到整数
// case data, ok := <-c:
// if !ok {
// break label //可以通过此方式跳出for循环
// }
fmt.Print(data, " ")

case ret := <-q:
// 从通道 q 接收到退出信号
fmt.Println("\nrange exit with code", ret)
break label // 使用标签跳出 for 循环
}
}

fmt.Println("\nmain exit")
}