Go 语言基础与实践

Golang 基本使用。

一、统一思想:12 因素应用宣言

在深入学习 Go 语言之前,我们先来了解一下构建云原生应用的指导原则——12 因素应用宣言(The Twelve-Factor App)。这套方法论由 Heroku 平台的开发者总结,旨在帮助开发者构建可扩展、可维护、易部署的云原生应用。

  1. 基准代码 (Codebase):一份基准代码,多份部署。使用版本控制系统(如 Git)管理代码,确保不同环境(开发、测试、生产)使用同一份代码的不同版本。

  2. 依赖 (Dependencies):显式声明依赖关系。使用依赖管理工具(如 Go Modules)管理项目依赖,确保依赖的明确性和可重复性。

  3. 配置 (Config):在环境中存储配置。将配置信息(如数据库连接、API 密钥)存储在环境变量中,而不是硬编码在代码中,提高应用的可移植性和安全性。

  4. 后端服务 (Backing Services):把后端服务当作附加资源。将数据库、消息队列、缓存等后端服务视为可插拔的资源,通过 URL 或配置信息进行连接,方便应用的迁移和扩展。

  5. 构建,发布,运行 (Build, Release, Run):严格分离构建和运行。将应用构建、发布和运行三个阶段分离,确保每个阶段的独立性和可重复性。

  6. 进程 (Processes):以一个或多个无状态进程运行应用。将应用设计为无状态进程,方便水平扩展和故障恢复。

  7. 端口绑定 (Port Binding):通过端口绑定提供服务。应用通过绑定端口对外提供服务,不依赖于特定的 Web 服务器或容器。

  8. 并发 (Concurrency):通过进程模型进行扩展。利用进程或线程模型实现应用的并发,提高应用的吞吐量和响应速度。

  9. 易处理 (Disposability):快速启动和优雅终止可最大化健壮性。应用应能够快速启动和优雅终止,方便部署、重启和故障恢复。

  10. 开发环境与线上环境等价 (Dev/Prod Parity):尽可能地保持开发、预发布、线上环境相同。使用相同的操作系统、依赖和配置,减少环境差异导致的问题。

  11. 日志 (Logs):把日志当作事件流。将应用的日志输出到标准输出(stdout),由外部系统(如日志收集器)进行处理和分析。

  12. 管理进程 (Admin Processes):后台管理任务当作一次性进程运行。将后台管理任务(如数据库迁移、数据备份)作为一次性进程运行,与应用的主进程分离。

面试知识点:

  • 什么是 12 因素应用宣言?它的核心原则是什么?
  • 为什么要在环境中存储配置?这样做有什么好处?
  • 如何理解应用的无状态性?无状态应用有什么优势?

二、Go 语言的诞生与设计哲学

1. 为什么需要 Go 语言?

在 Go 语言出现之前,开发者面临着一些挑战:

  • 硬件发展与软件瓶颈:硬件性能不断提升,但软件开发效率却没有同步提升。
  • 现有语言的不足
    • C/C++ 等原生语言缺乏好的依赖管理,编译速度慢。
    • Java/C# 等语言过于庞大,启动速度慢,内存占用高。
    • 现有语言对并发编程的支持不够友好,难以充分利用多核处理器。

Go 语言的出现,正是为了解决这些问题。

2. Go 语言的设计哲学

Go 语言的设计哲学可以用以下几个关键词概括:

  • Less is exponentially more(少即是多):Go 语言追求简洁,避免过度设计,减少不必要的复杂性。
  • Do Less, Enable More(做更少,成更多):Go 语言提供了一套精简但强大的工具集,让开发者能够更高效地完成工作。
  • 面向工程:Go 语言的设计目标是解决实际工程问题,而不是追求学术上的完美。
  • 正交性:Go 语言的特性之间相互独立,组合起来却能发挥强大的威力。

3. Go 语言的主要特性

  • 编译型语言:Go 语言是一种编译型语言,可以将代码编译成机器码,执行效率高。
  • 静态类型:Go 语言是一种静态类型语言,在编译时进行类型检查,可以减少运行时错误。
  • 垃圾回收:Go 语言内置垃圾回收机制,开发者无需手动管理内存,降低了开发难度。
  • 并发编程:Go 语言通过 goroutine 和 channel 提供了强大的并发编程支持,可以轻松编写高并发程序。
  • 简洁的语法:Go 语言的语法简洁明了,易于学习和使用。
  • 丰富的标准库:Go 语言提供了丰富的标准库,涵盖了网络编程、系统编程、数据处理等多个领域。

4. Go 语言不支持的特性

为了保持语言的简洁性和一致性,Go 语言有意不支持一些常见的特性:

  • 函数重载和操作符重载:避免代码的歧义和复杂性。
  • 隐式类型转换:减少潜在的错误和不确定性。
  • 继承:Go 语言使用组合来实现代码复用,而不是继承。
  • 异常处理:Go 语言使用显式的错误处理机制(error),而不是异常。
  • 断言:Go 语言鼓励开发者编写更健壮的代码,而不是依赖断言来捕获错误。
  • 静态变量:Go 语言不支持静态变量,避免全局状态带来的问题。

面试知识点:

  • Go 语言的设计目标是什么?它解决了哪些问题?
  • Go 语言有哪些主要的特性?这些特性有什么优势?
  • 为什么 Go 语言不支持某些常见的特性(如继承、异常)?

三、Go 语言环境搭建与基础

1. 下载与安装

2. 环境变量配置

  • GOROOT:Go 语言的安装目录。
  • GOPATH:Go 语言的工作目录,用于存放项目代码、依赖包和可执行文件。
    • src:存放项目源代码。
    • pkg:存放编译后的包文件。
    • bin:存放可执行文件。
  • GOOS:目标操作系统(如 linux、windows、darwin)。
  • GOARCH:目标处理器架构(如 amd64、arm64)。
  • GOPROXY:Go 模块代理,用于加速依赖包的下载。国内用户建议设置为 https://goproxy.cn

3. IDE 设置

  • 推荐使用 VS Code,并安装 Go 插件。
  • 其他可选的 IDE 包括:
    • Goland(JetBrains 出品,收费)
    • Vim、Sublime Text 等(需要配置相关插件)

4. 常用命令

  • go build:编译 Go 程序。
    • -o:指定输出文件名。
    • GOOSGOARCH 环境变量可以用于交叉编译。
  • go run:编译并运行 Go 程序。
  • go test:运行测试。
    • ./...:运行当前目录及子目录下的所有测试。
    • -v:显示详细的测试输出。
  • go vet:静态代码检查,发现潜在的错误。
  • go fmt:格式化 Go 代码。
  • go get:下载并安装依赖包。
  • go mod:Go 模块管理工具。
  • go doc:查看文档。
  • go env:查看 Go 环境变量。

5. 代码版本控制

  • 推荐使用 Git 进行代码版本控制。
  • 将代码托管到 GitHub、GitLab 等平台。

6. Golang Playground

面试知识点:

  • go buildgo run 有什么区别?
  • go vet 可以检查出哪些类型的错误?
  • 如何使用 Go Modules 管理项目依赖?
  • 如何进行交叉编译?

四、Go 语言控制结构

1. if 语句

1
2
3
4
5
6
7
8
9
10
11
12
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}

// 简短语句
if v := x - 100; v < 0 {
return v
}

2. switch 语句

1
2
3
4
5
6
7
8
9
10
switch var1 {
case val1:
// 空分支
case val2:
fallthrough // 执行 case3 中的代码
case val3:
f()
default:
// 默认分支
}

3. for 循环

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
// 计数器循环
for i := 0; i < 10; i++ {
sum += i
}

// while 循环
for sum < 1000 {
sum += sum
}

// 无限循环
for {
if condition {
break
}
}

// for-range 循环
for index, char := range myString {
// ...
}
for key, value := range myMap {
// ...
}
for index, value := range myArray {
// ...
}

面试知识点:

  • Go 语言中如何实现类似于 while 循环的功能?
  • for-range 循环遍历不同类型的数据时,有哪些需要注意的地方?

五、Go 语言常用数据结构

1. 变量与常量

  • 常量:使用 const 关键字定义。
  • 变量:使用 var 关键字定义。
1
2
const Pi = 3.14
var name string = "Go"

2. 变量定义与初始化

  • 变量声明var identifier type
  • 变量初始化var i, j int = 1, 2
  • 短变量声明c, python, java := true, false, "no!" (只能在函数内部使用)

3. 类型转换与推导

  • 类型转换T(v) 将值 v 转换为类型 T
  • 类型推导:在声明变量时不指定类型,Go 编译器会根据右值的类型自动推导。

4. 数组

  • 定义var identifier [len]type
  • 特点:相同类型、长度固定、连续内存。
1
myArray := [3]int{1, 2, 3}

5. 切片 (slice)

  • 定义var identifier []type
  • 特点:对数组的引用、动态长度、连续内存。
  • 常用方法
    • append:追加元素。
    • make:创建切片。
    • 切片操作:myArray[1:3]
1
2
mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4)

6. make 和 new

  • new:返回指针地址。
  • make:返回第一个元素,可预设内存空间。
1
2
3
mySlice1 := new([]int) // 返回 *[]int
mySlice2 := make([]int, 0) // 返回 []int,长度为 0
mySlice3 := make([]int, 10, 20) // 返回 []int,长度为 10,容量为 20

7. Map

  • 声明var map1 map[keytype]valuetype
  • 特点:键值对、无序。
  • 常用方法
    • make:创建 Map。
    • delete:删除键值对。
1
2
3
myMap := make(map[string]string)
myMap["a"] = "b"
delete(myMap, "a")

8. 访问 Map 元素

1
2
3
4
5
6
7
8
9
10
// 按 Key 取值
value, exists := myMap["a"]
if exists {
println(value)
}

// 遍历 Map
for k, v := range myMap {
println(k, v)
}

9. 结构体和指针

1
2
3
4
5
6
7
8
9
10
11
12
type MyType struct {
Name string
}

func printMyType(t *MyType) {
println(t.Name)
}

func main() {
t := MyType{Name: "test"}
printMyType(&t) // 传入指针
}
  • 通过 type … struct 关键字自定义结构体
  • Go 语言支持指针,但不支持指针运算
  • 指针变量的值为内存地址
  • 未赋值的指针为 nil

10. 结构体标签

1
2
3
4
5
6
7
8
9
10
11
type MyType struct {
Name string `json:"name"`
}

func main() {
mt := MyType{Name: "test"}
myType := reflect.TypeOf(mt)
name := myType.Field(0)
tag := name.Tag.Get("json")
println(tag)
}
  • 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag)
  • 使用场景:Kubernetes APIServer 对所有资源的定义都用 Json tag 和 protoBuff tag
  • NodeName string json:"nodeName,omitempty" protobuf:"bytes,10,opt,name=nodeName"

11. 类型别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Service Type string describes ingress methods for a service
type ServiceType string

const (
// ServiceTypeClusterIP means a service will only be accessible inside the
// cluster, via the ClusterIP.
ServiceTypeClusterIP ServiceType = "ClusterIP"

// ServiceTypeNodePort means a service will be exposed on one port of
// every node, in addition to 'ClusterIP' type.
ServiceTypeNodePort ServiceType = "NodePort"

// ServiceTypeLoadBalancer means a service will be exposed via an
// external load balancer (if the cloud provider supports it), in addition
// to 'NodePort' type.
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"

// ServiceTypeExternalName means a service consists of only a reference to
// an external name that kubedns or equivalent will return as a CNAME
// record, with no exposing or proxying of any pods involved.
ServiceTypeExternalName ServiceType = "ExternalName"
)

面试知识点:

  • 数组和切片有什么区别?
  • makenew 有什么区别?
  • 如何判断一个 Map 中是否存在某个键?
  • 结构体标签有什么作用?

课后练习 1.1

  • 安装 Go
  • 安装 IDE 并安装 Go 语言插件
  • 编写一个小程序

给定一个字符串数组
["I","am","stupid","and","weak"]
for 循环遍历该数组并修改为
["I","am","smart","and","strong"]

答案:

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() {
words := []string{"I", "am", "stupid", "and", "weak"}
replacements := map[string]string{
"stupid": "smart",
"weak": "strong",
}

for i, word := range words {
if replacement, ok := replacements[word]; ok {
words[i] = replacement
}
}

fmt.Println(words) // 输出 ["I", "am", "smart", "and", "strong"]
}

六、Go 语言函数

1. main 函数

  • 每个 Go 程序都应该有一个 main 包。
  • main 包里的 main 函数是程序的入口。
1
2
3
4
5
package main

func main() {
println("Hello, world!")
}

2. 参数解析

  • main 函数没有参数,不同于其他语言的[]string args
  • 可以使用 os.Args 获取命令行参数。
1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"os"
)

func main() {
args := os.Args
fmt.Println("Arguments:", args)
}
  • 可以使用 flag 包解析命令行参数。
1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"flag"
"fmt"
)

func main() {
name := flag.String("name", "world", "specify the name you want to say hi")
flag.Parse()
fmt.Println("Hello,", *name)
}

3. init 函数

  • init 函数会在包初始化时自动执行。
  • 谨慎使用 init 函数,避免循环依赖和不可重复运行的问题。
1
2
3
4
5
6
7
package main

var myVariable = 0

func init() {
myVariable = 1
}

4. 返回值

  • 多值返回:函数可以返回多个值。
  • 命名返回值:可以给返回值命名,并在函数体中直接使用。
1
2
3
4
5
6
7
8
9
func swap(x, y string) (string, string) {
return y, x
}

func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 裸返回,返回已命名的 x 和 y
}

5. 调用者忽略部分返回值

1
result, _ = strconv.Atoi(origStr) // 忽略错误返回值

6. 传递变长参数

1
func append(slice []Type, elems ...Type) []Type // 接收任意多个 Type 类型的参数

7. 内置函数

函数名 作用
close 管道关闭
len, cap 返回数组、切片、Map 的长度或容量
new, make 内存分配
copy, append 操作切片
panic, recover 错误处理
print, println 打印
complex, real, imag 操作复数

8. 回调函数 (Callback)

  • 将函数作为参数传递给其他函数,并在其他函数内部调用执行。
1
2
3
4
5
6
7
8
9
10
11
func doOperation(y int, f func(int, int)) {
f(y, 1)
}

func increase(a, b int) {
println("increase result is:", a+b)
}

func main() {
doOperation(1, increase)
}

9. 闭包

  • 匿名函数,可以访问其外部作用域的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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),
)
}
}

10. 方法

  • 作用在接收者上的函数。
1
2
3
4
5
6
7
8
9
10
11
12
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())
}

11. 传值还是传指针

  • Go 语言只有一种规则-传值
  • 函数内修改参数的值不会影响函数外原始变量的值
  • 可以传递指针参数将变量地址传递给调用函数,Go 语言会
    复制该指针作为函数内的地址,但指向同一地址
  • 思考:当我们写代码的时候,函数的参数传递应该用struct
    还是pointer
    • 如果需要修改参数的值,或者参数较大,传递指针更高效。
    • 如果不需要修改参数的值,且参数较小,传递值更安全。

12. 接口

  • 接口定义一组方法集合。
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 Abser interface {
Abs() float64
}

type MyFloat float64

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

type Vertex struct {
X, Y float64
}

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

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

a = f // MyFloat 实现了 Abser
a = &v // *Vertex 实现了 Abser

// a = v // 错误:Vertex 没有实现 Abser(Abs 方法的接收者是 *Vertex)

fmt.Println(a.Abs())
}

13. 注意事项

  • Interface 是可能为 nil 的,所以针对 interface 的使用一定要预先判空,否则会引起程序 crash(nil panic)
  • Struct 初始化意味着空间分配,对 struct 的引用不会出现空指针

14. 反射机制

  • reflect.TypeOf() 返回被检查对象的类型
  • reflect.ValueOf() 返回被检查对象的值
1
2
3
4
5
6
myMap := make(map[string]string, 10)
myMap["a"] = "b"
t := reflect.TypeOf(myMap)
fmt.Println("type:", t)
v := reflect.ValueOf(myMap)
fmt.Println("value:", v)

15. 基于 struct 的反射

1
2
3
4
5
6
7
8
9
10
11
12
13
// struct
myStruct := T{A: "a"}
v1 := reflect.ValueOf(myStruct)
for i := 0; i < v1.NumField(); i++ {
fmt.Printf("Field %d: %v\n", i, v1.Field(i))
}
for i := 0; i < v1.NumMethod(); i++ {
fmt.Printf("Method %d: %v\n", i, v1.Method(i))
}

// 需要注意receive是struct还是指针
result := v1.Method(0).Call(nil)
fmt.Println("result:", result)

16. Go 语言中的面向对象编程

  • 可见性控制
    • public-常量、变量、类型、接口、结构、函数等的名称大写
    • private -非大写就只能在包内使用
  • 继承
    • 通过组合实现,内嵌一个或多个struct
  • 多态
    • 通过接口实现,通过接口定义方法集,编写多套实现

17. Json 编解码

  • Unmarshal: 从 string 转换至 struct
1
2
3
4
5
6
7
8
func unmarshal2Struct(humanStr string) Human {
h := Human{}
err := json.Unmarshal([]byte(humanStr), &h)
if err != nil {
println(err)
}
return h
}
  • Marshal: 从 struct 转换至 string
1
2
3
4
5
6
7
8
func marshal2JsonString(h Human) string {
h.Age = 30
updatedBytes, err := json.Marshal(&h)
if err != nil {
println(err)
}
return string(updatedBytes)
}
  • json 包使用 map[string]interface{}[]interface{} 类型保存任意对象
  • 可通过如下逻辑解析任意 json
1
2
3
4
5
6
7
8
9
10
11
12
13
var obj interface{}
err := json.Unmarshal([]byte(humanStr), &obj)
objMap, ok := obj.(map[string]interface{})
for k, v := range objMap {
switch value := v.(type) {
case string:
fmt.Printf("type of %s is string, value is %v\n", k, value)
case interface{}:
fmt.Printf("type of %s is interface{}, value is %v\n", k, value)
default:
fmt.Printf("type of %s is wrong, value is %v\n", k, value)
}
}

面试知识点:

  • Go 语言中如何实现函数的重载?
  • 什么是闭包?闭包有什么作用?
  • Go 语言中的方法和普通函数有什么区别?
  • Go 语言中如何实现接口?接口和抽象类有什么区别?
  • 什么是反射?反射有什么作用?
  • 如何使用 encoding/json 包进行 JSON 编解码?

六、常用语法

1. 错误处理

Go 语言无内置 exceptio 机制,只提供 error 接口供定义错误

1
2
3
type error interface {
Error() string
}
  • 可通过 errors.Newfmt.Errorf 创建新的 error
  • var errNotFound error = errors.New("NotFound")
  • 通常应用程序对 error 的处理大部分是判断 error 是否为 nil
    如需将 error 归类,通常交给应用程序自定义,比如 kubernetes 自定义了与 apiserver 交互的不同类型错误
1
2
3
4
5
6
7
8
9
10
type StatusError struct {
ErrStatus metav1.Status
}

var _ error = &StatusError{}

// Error implements the Error interface.
func (e *StatusError) Error() string {
return e.ErrStatus.Message
}

2. defer

  • 函数返回之前执行某个语句或函数
  • 等同于Java 和C# 的finally
  • 常见的 defer 使用场景:记得关闭你打开的资源
    • defer file.Close()
    • defer mu.Unlock()
    • defer println("")

3. Panic 和 recover

  • panic: 可在系统出现不可恢复错误时主动调用 panic, panic 会使当前线程直接 crash
  • defer: 保证执行并把控制权交还给接收到 panic 的函数调用者
  • recover: 函数从 panic 或 错误场景中恢复
1
2
3
4
5
6
7
defer func() {
fmt.Println("defer func is called")
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic("a panic is triggered")

七. 多线程

1. 并发和并行

  • 并发(concurrency)
    • 两个或多个事件在同一时间间隔发生
  • 并行(parallellism)
    • 两个或者多个事件在同一时刻发生

2. 协程

  • 进程:
    • 分配系统资源(CPU 时间、内存等)基本单位
    • 有独立的内存空间,切换开销大
  • 线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位
    • 同一进程中的多线程共享内存空间,线程切换代价小
    • 多线程通信方便
    • 从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共
      享了地址空间和信号处理函数
  • 协程
    • Go语言中的轻量级线程实现
    • Golang 在runtime、系统调用等多方面对goroutine 调度进行了封装和处理,当遇到长时间执行
      或者进行系统调用时,会主动把当前goroutineCPU (P) 转让出去,让其他goroutine 能被调度
      并执行,也就是Golang 从语言层面支持了协程

3. Communicating Sequential Process

  • CSP
    • 描述两个独立的并发实体通过共享的通讯channel进行通信的并发模型。
  • Go 协程 goroutine
    • 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协
      作式调度。
    • 是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。
  • 通道 channel
    • 类似UnixPipe,用于协程之间通讯和同步。
    • 协程之间虽然解耦,但是它们和Channel有着耦合。

4. 线程和协程的差异

  • 每个 goroutine (协程) 默认占用内存远比 JavaC 的线程少
    • goroutine2KB
    • 线程:8MB
  • 线程/goroutine 切换开销方面,goroutine 远比线程小
    • 线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PCSP…等寄存器的刷新
    • goroutine:只有三个寄存器的值修改-PC / SP / DX.
  • GOMAXPROCS
    • 控制并行线程数量

5. 协程示例

  • 启动新协程:go functionName()
1
2
3
4
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
time.Sleep(time.Second)

6. channel - 多线程通信

  • Channel 是多个协程之间通讯的管道
  • 一端发送数据,一端接收数据
  • 同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争
  • 协调协程的执行顺序
  • 声明方式
    • var identifier chan datatype
  • 操作符<-
  • 示例
1
2
3
4
5
6
ch := make(chan int)
go func() {
fmt.Println("hello from goroutine")
ch <- 0 //数据写入Channel
}()
i := <-ch //从Channel中取数据并赋值

7. 通道缓冲

  • 基于 Channel 的通信是同步的
  • 当缓冲区满时,数据的发送是阻塞的
  • 通过 make 关键字创建通道时可定义缓冲区容量,默认缓冲区容量为 0
  • 下面两个定义的区别?
    • ch:= make(chan int)
    • ch:= make(chan int,1)

8. 遍历通道缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
rand.Seed(time.Now().UnixNano())
n := rand.Intn(10) // n will be between 0 and 10
fmt.Println("putting: ", n)
ch <- n
}
close(ch)
}()
fmt.Println("hello from main")
for v := range ch {
fmt.Println("receiving: ", v)
}

9. 单向通道

  • 只发送通道
    • var sendOnly chan<- int
  • 只接收通道
    • var readOnly <-chan int
  • Istio webhook controller
    • func (w *WebhookCertPatcher) runWebhookController(stopChan <-chan struct{}) {}
  • 如何用: 双向通道转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var c = make(chan int)
go prod(c)
go consume(c)

func prod(ch chan<- int) {
for {
ch <- 1
}
}

func consume(ch <-chan int) {
for {
<-ch
}
}

10. 关闭通道

  • 通道无需每次关闭
  • 关闭的作用是告诉接收者该通道再无新数据发送
  • 只有发送方需要关闭通道
1
2
3
4
5
ch := make(chan int)
defer close(ch)
if v, notClosed := <-ch; notClosed {
fmt.Println(v)
}

11. select

  • 当多个协程同时运行时,可通过 select 轮询多个通道
  • 如果所有通道都阻塞则等待,如定义了 default 则执行 default
  • 如多个通道就绪则随机选择
1
2
3
4
5
6
7
8
select {
case v := <-ch1:
...
case v := <-ch2:
...
default:
...
}

12. 定时器 Timer

  • time.Ticker 以指定的时间间隔重复的向通道 C 发送时间值
  • 使用场景
    • 为协程设定超时时间
1
2
3
4
5
6
7
8
timer := time.NewTimer(time.Second)
select {
// check normal channel
case <-ch:
fmt.Println("received from ch")
case <-timer.C:
fmt.Println("timeout waiting from channel ch")
}

13. 上下文 Context

  • 超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
  • Context 是设置截止日期、同步信号,传递请求相关值的结构体
1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • 用法
    • context.Background
    • context.TODO
    • context.WithDeadline
    • context.WithValue
    • context.WithCancel
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
// 如何停止一个子协程
done := make(chan bool)
go func() {
for {
select {
case <-done:
fmt.Println("done channel is triggerred, exit child go routine")
return
}
}
}()
close(done)

// 基于 Context 停止子协程
// Context 是 Go 语言对 go routine 和 timer 的封装
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go process(ctx, 100*time.Millisecond)
<-ctx.Done()
fmt.Println("main:", ctx.Err())

func process(ctx context.Context, duration time.Duration) {
select {
case <-time.After(duration):
fmt.Println("process successfully")
case <-ctx.Done():
fmt.Println("process cancelled")
}
}
  • 通过 context 包, 可以取消 goroutine 的执行, 或者给 goroutine 设置 Deadline, 超时后 goroutine 会退出。

面试知识点:

  • Go 语言中如何实现并发编程?
  • goroutine 和线程有什么区别?
  • channel 的作用是什么?如何使用 channel 进行协程间通信?
  • 如何实现一个有缓冲的 channel
  • 如何使用 select 语句处理多个 channel
  • 如何使用 context 包取消 goroutine 的执行?

课后练习 1.2

  • 基于 Channel 编写一个简单的单线程生产者消费者模型
    • 队列:
      队列长度 10,队列元素类型为 int
    • 生产者:
      每 1 秒往队列中放入一个类型为 int 的元素,队列满时生产者可以阻塞
    • 消费者:
      每一秒从队列中获取一个元素并打印,队列为空时消费者阻塞

答案:

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

func producer(ch chan<- int) {
i := 0
for {
time.Sleep(1 * time.Second)
ch <- i
fmt.Println("Produced:", i)
i++
}
}

func consumer(ch <-chan int) {
for {
time.Sleep(1 * time.Second)
i := <-ch
fmt.Println("Consumed:", i)
}
}

func main() {
ch := make(chan int, 10) // 缓冲大小为 10 的 channel

go producer(ch)
go consumer(ch)

select {} // 阻塞主 goroutine,防止程序退出
}

八、Go Modules

1. 为什么需要 Go Modules

在 Go 1.11 版本之前,Go 语言的依赖管理一直是一个痛点。GOPATH 模式存在以下问题:

  • 项目必须放在 $GOPATH/src 目录下:限制了项目存放的位置,不够灵活。
  • 没有版本控制:无法指定项目依赖的特定版本,容易出现版本冲突。
  • 无法处理 vendor 依赖:无法将依赖包复制到项目内部,不利于项目的独立性和可移植性。

Go Modules 的出现,解决了这些问题,成为了 Go 语言官方推荐的依赖管理方式。

2. Go Modules 的主要特性

  • 项目可以放在任何位置:不再受限于 $GOPATH/src 目录。
  • 版本控制:可以指定项目依赖的特定版本,解决了版本冲突问题。
  • vendor 支持:可以将依赖包复制到项目内部的 vendor 目录,提高了项目的独立性和可移植性。
  • 模块代理:可以通过设置 GOPROXY 环境变量,使用模块代理加速依赖包的下载。

3. Go Modules 的基本使用

  • 初始化模块:在项目根目录下执行 go mod init <module_name>,创建 go.mod 文件。
  • 添加依赖:执行 go get <package_name>@<version>,会自动更新 go.modgo.sum 文件。
  • 构建项目:执行 go build,会自动下载并构建依赖。
  • 运行项目:执行 go run,会自动下载、构建并运行项目。
  • 整理依赖:执行 go mod tidy,会移除未使用的依赖,并更新 go.modgo.sum 文件。
  • vendor 依赖:执行 go mod vendor,会将依赖包复制到项目内部的 vendor 目录。

面试知识点:

  • Go Modules 解决了 GOPATH 模式的哪些问题?
  • go.modgo.sum 文件有什么作用?
  • 如何使用 Go Modules 添加、更新和删除依赖?
  • 如何使用 vendor 依赖?

九、Go 语言与云原生

Go 语言的特性使其非常适合云原生应用开发:

  • 高效的编译和执行速度:Go 语言的编译速度快,生成的二进制文件小,启动速度快,非常适合云原生环境下的快速部署和弹性伸缩。
  • 强大的并发编程支持:Go 语言的 goroutine 和 channel 机制,可以轻松编写高并发程序,充分利用多核处理器,提高应用的吞吐量和响应速度。
  • 简洁的语法和丰富的标准库:Go 语言的语法简洁易学,标准库功能丰富,可以减少开发者的工作量,提高开发效率。
  • 跨平台编译:Go 语言支持交叉编译,可以方便地为不同的操作系统和处理器架构构建应用。
  • 容器友好:Go 语言生成的二进制文件不依赖于外部库,非常适合打包成 Docker 镜像,方便部署和管理。

Go 语言已经成为云原生领域的主流语言,许多知名的云原生项目都是用 Go 语言开发的,例如:

  • Docker:容器引擎。
  • Kubernetes:容器编排平台。
  • Istio:服务网格。
  • Etcd:分布式键值存储。
  • Prometheus:监控系统。

Go 语言基础与实践
https://mfzzf.github.io/2025/03/16/golang/
作者
Mzzf
发布于
2025年3月16日
许可协议