文章目录
- 2、Go语言
- Go 语言的原则
- 为什么需要 Go 语言
- Go 语言不支持的特性
- Go语言特性衍生来源
- Go语言安装
- 配置Go模块国内代理
- Go的目录介绍
- Go 常用命令
- 基本命令
- Go build
- Go test
- Go vet
- IDE
- File Watcher
- 快捷键
- Go语言的依赖管理
- 依赖管理的概念
- 依赖管理的三个阶段 GOPATH、GOVENDOR、go mod
- GOPATH方式
- GOVENDOR方式
- go mod方式
- 旧的项目迁移到go mod
- main 函数
- 获取输入参数和标志值
- Init 函数
- 变量
- 标准声明
- 批量声明
- 定义及初始化
- 标准
- 多个
- 类型推导
- 短变量声明
- 匿名变量_
- 内建变量类型
- 字符串遍历
- 字符串常用方法
- 强制类型转换
- 常量与枚举
- 常量
- 枚举
- iota
- fmt.Printf格式化输出
- 条件语句
- if else
- switch
- 循环语句
- for
- for range
- 函数 func
- 可变参数列表
- 内置函数
- 指针(变量取地址&、 地址取变量值*)
- 取变量的 指针地址
- 根据指针地址 修改变量的值
- `b := &a`的剖析:
- 引用传递的实现
- 数组Array
- 声明
- 遍历
- 数组是值类型
- 引用传递的实现
- 切片Slice
- 定义 及 从数组中创建切片
- 证明Slice是引用类型
- Slice的扩展
- Slice底层实现
- 向Slice添加元素
- Slice的创建、拷贝、删除
- Slice的遍历
- 切片转字符串
- 集合Map 映射关系
- 创建
- 遍历
- 获取v、判断key是否存在
- 删除元素
- 获取元素个数
- 结构体struct
- 定义
- 实例化
- 自己实现构造函数
- 为结构定义方法(接收者)
- 自定义内置类型
- 封装
- 扩展已有类型的三种方式
- 定义别名
- 使用组合
- 使用内嵌(Embedding)来扩展 (实现类似继承、重载)
- defer
- defer执行时机
- defer经典案例
- 接口interface
- 概念
- 声明
- duck typing概念
- 接口的组合
- 空接口可以被当作为任何类型
- 函数与闭包
- 斐波那契数列的闭包实现
- 为函数实现接口,实现斐波那契数列
- 多态
- 概念
- 为什么要用多态
- 多态有什么好处
- Go语言中的多态
- 反射
- 回调函数(Callback)
- Json编解码
- Unmarshal:从string转换至struct
- Marshal:从 struct 转换至 string
- json 包使用 map[string]interface{} 和 []interface{} 类型保存任意对象
- 错误处理
- Panic 和 recover
- 多线程
- 并发和并行
- 协程
- Go语言的多线程模型:通信顺序处理Communicating Sequential Process
- 线程和协程的差异
- 协程示例
- channel - 多线程通信
- 通道缓冲
- 遍历通道缓冲区,使用for rang方式
- 单向通道
- 关闭通道
- select
- 定时器 Timer
- 上下文 Context
- 如何停止一个子协程
- 基于 Context 停止子协程
- 线程加锁(Sync包)
- 理解线程安全
- 锁
- Mutex示例
- Kubernetes 中的 informer factory
- RWMutex示例
- WaitGroup 示例
- Kubernetes 中的 端对端测试的源码
- Once示例
- Cond示例
- Kubernetes 中的队列,标准的生产者消费者模式
- 线程调度
- Linux内核原理
- Linux 进程的内存使用
- CPU 对内存的访问
- 进程切换开销
- 线程切换开销
- 用户线程
- Goroutine
- MPG的对应关系
- GMP 模型细节
- P的状态
- G的状态
- G的状态转换图
- G 所处的位置
- Goroutine 创建过程
- 将 Goroutine 放到运行队列上
- 调度器行为
- 内存管理
- 关于内存管理的争论
- 堆内存管理
- 堆内存管理的挑战
- ThreadCacheMalloc 概览(TCMalloc)
- Go 语言内存分配
- 内存回收
- mspan
- GC 工作流程
- 三色标记
- 垃圾回收触发机制
- 网络
- 理解网络协议层
- 理解Socket
- 理解 net.http 包
- 阻塞 IO 模型
- 非阻塞 IO 模型
- IO 多路复用
- 异步IO
- Linux epoll
- Go 语言高性能 httpserver 的实现细节
- 调试
- debug
- dlv 的配置
- 更多 debug 方法
- Glog 使用方法示例
- 性能分析(Performance Profiling)
- 分析 CPU 瓶颈
- 其他可用 profiling 工具分析的问题
- 针对 http 服务的 pprof
- 分析 go profiling 结果
- 结果分析示例
2、Go语言
Go 语言的原则
- “Less is exponentially more“ – Rob Pike, Go Designer
- “Do Less, Enable More” – Russ Cox, Go Tech Lead
为什么需要 Go 语言
- 其他编程语言的弊端。
- 硬件发展速度远远超过软件。
- C 语言等原生语言缺乏好的依赖管理 (依赖头文件)。
- Java 和 C++ 等语言过于笨重。
- 系统语言对垃圾回收和并行计算等基础功能缺乏支持。
- 对多核计算机缺乏支持。
- Go 语言是一个可以编译高效,支持高并发的,面向垃圾回收的全新语言。
- 秒级完成大型程序的单节点编译。
- 依赖管理清晰。
- 不支持继承,程序员无需花费精力定义不同类型之间的关系。
- 支持垃圾回收,支持并发执行,支持多线程通讯。
- 对多核计算机支持友好。
Go 语言不支持的特性
- 不支持函数重载和操作符重载
- 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
- 支持接口抽象,不支持继承
- 不支持动态加载代码
- 不支持动态链接库
- 通过 recover 和 panic 来替代异常机制
- 不支持断言
- 不支持静态变量
Go语言特性衍生来源
Go语言安装
- 官网:golang.org
- 国内下载:studygolang.com
- macOS:
brew install go
- 更新Go:
brew upgrade go
配置Go模块国内代理
直接执行下面命令配置Go国内镜像:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
检查配置是否成功:
go env
Go的目录介绍
- GOROOT
- go的安装目录
- GOPATH:
默认目录在unix、linux:~/go
;windows:%USERPROFILE%\go
- src:存放源代码,建议不管用哪种依赖管理模式,都建议把源代码放在此。
- pkg:存放依赖包
- bin:存放可执行文件
Go 常用命令
# 设置全局go的环境变量
go env -w GO111MODULE=on
# 设置命令端临时go的环境变量
export GO111MODULE=on
# 查看go的环境变量
go env
# 查看go的版本
go version
# go mod模式初始化:创建go.mod文件
go mod init <mygomodname>
# 安装go的依赖库(Latest release版本)
go get -u go.uber.org/zap
# 安装go的旧版本依赖库
go get -u go.uber.org/zap@v1.21
# 进行清洁(去掉go.mod文件中项目不需要的依赖,清理go.sum文件中同名依赖库的不同版本为代码使用版本)
go mod tidy
# build当前目录所有的go文件,会把可执行文件放在当前目录下。
go build
# build当前目录及所有子目录下所有的go文件,不会产生可执行文件,只是检查编译是否正确。
go build ./...
# build当前目录及所有子目录下所有的go文件,产生可执行文件,在$GOPATH下的bin目录
go install ./...
# 指定输出目录
go build –o bin/mybinary .
# 编译成linux可执行的二进制文件
GOOS=linux go build
# 代码标准化entry.go文件
go fmt entry.go
基本命令
- bug:start a bug report
- build:compile packages and dependencies
- clean:remove object files and cached files
- doc:show documentation for package or symbol
- env:print Go environment information
- fix:update packages to use new APIs
- fmt:gofmt (reformat) package sources
- generate:generate Go files by processing source
- get:add dependencies to current module and install them
- install:compile and install packages and dependencies
- list:list packages or modules
- mod:module maintenance
- run:compile and run Go program
- test:test packages
- tool:run specified go tool
- version:print Go version
- vet:report likely mistakes in packages
Go build
- Go 语言不支持动态链接,因此编译时会将所有依赖编译进同一个二进制文件。
- 指定输出目录。
- go build –o bin/mybinary .
- 常用环境变量设置编译操作系统和 CPU 架构。是用来交叉编译的。
- 编译成linux可执行的二进制文件:
GOOS=linux go build
GOOS=linux GOARCH=amd64 go build
- 全支持列表。
GOOS
表示操作系统,比如windows、linux等;GOARCH
表示平台比如amd64、386等。
关于支持的操作系统和平台,在$GOROOT/src/go/build/syslist.go
文件中有详细定义。
Go test
Go 语言原生自带测试
import "testing"
func TestIncrease(t *testing.T) {
t.Log("Start testing")
increase(1, 2)
}
go test ./… -v 运行测试
go test 命令扫描所有*_test.go为结尾的文件,惯例是将测试代码与正式代码放在同目录, 如 foo.go 的测试代码一般写在 foo_test.go
Go vet
IDE
File Watcher
可以安装goimports
快捷键
调用func时,自动生成返回值
option+command+v
Go语言的依赖管理
依赖管理的概念
这里的依赖指的是我们引用别人写好的库,比如uber-go/zap,主要用来在 Go 中快速、结构化、分级日志记录。
所以,我们需要把别人的库拉到本地,和我们自己的代码一起build,一起工作。
依赖管理的三个阶段 GOPATH、GOVENDOR、go mod
-
GOPATH
:最早的管理方法,不好用。 -
GOVENDOR
:比GOPATH稍好一点,但是还是不好用。 -
go mod
:在2018年底,也就是Go 1.11版本开始,把对Go的包管理做到了Go的命令里去。使用go mod逐步淘汰GOPATH、GOVENDOR。
这里说明一下:jetbrains的GoLand中创建新项目选择"Go"就是GOPATH方式了。
GOPATH方式
- 所有的依赖都需要到GOPATH目录下去找,会造成所有的项目的依赖都放在GOPATH目录下,GOPATH会越来越大、甚至整个github都镜像到GOPATH下了。
- GOPATH方式对结构目录有要求,名字是固定的。一定要创建src,否则依赖的库将找不到。
- GOPATH模式会去以下目录中找依赖:
- $GOROOT
- $GOPATH/src
- GOPATH只能去git clone github的tag为Latest release版本的zap库,无法做到多个项目用不同版本的zap库。(为了解决此问题,诞生了GOVENDOR)
mkdir /tmp/gopathtest
cd /tmp/gopathtest
go env
# 设置全局go的环境变量(仅限测试使用,这样会把整个系统下的GOPATH都设置为此目录。)
go env -w GOPATH=/tmp/gopathtest
# 临时设置go的GOPATH(推荐测试使用)
export GOPATH=/tmp/gopathtest
mkdir src
# 打开jetbrains的GoLand创建新项目,选择“Go”而不是“Go Modules”。Project Location设置为/tmp/gopathtest/src/project1
# 因为我们是用export方式,所以需要再配置GoLand的GOPATH为/tmp/gopathtest
# 另外,我们还要设置GO111MODULE=off
go get -u go.uber.org/zap # 会clone github的tag为Latest release版本
cd /tmp/gopathtest/src
ls # go.uber.org project1
// 先配置一下jetbrains的GoLand的Run/Debug Configurations的Environment:GO111MODULE=off
package main
import "go.uber.org/zap"
func main(){
log, _ := zap.NewProduction()
log.Warn("warning test")
}
GOVENDOR方式
- 和GOPATH差不多,只是多了一个在项目文件夹内新建一个vendor文件夹,新建完后IDE会认识这个文件夹的。
- 会先去项目内的vendor文件夹去找依赖库,如果找不到在到$GOPATH/src下去找。
- 为了解决我们每次go get命令下载完依赖库后手工拷贝到vendor文件夹内,大量第三方依赖管理工具针对GOVENDOR而诞生:glide、dep、go dep、…
- 一般开发的时候我们不会手动的去动vendor目录,动的都是第三方管理工具的配置文件。例如:glide.yml
mkdir /tmp/govendortest
cd /tmp/govendortest
go env
export GOPATH=/tmp/govendortest
mkdir src
mkdir src/project1/vendor
mkdir src/project2/vendor
# 然后我们只需要把zap的所有文件夹及文件全部放在各自项目文件夹下的vendor文件夹下即可。
# 别忘了设置GO111MODULE=off
// 测试代码与GOPATH相同
go mod方式
- 我们的项目可以建在任何地方,用户只需要专注项目即可,不必关心目录结构。
- 初始化:
go mod init
会创建go.mod文件 - 添加依赖:
go get
- 更新依赖:
go get [@v...] , go mod tidy
- 旧项目迁移到go mod:
go mod init , go build ./...
- 在jetbrains的GoLand中选择创建go modules会自动帮我们创建一个go.mod和go.sum文件
- go.sum是记录库的版本和hash,确保我们拉下来的是正确的,没有被篡改的。
- go.mod文件会被go自动编辑,当我们的代码没有用到某库时,后面会写// indirect ;如果我们使用了该库,则没有// indirect 标记。
- 由go命令统一的管理,依赖库会自动保存在$GOPATH/pkg/mod下
- 依赖的检索逻辑是:
- 先去go.mod文件去查用到的库及库的版本是什么
- 然后去$GOPATH/pkg/mod/go.uber.org/zap@1.21.0去找
- 如果我们要变更使用库的版本,不要手动改go.mod文件,要使用命令的方式:
go get -u go.uber.org/zap@v1.11
- 我们查看go.sum文件发现1.21和1.11都同时存在,我们使用:
go mod tidy
进行清洁(去掉go.mod文件中项目不需要的依赖。),清洁后发现,1.21被删除了。
旧的项目迁移到go mod
# 把当前目录以及所有子目录下所有的go文件全部build一遍,这样在build的时候碰到代码中的import就会触发go mod去拉取库放在本地。
go mod init # 创建go.mod文件
go build ./...
main 函数
Go语言入口就是package main
func main
是固定搭配。
- 每个 Go 语言程序都应该有个 main package
- Main package 里的 main 函数是 Go 语言程序入口
所以,一个项目只允许一个入口,如果是测试用,可以修改IDE的Run kind为file。
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
获取输入参数和标志值
package main
import (
"flag"
"fmt"
"os"
)
func main() {
/*
flag.String()定义了一个带有指定名称、默认值和使用字符串的字符串标志。
返回值是存储标志值的字符串变量的地址。
os.Args保存命令行参数,并以程序名称开头。
*/
name := flag.String("name", "jenrey", "specify the name you want to say hi")
flag.Parse()
fmt.Println("os args is:", os.Args)
fmt.Println("input parameter is:", *name)
fullString := fmt.Sprintf("Hello %s from Go\n", *name)
fmt.Println(fullString)
}
Init 函数
- Init 函数:会在包初始化时运行
- 谨慎使用 init 函数
- 当多个依赖项目引用统一项目,且被引用项目的初始化在 init 中完成,并且不可重复运行时,会导致启动错误
# k8s解析参数的逻辑
kubernetes -> glog -> init() -> flag parse parameter
# k8s引用了项目a,项目a在自己的vendor目录引用了自己的glog
kubernetes -> a -> vendor -> glog -> init() -> flag parse parameter
# 综上,整个项目里glog存在于多个地方,这个时候Go在编译的时候就不认为这两个glog是一个包,所以,就会在第4行中重新做init(),然后重新的flag parse parameter
# 所以,就会发现第4行的flag在之前第2行中被定义过了,重复定义,所以最终整个项目跑不起来了
# 现在的k8s废弃了glog,用的是自己开发的klog提供了自己的log机制
- 一般都是做一些初始化的操作时需要用init函数
- 如果main包引用了a包,而a包又引用了b包,则在main方法执行的时候会先执行b包的init方法,然后再执行a包的init方法,最后执行main包的init方法
package main
import (
"fmt"
_ "jenrey.com/learn-go/a"
_ "jenrey.com/learn-go/b"
)
func init() {
fmt.Println("main init")
}
func main() {
fmt.Println("main")
}
package a
import (
"fmt"
_ "jenrey.com/learn-go/b"
)
func init() {
fmt.Println("init from a")
}
package b
import "fmt"
func init() {
fmt.Println("init from b")
}
# 结果展示
init from b
init from a
main init
main
说明:当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是希望它执行init()函数而已。这个时候就可以使用 import _ 引用该包,当然也就无法通过包名来调用包中的其他函数。
b没有被打印两次是因为init方法只执行一次。
变量
函数外的每个语句都必须以关键字开始(var、const、func等)
标准声明
var a int
var s string
var isOK bool
var d float32
批量声明
var (
a string = "qwe"
b int
c bool = true
d float32
)
定义及初始化
标准
var name string = "Q1mi"
var age int = 18
多个
// 一次初始化多个变量
var name, age = "Q1mi", 20
类型推导
var name = "Q1mi"
var age = 18
短变量声明
:=
不能使用在函数外。
// 短变量声明并初始化只能在函数内部
package main
import fmt
// 包内部变量m(Go语言没有全局变量一说)
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
匿名变量_
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。
// 匿名变量(作用:想要忽略某个值)
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}
内建变量类型
- bool,string
- (u)int,(u)int8,(u)int16,(u)int32,(u)int64,uintptr
- byte,rune
- float32,float64,complex64,complex128
- 特殊类型:error
- 指针类型:*int、*int64、*string、*[10]int等
uint8:无符号8位整型(0到255)或者叫 byte 型,代表了ASCII码的一个字符。
int8:有符号8位整型(-128到127)
int:32位操作系统上就是int32,64位操作系统上就是int64
ptr:就是指针,指针的长度和操作系统相关,64位操作系统就是64位,32位操作系统就是32位。
rune:类似于其他语言的char,就是字符型。当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。rune 类型来处理 Unicode。
complex64、complex128:复数,复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。
package main
import (
"fmt"
"math/cmplx"
)
func main() {
var c1 complex128
c1 = 3 + 4i
var c2 complex128
c2 = 2 + 3i
fmt.Println(cmplx.Abs(c1)) // 5
fmt.Println(cmplx.Abs(c2)) //3.6055512754639896
}
字符串遍历
var s string = "Yes你好啊!"
for i, ch := range []rune(s) {
fmt.Println(i,string(ch))
fmt.Printf("%d %c\n", i, ch)
}
字符串常用方法
import (
"fmt"
"strings"
)
strings.Join()
strings.Split()
strings.Fields()
strings.Contains()
strings.Index()
strings.ToLower()
strings.ToUpper()
strings.Trim()
strings.TrimRight()
strings.TrimLeft()
强制类型转换
Go语言只有强制类型转换,没有隐式类型转换。
int() // 强制转int
float64() // 强制转float64
常量与枚举
常量
变量使用var
定义,常量使用const
定义,常量在定义的时候必须赋值且后期不能被修改。
const filename string = "abc.txt"
const a, b = 3, 4 // 不确定类型,既可以是int也可以是float
枚举
const (
cpp = 0
java = 1
python = 2
golang = 3
)
iota
iota是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota
计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
举个例子:
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
使用_
跳过某些值
const (
n1 = iota //0
n2 //1
_
n4 //3
)
iota
声明中间插队
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
const (
b = 1 << (10 * iota)
kb
mb
gb
tb
pb
)
func main() {
// 1 1024 1048576 1073741824 1099511627776 1125899906842624
fmt.Println(b, kb, mb, gb, tb, pb)
}
fmt.Printf格式化输出
占位符 | 说明 |
%s | 直接输出字符串或[]byte |
%q | 该值用双引号括起来 |
%T | 打印值的类型 |
%v | 相应值的默认格式 |
%d | 表示位十进制 |
条件语句
if else
func bounded(v int) int {
if v > 100 {
return 100
} else if v < 100 {
return 0
} else {
return v
}
}
if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
package main
import (
"fmt"
"io/ioutil"
)
func main() {
const filename = "abc.txt"
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
// 如果文件不存在则提示:open abc.txt: no such file or directory
// 如果文件存在则打印文件内容
}
switch
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
}
// switch后不带表达式
func grade(score int) string {
g := ""
switch {
case score < 0 || score > 100:
panic(fmt.Sprintf("Wrong score:%d", score))
case score < 60:
g = "D"
case score < 80:
g = "C"
case score < 90:
g = "B"
case score <= 100:
g = "A"
}
return g
}
func main() {
fmt.Println(grade(50), grade(70), grade(100))
}
fallthrough
fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func switchDemo() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
// a
// b
循环语句
for
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
package main
import (
"fmt"
"strconv"
)
func convertToBin(n int) string {
/*整数转二进制*/
result := ""
for ; n > 0; n /= 2 {
lsb := n % 2
// strconv.Itoa函数的参数是一个整型数字,它可以将数字转换成对应的字符串类型的数字。
result = strconv.Itoa(lsb) + result
}
return result
}
func main() {
fmt.Println(convertToBin(5), convertToBin(13)) // 101 1101
}
Go语言没有while,可以使用没有初始条件和结束条件的for
func printFile(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
// 死循环
for{
fmt.Println("abc")
}
for range
遍历数组,切片,字符串,Map 等
需要注意:如果 for range 遍历指针数组,则 value 取出的指 针地址为原指针地址的拷贝。
for index, char := range myString {
...
}
for key, value := range MyMap {
...
}
for index, value := range MyArray {
...
}
函数 func
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
Go语言只有值传递
// 返回单值
func eval(a int, b int) int{
p := a + b
return p
}
// 返回多值
func eval(a,b int) (q int, r string){
q = a / b
r = a % b
return
// return a / b, a % b
}
package main
import (
"fmt"
)
func eval(a, b int, op string) (int, error) {
switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
q, _ := div(a, b)
return q, nil
default:
return 0, fmt.Errorf("unsupported operation:%s", op)
}
}
func div(a, b int) (q, r int) {
return a / b, a % b
}
func main() {
// 0 unsupported operation:x
fmt.Println(eval(3, 4, "x"))
q, r := div(13, 3)
// 4 1
fmt.Println(q, r)
// Error: unsupported operation:x
if result, err := eval(3, 4, "x"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(result)
}
}
可变参数列表
func sum(number ...int) int {
/*求可变参数列表的和*/
s:=0
for i := range number {
s+=number[i]
}
return s
}
func main() {
a := sum(1,2,3)
fmt.Println(a)
}
内置函数
内置函数就是不需要加任何的包名、引用名,就是最顶级的函数。
函数名 | 作用 |
close | 管道关闭 |
len, cap | 返回数组、切片,Map 的长度或容量 |
new, make | 内存分配 |
copy, append | 操作切片 |
panic, recover | 错误处理 |
print, println | 打印 |
complex, real, imag | 操作复数 |
指针(变量取地址&、 地址取变量值*)
Go语言指针不能进行偏移和运算。
指针变量的值为内存地址。
未赋值的指针为 nil
Go语言只有值传递,没有引用传递,但是我们可以通过指针地址传递的方式实现类似于引用传递的效果。所以变量传入func的参数中是值,相当于把变量a的值拷贝到func的参数中去,避免了func会改变原变量的值的问题。
Go语言不支持对指针进行操作的,会自动判断x如果是指针类型变量,会找到指针对应的变量。
取变量的 指针地址
&
:取地址
var a int = 2
var pa *int = &a
fmt.Println(pa) // 0xc0000b2008
根据指针地址 修改变量的值
*
:根据地址取值
*pa = 3
fmt.Println(a) // 3
b := &a
的剖析:
引用传递的实现
func swap(a, b *int) {
/*变量的内存地址前后没有变化,也就没有产生新的变量,只是单纯的交换了内存地址空间内的变量值*/
*b, *a = *a, *b
}
func main() {
a, b := 3, 4
swap(&a, &b)
fmt.Println(a, b) // 4 3
}
func swap(a, b int) (int, int) {
println(&a,&b) // 与上面对比,这里产生了新的内存地址
return b, a
}
func main() {
/*a,b变量的内存地址前后没有变化*/
a, b := 3, 4
a, b = swap(a, b)
fmt.Println(a, b) // 4 3
}
数组Array
相同类型且长度固定连续内存片段
必须指定存放的元素类型和容量(长度)。Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。
数组是值类型,也就是值传递,赋值和传参会复制整个数组(ctrl+c、ctrl+v)。因此改变副本的值,不会改变本身的值。
[10]int和[20]int是不同类型。
Go语言一般不直接使用数组,数组指针我们一般也是不用的,而是使用切片来实现。
切片是引用类型。
声明
func main() {
var arr1 [5]int // [0 0 0 0 0]
arr2 := [3]int{3, 1, 5} // [3 1 5]
// 让编译器帮我们数 数组的长度
arr3 := [...]int{2, 4, 6, 8, 10} // [2 4 6 8 10]
// 二维数组,2行3列
var arr4 [2][3]int // [[0 0 0] [0 0 0]]
// 指定索引值
arr5 := [...]int{1: 1, 3: 5} // [0 1 0 5]
fmt.Println(arr1, arr2, arr3, arr4, arr5)
}
遍历
// for循环遍历
arr3 := [...]int{2, 4, 6, 8, 10} // [2 4 6 8 10]
for i := 0; i < len(arr3); i++ {
fmt.Println(arr3[i])
}
// for range遍历
arr3 := [...]int{2, 4, 6, 8, 10} // [2 4 6 8 10]
for i := range arr3 {
fmt.Println(arr3[i])
}
// for range遍历并获取索引和值
arr3 := [...]int{2, 4, 6, 8, 10} // [2 4 6 8 10]
for i, v := range arr3 {
fmt.Println(i, v)
}
数组是值类型
func printArray(arr [5]int) {
arr[1] = 100
fmt.Println(arr) // [0 100 0 0 0]
}
func main() {
var arr1 [5]int
printArray(arr1)
fmt.Println(arr1) // [0 0 0 0 0]
}
引用传递的实现
func printArray(arr *[5]int) {
arr[1] = 100 // 这是是Go的灵活,我们不需要(*arr)[1] = 100
fmt.Println(arr) // &[0 100 0 0 0]
}
func main() {
var arr1 [5]int
fmt.Println(arr1) // [0 0 0 0 0]
printArray(&arr1)
fmt.Println(arr1) // [0 100 0 0 0]
}
// 采用切片Slice的方式
func printArray(arr []int) {
arr[1] = 100
fmt.Println(arr) // [0 100 0 0 0]
}
func main() {
var arr1 [5]int
fmt.Println(arr1) // [0 0 0 0 0]
printArray(arr1[:]) // 数组取切片
fmt.Println(arr1) // [0 100 0 0 0]
}
切片Slice
非常重要。
虽然Go语言所有类型都是值类型,但是Slice肚子里面有数据结构,官方文档说Slice是对Array的一个view视图(arr[2:6]就是我们要看arr数组的索引2到5)
切片是对数组一个连续片段的引用
切片是引用类型。不加数组长度就是Slice。
Slice本身没有数据,是对底层Array的一个view。
切片在未初始化之前默认为nil, 长度为0
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断。切片之间是不能比较的,我们不能使用
==
操作符来判断两个切片是否含有全部相等元素。一个
nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
。切片的容量策略是按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
定义 及 从数组中创建切片
func main() {
// 数组arr
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Printf("%T\n", arr) // [8]int
// 切片s
s := arr[2:6]
fmt.Printf("%T\n", s) // []int
fmt.Println(s) // [2 3 4 5]
}
func main() {
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]
}
证明Slice是引用类型
func updateSlice(s []int) {
s[0] = 100
}
func main() {
// 创建数组
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
// 创建切片
s1 := arr[2:]
fmt.Println(s1) // [2 3 4 5 6 7]
updateSlice(s1)
fmt.Println(s1) // [100 3 4 5 6 7]
fmt.Println(arr) // [0 1 100 3 4 5 6 7]
s2 := arr[:]
fmt.Println(s2) // [0 1 100 3 4 5 6 7]
updateSlice(s2)
fmt.Println(s2) // [100 1 100 3 4 5 6 7]
fmt.Println(arr) // [100 1 100 3 4 5 6 7]
// Reslice(切片再切片)
s2 = s2[:5]
s2 = s2[2:]
fmt.Println(s2) // [100 3 4]
fmt.Println(arr) // [100 1 100 3 4 5 6 7]
}
Slice的扩展
Slice是可以向后扩展,不可以向前扩展。
s[i]不可以超越len(s),向后扩展不可以超过底层数组cap(s)
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
fmt.Println(s1) // [2 3 4 5]
s2 := s1[3:5]
fmt.Println(s1) // [2 3 4 5]
/*panic: runtime error: index out of range [4] with length 4*/
//fmt.Println(s1[4])
fmt.Println(s2) // [5 6]
}
Slice底层实现
s[i]不可以超越len(s),向后扩展不可以超过底层数组cap(s)ptr:指向了Slice开头的那个元素。底层数组的指针。
len:Slice的长度是多少。所以,我们用s1[4]取值是不能取到len下面的方块的值。大于等于len都会报错。
cap:容量(capacity)
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]
fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n", s2, len(s2), cap(s2))
}
// s1=[2 3 4 5], len(s1)=4, cap(s1)=6
// s2=[5 6], len(s2)=2, cap(s2)=3
向Slice添加元素
- 添加元素时如果超越cap,系统会重新分配更大的底层数组。
- 重新分配后,原来的数组元素会拷贝到新分配的底层数组中。
- Go语言有垃圾回收机制,如果原数组有人在用,则不会回收,反之。
- 由于值传递的关系,必须接收append的返回值。
func main() {
// 创建数组
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Printf("arr=%v, len(arr)=%d, cap(arr)=%d\n", arr, len(arr), cap(arr))
// 创建切片
s1 := arr[2:6]
fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n", s1, len(s1), cap(s1))
// Reslice(切片再切片)
s2 := s1[3:5]
fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n", s2, len(s2), cap(s2))
fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n", s1, len(s1), cap(s1))
// s3,s4,s5都是[]int类型
s3 := append(s2, 10) // 把arr的7变成了10
fmt.Printf("s3=%v, len(s3)=%d, cap(s3)=%d\n", s3, len(s3), cap(s3))
fmt.Printf("arr=%v, len(arr)=%d, cap(arr)=%d\n", arr, len(arr), cap(arr))
/*
s4、s5的11、12不是存在Slice里的,因为Slice只是对数组的一个view。
所以s4、s5 view的就不是arr了,view的是一个新的数组(计作newArr)。
Go语言内部会开一个新的newArr,把arr拷贝到newArr里,新的newArr长度会设的更长一些。
因此,添加元素时如果超越cap,系统会重新分配更大的底层数组。
*/
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Printf("s4=%v, len(s4)=%d, cap(s4)=%d\n", s4, len(s4), cap(s4))
fmt.Printf("s5=%v, len(s5)=%d, cap(s5)=%d\n", s5, len(s5), cap(s5))
fmt.Printf("arr=%v, len(arr)=%d, cap(arr)=%d\n", arr, len(arr), cap(arr))
}
arr=[0 1 2 3 4 5 6 7], len(arr)=8, cap(arr)=8
s1=[2 3 4 5], len(s1)=4, cap(s1)=6
s2=[5 6], len(s2)=2, cap(s2)=3
s1=[2 3 4 5], len(s1)=4, cap(s1)=6
s3=[5 6 10], len(s3)=3, cap(s3)=3
arr=[0 1 2 3 4 5 6 10], len(arr)=8, cap(arr)=8
s4=[5 6 10 11], len(s4)=4, cap(s4)=6
s5=[5 6 10 11 12], len(s5)=5, cap(s5)=6
arr=[0 1 2 3 4 5 6 10], len(arr)=8, cap(arr)=8
Slice的创建、拷贝、删除
// 创建切片
func main() {
var s1 []int
s2 := []int{2, 4, 6, 8} // 创建一个arr为[2,4,6,8],然后又建Slice去view这个arr
s3 := make([]int, 8) // 创建长度为8的切片
s4 := make([]int, 4, 12) // 底层数组的长度为32
}
// [], len=0, cap=0
// [2 4 6 8], len=4, cap=4
// [0 0 0 0 0 0 0 0], len=8, cap=8
// [0 0 0 0], len=4, cap=12
// 拷贝切片
func main() {
s1 := []int{2, 4, 6, 8}
s2 := make([]int, 8)
copy(s2, s1)
fmt.Printf("%v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
}
// [2 4 6 8 0 0 0 0], len=8, cap=8
// 删除指定索引
/*
func append(slice []Type, elems ...Type) []Type
接的是一个可变参数,所以我们需要使用...
...是Go语言的语法糖
第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。
第二个用法是slice可以被打散进行传递。
*/
func main() {
s1 := []int{2, 4, 6, 8, 10, 12, 14, 16}
s1 = append(s1[:3], s1[4:]...) // 删除下标为3的元素
fmt.Println(s1) // [2 4 6 10 12 14 16]
}
// 删除头、尾元素
func main() {
s1 := []int{2, 4, 6, 8, 10, 12, 14, 16}
// 删除头
s1 = s1[1:]
fmt.Println(s1) // [4 6 8 10 12 14 16]
// 删除尾
s1 = s1[:len(s1)-1]
fmt.Println(s1) // [4 6 8 10 12 14]
}
Slice的遍历
s2 := []int{1, 2, 3, 4, 5}
for k, v:= range s2{
fmt.Println(k, v)
}
切片转字符串
strings.Join(s, ",")
集合Map 映射关系
Map内的kv是无序的,是hash map。
Map的key:
- Map使用哈希表,必须可以比较相等才可以作为Map的key。
- 除了Slice、Map、Function的内建类型都可以作为Map的key
- Struct类型不包含上述字段,也可以作为Map的key,会在编译阶段检查是否合规。
创建
func main() {
m1 := map[string]int{
"id": 1,
"age": 18,
}
m2 := make(map[string]string) // m2==empty map
var m3 map[string]int // m3==nil
fmt.Println(m1, m2) // map[age:18 id:1] map[] map[]
}
遍历
不保证遍历顺序,多次遍历map的结果可能是不同的,如需顺序,需手动对key排序。(把Map中所有的key拿出来加到Slice,Slice是可以排序的)
for k, v := range m1 {
fmt.Println(k, v)
}
// 使用切片对Map实现伪排序遍历
import (
"fmt"
"sort"
)
func main() {
m1 := map[string]int{
"id": 1,
"age": 18,
}
// 遍历Map m1,每一次遍历打印的结果可能是不同的顺序
for k, v := range m1 {
fmt.Println(k, v)
}
// 创建切片
var keys []string
// 把m1的key加入到切片中
for key := range m1 {
keys = append(keys, key)
}
// 给切片排序,从小到大
sort.Sort(sort.StringSlice(keys))
fmt.Println(keys) // [age id]
// 给切片排序,从大到小
//sort.Sort(sort.Reverse(sort.StringSlice(keys)))
//fmt.Println(keys) // [id age]
//因为切片存储的是m1的所有key(已排序),所以通过遍历切片来打印m1的指定key,这样就保证了遍历Map的顺序是有序的
for key := range keys {
fmt.Println(key, keys[key])
}
}
获取v、判断key是否存在
// k存在
ageValue := m1["age"]
// k不存在,不会报错,获取Value类型的初始值
ageValue := m1["age01"] // 拿到的是空串
// 判断k是否存在
if ageValue, ok := m1["age"]; ok {
fmt.Println(ageValue)
} else {
fmt.Println("key does not exist")
}
删除元素
delete(m1, "age")
获取元素个数
len(m1)
结构体struct
- Go语言仅支持封装,不支持继承和多态
- Go语言没有class,只有struct
- Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称
struct
。 也就是我们可以通过struct
来定义自己的类型了。 - Go语言中通过
struct
来实现面向对象。 - Go语言没有构造函数。
- 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag)
type MyType struct {
Name string `json:"name"`
}
- 使用场景:Kubernetes APIServer 对所有资源的定义都用 Json tag 和 protoBuff tag
- NodeName string
json:"nodeName,omitempty" protobuf:"bytes,10,opt,name=nodeName"
package main
import (
"fmt"
"reflect"
)
type MyType struct {
Name string `json:"name1"`
}
func main() {
mt := MyType{Name: "test"}
myType := reflect.TypeOf(mt) // 返回被检查对象的类型
fmt.Println(myType) // main.MyType
name := myType.Field(0) //返回struct类型的第i个字段
fmt.Println(name) //{Name string json:"name1" 0 [0] false}
tag := name.Tag.Get("json")
println(tag) // name1
}
定义
type person struct {
name, city string
age int
}
实例化
var p1 person
p1.name="张三"
p1.city="北京"
p1.age=18
// 或
p := person{"李四", "天津", 28}
自己实现构造函数
构造函数约定俗成使用new开头
func newPerson(name, city string, age int) *person {
return &person{
name: name,
city: city,
age: age,
}
}
为结构定义方法(接收者)
类似于其他语言的this、self。
只是一个语法上的写法,和正常的func没什么区别。
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 要改变内容必须使用指针接收者
- 结构过大也考虑使用指针接收者
- 一致性:如有指针接收者,最好都是用指针接收者。
// 值接收者
func (p person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
// 指针接收者
func (p *person) setAge(age int) {
// 这里要用指针,因为Go都是值传递。
p.age = age
}
func main() {
p1 := newPerson("小王子", "北京", 25)
p1.Dream() // 小王子的梦想是学好Go语言!
p1.setAge(16)
fmt.Println(p1.name, p1.city, p1.age) // 小王子 北京 16
}
自定义内置类型
其实是“扩展已有类型的三种方式”的第一种定义别名的方式。
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
封装
- 名字一般使用CamelCase
- 针对包级别来说:
- 首字母大写:public,即包外可以引用使用。
- 首字母小写:private,包外不可引用。
- 为结构定义的方法必须放在同一个包内,可以是不同的文件
- 一个文件夹内只能有一个包
- func main()必须要在main包下
扩展已有类型的三种方式
- 定义别名:最简单
- 使用组合:最常用
- 使用内嵌:需要较高代码能力,只有当可以帮我们省下许多代码时我们才使用该方式
定义别名
~/myProject/yys-go
├── app.go
├── go.mod
└── queue
└── q.go
q.go文件代码:
package queue
type Queue []int
func (q *Queue) Push(v int) {
*q = append(*q, v)
}
func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
app.go文件代码:
package main
import (
"fmt"
"yys-go/queue"
)
func main() {
q := queue.Queue{1} // queue是包名
q.Push(2)
q.Push(3)
fmt.Println(q) //[1 2 3]
fmt.Println(q.Pop()) // 1
fmt.Println(q.Pop()) // 2
fmt.Println(q.IsEmpty()) // false
fmt.Println(q.Pop()) // 3
fmt.Println(q.IsEmpty()) // true
}
使用组合
~/myProject/yys-go
├── entry.go
├── go.mod
└── tree
└── node.go
entry.go
package main
import "yys-go/tree"
func main() {
var root tree.Node
root = tree.Node{Value: 3}
root.Left = &tree.Node{}
root.Right = &tree.Node{5, nil, nil}
root.Right.Left = new(tree.Node)
root.Left.Right = tree.CreateNode(2)
root.Right.Left.SetValue(4)
root.Traverse() // 0 2 3 4 5
/*
3
/ \
0 5
/ \ / \
nil 2 4 nil
*/
}
node.go
package tree
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func (node Node) Print() {
fmt.Println(node.Value, " ")
}
func (node *Node) SetValue(value int) {
if node == nil {
fmt.Println("Setting value to nil node. Ignored.")
}
node.Value = value
}
func (node *Node) Traverse() {
if node == nil {
return
}
node.Left.Traverse()
node.Print()
node.Right.Traverse()
}
func CreateNode(value int) *Node {
return &Node{Value: value}
}
下面是扩展功能
entry.go
type myTreeNode struct {
node *tree.Node
}
// 扩充方法
func (myNode *myTreeNode) postOrder() {
if myNode == nil || myNode.node == nil {
return
}
left := myTreeNode{myNode.node.Left}
left.postOrder()
right := myTreeNode{myNode.node.Right}
right.postOrder()
myNode.node.Print()
}
func main() {
var root tree.Node
root = tree.Node{Value: 3}
root.Left = &tree.Node{}
root.Right = &tree.Node{5, nil, nil}
root.Right.Left = new(tree.Node)
root.Left.Right = tree.CreateNode(2)
root.Right.Left.SetValue(4)
root.Traverse() // 0 2 3 4 5
fmt.Println()
myRoot := myTreeNode{&root}
myRoot.postOrder() // 2 0 4 5 3
}
使用内嵌(Embedding)来扩展 (实现类似继承、重载)
entry.go
前段代码可以看“使用组合”的示例,下面为内嵌扩展代码。
type myTreeNode struct {
/*
语法糖,省下代码的量,代码更简洁一些。
其实是有名字的,是"."后面的名字,但是我们不写名字
直接用myTreeNode类型的示例去.即可
相当于tree.Node里面的成员变量和方法都拉出来铺在myTreeNode的结构里面。
*/
*tree.Node // Embedding
}
func (myNode *myTreeNode) postOrder() {
if myNode == nil || myNode.Node == nil {
return
}
left := myTreeNode{myNode.Left}
left.postOrder()
right := myTreeNode{myNode.Right}
right.postOrder()
myNode.Print()
}
// 我们也可以用内嵌的方式实现类似其他语言的重载
func (myNode *myTreeNode) Traverse(){
fmt.Println("This method is shadowed.")
}
func main(){
root := myTreeNode{&tree.Node{Value: 3}}
root.Traverse() // 执行的是23行的myTreeNode类型的Traverse方法
root.Node.Traverse() // 执行的是Node类型的Traverse方法
}
defer
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
start
end
3
2
1
defer执行时机
在Go语言的函数中return
语句在底层并不是原子操作,它分为 给返回值赋值 和 RET指令 两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
defer经典案例
func f1() int {
x := 5
defer func() {
x++ //修改的是x不是返回值
}()
return x // 返回的是x的值,因为1行写的是int
}
func f2() (x int) {
defer func() {
x++
}()
return 5 // 第一步:返回值赋值即x=5 ;第二步:defer即x++,x=6 ; 第三步:返回x
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x // 第一步:y=x=5 ; 第二步:defer修改的是x; 第三步返回y的值;
}
func f4() (x int) {
defer func(x int) {
x++
}(x) // 函数传参改的是副本。
return 5 // 返回值=x=5
}
func main() {
fmt.Println(f1()) // 5
fmt.Println(f2()) // 6
fmt.Println(f3()) // 5
fmt.Println(f4()) // 5
}
接口interface
概念
- 在Go语言中,接口是一种抽象的类型。
interface
是一组method
的集合,是duck-type programming
的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。- 为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
- 在Go语言中,接口是由接口使用者去规范的,接口声明者只负责定义接口及接口需要实现的方法,接口的实现是隐式的,只要我们的类型实现了接口里面的方法,就可以了。
- Go可以使用值接收者接口,也可以使用指针接收者接口。
- 值接收者接口:不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。
- 指针接收者接口:只能接收dog指针类型的变量。
- 要实现接口下全部方法。
- 接口是值传递。
声明
type Sayer interface {
say()
}
type dog struct {}
type cat struct {}
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x,未实现say()方法会报错
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x,未实现say()方法会报错
x.say() // 汪汪汪
}
package main
import "fmt"
/*
1、在代码中定义了两个结构体:Teacher和Student;
2、定义了一个接口:Person,接口中声明了一个方法:notice();
3、在Teacher和Student中都存在notice()方法的实现,并且方法签名与Person中的notice()一致;
4、main包中的全局函数sendMsg(p Person) ,通过输入参数为Person的接口,来调用实现Person接口的notice方法;
5、函数sendMsg(p Person),是一个通过接口实现的多态应用;
*/
type Teacher struct {
Name string
}
type Student struct {
Name string
}
type Person interface {
notice()
}
func (t Teacher) notice() {
fmt.Println(t.Name, "hello")
}
func (s Student) notice() {
fmt.Println(s.Name, "hello")
}
//sendMsg接收一个实现了Person接口的struct
func sendMsg(p Person) {
p.notice()
}
func main() {
t := Teacher{"Teacher Liu"}
s := Student{"Student Li"}
sendMsg(t) // Teacher Liu hello
sendMsg(s) // Student Li hello
}
duck typing概念
- Duck Typing的原话是:“走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就是一只鸭子”。
- 描述事物的外部行为而非内部结构,并不关心这只鸭子是长肉的还是充气的。
- 严格说Go属于结构化类型系统,类似duck typing
python代码
# retriever就是一个Duck Typing的对象,使用者约定好这个retriever会有一个get函数就可以了
# 需要 运行的时候 才知道retriever有没有get方法
# 需要retriever实现get方法,那我们如何知道是否实现了呢?我们就需要注释来说明接口有什么要求来告诉使用者retriever需要实现get方法。
def download(retriever):
return retriever.get("www.baidu.com")
C++代码
/*
C++也能支持Duck Typing,它是通过模板来支持的。
这段代码与python的实现方式类似,这个retriever随便什么类型都可以,只要实现一个get方法,就能通过编译。
那么这种实现方法有什么缺点呢,就是,编译时,才知道传入的retriever有没有get方法。
但它比python好一点了,python是运行时才知道,C++是编译时就知道
同样,这种情况,还是需要注释来说明。
*/
template <class R>
string download(const R& retriever) {
return retriever.get("http://www.baidu.com")
}
java代码
/*
java没有duck typing,下面是类似实现,它同样也用了模板类型;
因为传入的类型是R,就逼着使用者必须实现get方法,解决了python运行时才知道是否实现get方法的缺陷,也解决了需要注释来说明的缺点;
传入的参数必须实现Retriever接口,是强制的。
java不是duck typing,如果download函数只依赖retriever的get方法,而Retriever接口必须要实现除get方法以外的其他方法,那么使用者也必须一同实现,很不灵活,虽然Go也需要实现接口里全部的方法,但Go可以同时实现多个接口的方法。
即如果retriever参数需要同时实现两个或以上的接口方法时,java是没有办法做到的。但go语言可以做到。
*/
<R extends Retriever>
String download(R retriever) {
return r.get("www.baidu.com")
}
Go代码
package main
// 定义Fetcher接口
type Fetcher interface {
Get(url string) string
}
// 定义Saver接口
type Saver interface {
Save(content string)
}
// 定义FetcherAndSaver接口(接口的组合)
type FetcherAndSaver interface {
Fetcher
Saver
}
// 定义download方法
func download(f Fetcher) string {
return f.Get("http://www.baidu.com")
}
// 定义save方法
func save(f Saver) {
f.Save("some thing")
}
// 定义downloadAndSave方法
func downloadAndSave(f FetcherAndSaver) {
content := f.Get("http://www.baidu.com")
f.Save(content)
}
// 定义MyFetcherAndSaver结构体
type MyFetcherAndSaver struct {
}
// MyFetcherAndSaver实现Fetcher接口
func (f MyFetcherAndSaver) Get(url string) string {
...
}
// MyFetcherAndSaver实现Saver接口
func (f MyFetcherAndSaver) Save(content string) {
...
}
func main() {
f := MyFetcherAndSaver{}
/*
因为MyFetcherAndSaver同时实现了Fetcher、Saver两个接口
*/
download(f) // 可以当作Fetcher类型
save(f) // 也可以当作Saver类型
downloadAndSave(f) //当然本就是FetcherAndSaver类型
}
这里定义了三个接口,只要有Get方法的就是Fetcher,只要有Save方法的就是Saver,同时有Get方法和Save方法就是FetcherAndSaver
实现者MyFetcherAndSaver并不需要声明它实现了哪些接口,只要它有相关接口所定义的方法,那么它的实例,就即能作为Fetcher接口来使用,又能作为Saver接口来使用,也能作为FetcherAndSaver接口来使用。
Go的实现方法相对比较灵活,又不失类型检查。总的来说,特点有:
1、既能同时实现多个接口
2、又具有python,C++的Duck Typing灵活性
3、有具有java的类型检查。
接口的组合
type Sayer interface {
say() string
}
type Thinker interface {
think() string
}
type Animal interface {
Sayer
Thinker
behavior() string
}
空接口可以被当作为任何类型
interface{}
// 也可以
var obj interface{}
函数与闭包
- 函数是一等公民:参数、变量、返回值都可以是函数。
- Go语言没有Lambda表达式,但是有匿名函数。
- 匿名函数
- 不能独立存在
- 可以赋值给其他变量
x:=func(){}
- 可以直接调用
func(x,y int){println(x+y)}(1,2)
- 可作为函数返回值
func Add()(func(b int)int)
python代码:
def adder():
sum = 0
def f(value):
nonlocal sum
sum += value
return sum
return f
a = adder() # 接收返回函数f
for i in range(10):
print(a(i))
"""
0
1
3
6
10
15
21
28
36
45
"""
Go代码:
func adder() func(value int) int {
sum := 0 // 称sum为自由变量,可以理解成“全局”变量
return func(value int) int {
sum += value
return sum
}
}
func main() {
/*
0 = 0+0
1 = 0+1
3 = 1+2
6 = 3+3
10 = 6+4
15 = 10+5
21 = 15+6
28 = 21+7
36 = 28+8
45 = 36+9
*/
adder := adder()
for i := 0; i < 10; i++ {
fmt.Println(adder(i))
}
}
// 也可以这么写,更正统一些
type iAdder func(int) (int, iAdder)
func adder2(base int) iAdder {
return func(v int) (int, iAdder) {
return base + v, adder2(base + v)
}
}
func main() {
a := adder2(0)
for i := 0; i < 10; i++ {
var s int
s, a = a(i)
fmt.Println(i, s)
}
}
斐波那契数列的闭包实现
func Fibonacci() func(v int) []int {
s1,s2 := 0,1
var s []int
return func(v int) []int {
for i := 0; i < v; i++ {
s1,s2=s2,s1+s2
s = append(s, s1)
}
return s
}
}
func main() {
f := Fibonacci()
fmt.Println(f(7))
}
为函数实现接口,实现斐波那契数列
import (
"bufio"
"fmt"
"io"
"strings"
)
func Fibonacci() intGen {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
type intGen func() int
// 实现io.Reader接口,就可以用bufio.NewScanner打印出来了
func (g intGen) Read(p []byte) (n int, err error) {
next := g()
s := fmt.Sprintf("%d\n", next)
if next > 100 {
return 0, io.EOF
}
// TODO:incorrect if p is too small!
return strings.NewReader(s).Read(p)
}
func printFileContents(reader io.Reader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
func main() {
f := Fibonacci()
printFileContents(f) // 1 1 2 3 5 8 13 21 34 55 89
}
多态
概念
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。
简单的说:就是用基类的引用指向子类的对象。
为什么要用多态
我们知道,封装可以隐藏实现细节,使得代码模块化;
继承可以扩展已存在的代码模块(类);
它们的目的都是为了“代码重用”。
而多态除了代码的复用性外,还可以解决项目中紧耦合的问题,提供程序的可扩展性。
耦合度讲的是模块和模块之间、代码和代码之间的关联度,通过对系统的分析把它们分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的目的,模块和模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。
多态有什么好处
- 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。 // 继承
- 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 // 多态的真正作用
Go语言中的多态
声明对象时候是抽象的,初始化时候是具体的,当针对这个抽象的对象去调用函数的时候,由程序来识别真正的类型是什么,然后就去找真正类型里面的方法去执行。
总结两点:
- 在Go中,定义一个interface类型,该类型说明了它有哪些方法。使用时,在函数中将该interface类型作为函数的形参,任意一个实现了interface类型的实参都能作为该interface的实例对象。
- Go语言没有继承,只有组合,可通过组合达到“继承”方法的目的。
package main
import "fmt"
type People interface {
getName() string
}
type Human struct {
firstName, lastName string
}
func (h *Human) getName() string {
return h.firstName + "," + h.lastName
}
func main() {
var p1 People // 声明了一个变量p1,类型是People接口类型
p1 = new(Human) // 初始化变量p1时,我们用的是Human类型去初始化的
// 当我们使用变量p1时,本身的类型是People接口类型,Go程序能辨识在初始化的时候是Human,知道在getName()时通过Human的getName方式去获取到名字
name := p1.getName()
fmt.Println(name)
/*
总结,声明对象时候是抽象的,初始化时候是具体的,
当针对这个抽象的对象去调用函数的时候,由程序来识别真正的类型是什么,
然后就去找真正类型里面的方法去执行。
*/
}
反射
reflect.TypeOf()
返回被检查对象的类型reflect.ValueOf()
返回被检查对象的值
package main
import (
"fmt"
"reflect"
)
func main() {
myMap := make(map[string]string, 10)
myMap["a"] = "b"
t := reflect.TypeOf(myMap)
fmt.Println("type:", t) //type: map[string]string
v := reflect.ValueOf(myMap)
fmt.Println("value:", v) //value: map[a:b]
}
回调函数(Callback)
- 函数作为参数传入其它函数,并在其他函数内部调用执行
- strings.IndexFunc(line, unicode.IsSpace)
- Kubernetes controller的leaderelection
package main
func main() {
DoOperation(1, increase) // increase result is: 2
DoOperation(1, decrease) // decrease result is: 0
}
func increase(a, b int) {
println("increase result is:", a+b)
}
func DoOperation(y int, f func(int, int)) {
f(y, 1)
}
func decrease(a, b int) {
println("decrease result is:", a-b)
}
Json编解码
使用标准库encoding/json的Marshal和Unmarshal方法
Unmarshal:从string转换至struct
func unmarshal2Struct(humanStr string) Human {
h := Human{}
err := json.Unmarshal([]byte(humanStr), &h)
if err != nil {
println(err)
}
return h
}
Marshal:从 struct 转换至 string
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
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 语言无内置 exceptio 机制,只提供 error 接口供定义错误
// Go源码中对于error接口是这样写的
type error interface {
Error() string
}
- 可通过 errors.New 或 fmt.Errorf 创建新的 error
var errNotFound error = errors.New("NotFound")
fmt.Println(errNotFound) // NotFound
// fmt.Errorf该函数所做的其实就是先调用 fmt.Sprintf 函数,得到确切的错误信息;再调用 errors.New 函数,得到包含该错误信息的 error 类型值,最后返回该值。
ferrWithCtx := fmt.Errorf("index %v is out of bounds", "1")
fmt.Println(ferrWithCtx) // index 1 is out of bounds
- 通常应用程序对 error 的处理大部分是判断 error 是否为 nil
如需将 error 归类,通常交给应用程序自定义,比如 kubernetes 自定义了与 apiserver 交互的不同类型错误
// 下面为kubernetes的源码,k8s自己定义了名为StatusError的struct
type StatusError struct {
ErrStatus metav1.Status
}
var _ error = &StatusError{}
// StatusError实现了error接口的Error方法
func (e *StatusError) Error() string {
return e.ErrStatus.Message
}
Panic 和 recover
Go语言并不提供try catch的写法。
“在go语言开发中,没有类似try…catch的异常捕获机制,由于go语言中函数或方法都是可以有多个返回值的,所以通常使用的是直接在方法或者函数上对异常进行返回,然后调用这个方法或函数的地方针对返回的异常进行处理。”
通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
在其他语言里,宕机往往以异常的形式存在,底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。
- panic: 可在系统出现不可恢复错误时主动调用 panic,panic 会使当前线程直接 crash
- 如果想退出不执行后续代码程序不要使用
panic("")
,要使用os.Exit(0)
- defer: 保证执行并把控制权交还给接收到 panic 的函数调用者
- recover: 函数从 panic 或 错误场景中恢复
func main() {
defer func() {
fmt.Println("defer func is called")
if err := recover(); err != nil {
fmt.Println("Error:", err)
}
}()
panic("a panic is triggered")
fmt.Println("stop") // 不会执行
}
/*
defer func is called
Error: a panic is triggered
*/
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("error:", err)
}
}()
defer func() {
if err := recover(); err != nil {
fmt.Println("panic1")
panic(err) // 注意这里,按照从下往上的循序进行异常处理,原因的话了解defer
}
}()
fmt.Println("start")
panic("Big Error")
fmt.Println("stop")
}
/*
start
panic1
error: Big Error
*/
多线程
多线程是Go语言最强最亮点的地方!
并发和并行
- 并发(concurrency)
- 两个或多个事件在同一时间间隔发生(交替发生)
- 并行(parallellism)
- 两个或者多个事件在同一时刻发生
协程
- 进程:(可以通过ps命令看到的,
ps -ef
,例如微信app)
- 分配系统资源(CPU 时间、内存等)基本单位
- 有独立的内存空间,切换开销大
- 线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位
- 同一进程中的多线程共享内存空间,线程切换代价小
- 多线程通信方便
- 从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共享了地址空间和信号处理函数
- 协程
- Go 语言中的轻量级线程实现(可以暂时把协程当作线程来理解)
- Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行 或者进行系统调用时,会主动把当前 goroutine 的 CPU § 转让出去,让其他 goroutine 能被调度 并执行,也就是 Golang 从语言层面支持了协程
Go语言的多线程模型:通信顺序处理Communicating Sequential Process
- CSP
- 描述两个独立的并发实体通过共享的通讯 channel 进行通信的并发模型。
- Go 协程 goroutine(暂时理解成Go语言中对线程的一个实现)
- 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。
- 是一种绿色线程,微线程,它与 Coroutine 协程也有区别,能够在发现堵塞后启动新的微线程。
- 通道 channel
- 类似 Unix 的 Pipe,用于协程之间通讯和同步。
- 协程之间虽然解耦,但是它们和 Channel 有着耦合。
线程和协程的差异
- 每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少
- goroutine:2KB
- 线程:8MB
- 线程/goroutine 切换开销方面,goroutine 远比线程小
- 线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP…等寄存器的刷新
- goroutine:只有三个寄存器的值修改 - PC / SP / DX.
GOMAXPROCS
- 控制并行线程数量
协程示例
启动新协程:go
functionName()
任何代码只需要在前面加一个go
关键字就起了一个新的协程
func main() {
for i := 0; i < 10; i++ {
go fmt.Println(i) // 加了go关键字,打印的顺序就会乱掉了,至于谁先打印那是CPU的事
}
time.Sleep(1 * time.Second) // 1*可以省略
}
channel - 多线程通信
在java中,通过加锁的方式去实现的,有两个线程要互相通信,可以开辟一个共享变量,这个共享变量两个线程都会去访问,一边改,一边读,此时就完成了这两个线程的通信,但是会出现一些问题,比如如何保证读写顺序?如何保证在读的时候一定是最新的值?此时就涉及到加锁了。所以在java中多线程的通信是很麻烦的。
- Channel 是多个协程之间通讯的管道
- 一端发送数据,一端接收数据
- 同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争
- 协调协程的执行顺序
- 声明方式
- var identifier chan datatype
- 操作符<-
- 示例
func main() {
ch := make(chan int) // 创建channel
go func() {
fmt.Println("hello from goroutine")
ch <- 0 // 数据写入Channel
}()
i := <-ch // 从Channel中取数据并赋值
fmt.Println(i)
}
/*
hello from goroutine
0
*/
// 如果上面的代码没有channel
func main() {
go func() {
fmt.Println("hello from goroutine")
}()
}
// 那么可能就无法保证会打印出来,因为子goroutine起来之后,主程序就退出了,那么我们可以通过time.Sleep(time.Second)等1秒中让子goroutine执行完,主程序再退出
// 那么我们怎么知道要我们起的新goroutine要执行多久呢?如果我们等1秒,但是新goroutine处理了1分钟,那么我们依然无法保证能等到新goroutine的结果的
// 所以,最简单的我们就是通过一个channel的特性是阻塞的,在主程序中从channel里面读数据,只要新goroutine没执行完,channel里面是没数据的,主程序就会阻塞在这里。这样就能确保上下都被执行了,也不用关心要等多久,让goroutine自己去处理
通道缓冲
- 基于 Channel 的通信是同步的
- 当缓冲区满时,数据的发送是阻塞的
- 通过 make 关键字创建通道时可定义缓冲区容量,默认缓冲区容量为 0
- length=0的channel,塞进去一个数据就要等receiver(接收者)去读,如果不读的话下一个数据是塞不进去的。
func main() {
ch := make(chan int) // 默认缓冲区容量为 0
ch := make(chan int, 1) // 默认缓冲区容量为 1,可存2个int数据
}
遍历通道缓冲区,使用for rang方式
消费缓冲区数据时和写入缓冲区数据的顺序是相同的。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
ch := make(chan int, 10) // 创建channel设置缓冲区为10,刚好对应下面随机10个int
go func() {
for i := 0; i < 10; i++ {
rand.Seed(time.Now().UnixNano()) // 设置随机种子值
n := rand.Intn(10) // 在此区间[0,n)产生随机int
fmt.Println(n)
ch <- n
}
close(ch)
}()
//time.Sleep(1 * time.Second) // 如果这里有此代码,则"hello from main"就会出现在上面匿名函数执行完;如果不加因为cpu太快了,所以测试了几次都是"hello from main"在第一行打印,实际上是go关键字的匿名函数和后续代码多线程并发执行的
fmt.Println("hello from main")
for v := range ch {
fmt.Println("receiving: ", v)
}
}
/*
hello from main
8
5
3
7
1
receiving: 8
receiving: 5
receiving: 3
8
receiving: 7
receiving: 1
receiving: 8
4
0
9
2
receiving: 4
receiving: 0
receiving: 9
receiving: 2
*/
单向通道
- 只发送通道
var sendOnly chan<- int
- 只接收通道
var readOnly <-chan int
- Istio webhook controller
- func (w *WebhookCertPatcher) runWebhookController(stopChan <-chan struct{}) {}
- 应用场景:双向通道转换单向通道,避免了误操作,即生产者只用来生产,避免其去消费。
package main
import (
"fmt"
"time"
)
func prod(ch chan<- int) { // 把双向通道转换成发送通道
sum := 0
for i := 1; i <= 5; i++ {
ch <- sum
sum += i
//fmt.Println(<-ch) // Invalid operation: <-ch (receive from send-only type chan<- int)
}
}
func consume(ch <-chan int) { // 把双向通道转成接收通道
for {
fmt.Println("receiving: ", <-ch)
}
}
func main() {
var c = make(chan int) // 创建channel,双向通道
go prod(c) // 生产者
go consume(c) // 消费者
time.Sleep(1 * time.Second) // 1*可以省略
}
关闭通道
- 通道无需每次关闭
- 关闭的作用是告诉接收者该通道再无新数据发送
- 只有发送方需要关闭通道
func main() {
ch := make(chan int)
defer close(ch)
go func() { ch <- 0 }()
if v, notClosed := <-ch; notClosed {
fmt.Println(v, notClosed) // 0 true
}
}
select
在一个场景中,我们需要从不同的通道读数据,不同通道的数据走不同的逻辑处理,就需要用到select语句
- 当多个协程同时运行时,可通过 select 轮询多个通道
- 如果所有通道都阻塞则等待,如定义了 default 则执行 default
- 如多个通道就绪则随机选择
select {
case v:= <- ch1:
...
case v:= <- ch2:
...
default:
...
}
定时器 Timer
- time.Ticker 以指定的时间间隔重复的向通道 C 发送时间值
- 使用场景
- 为协程设定超时时间
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")
}
// 不恰当的例子
func main() {
ch := make(chan int)
defer close(ch)
timer := time.NewTimer(3 * time.Second) // 设置定时器为3秒
fmt.Println("当前时间为:", time.Now())
go func() {
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
}()
OuterLoop:
for {
select {
case v := <-ch:
fmt.Println("received from ch", v)
case t := <-timer.C: // 从定时器拿数据
fmt.Println("timeout waiting from channel ch", t)
break OuterLoop // 跳出标签处for循环
}
}
}
func main() {
ticker := time.NewTicker(2 * time.Second)
for t := range ticker.C { // 每2秒执行一下循环
fmt.Println(t)
}
}
/*
2022-07-09 12:30:36.431389 +0800 CST m=+2.000755936
2022-07-09 12:30:38.431261 +0800 CST m=+4.000617609
2022-07-09 12:30:40.430916 +0800 CST m=+6.000261658
...
*/
上下文 Context
比如一个web应用,每一个请求过来,都会起一个新的goroutine帮我们处理,有些时候我们的web Server要强制重启,可能我们哪些已经发过来的请求已经在修改数据了,或者正在处理一个很重要的操作,我们有没有办法可以把其cancel取消掉呢?
- 超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
- Context 是设置截止日期、同步信号,传递请求相关值的结构体
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- 用法
- context.Background
- Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background
- context.TODO
- TODO是在不确定使用什么context的时候才会使用
- context.WithDeadline
- 超时时间
- context.WithValue
- 向context添加键值对
- context.WithCancel
- 创建一个可取消的context
如何停止一个子协程
done channel 方式:
func main() {
messages := make(chan int, 10)
done := make(chan bool)
defer close(messages)
// consumer,消费者
go func() {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C { // 每1秒执行一次循环
select {
case <-done: // 如果done chan关闭,则执行此处
fmt.Println("child process interrupt...")
return // return后协程就被关闭了
default: // 如果done chan没关闭(即done chan阻塞了,)就执行default
fmt.Printf("send message: %d\n", <-messages)
}
}
}()
// producer,生产者
for i := 0; i < 10; i++ {
messages <- i // 向messages chan写数据
}
time.Sleep(5 * time.Second)
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
/*
send message: 0
send message: 1
send message: 2
send message: 3
send message: 4
child process interrupt...
main process exit!
*/
基于 Context 停止子协程
Background 方式:
func main() {
baseCtx := context.Background() // 初始化最顶层的context
ctx := context.WithValue(baseCtx, "a", "b") // 向baseCtx添加键值对
go func(c context.Context) { //在另外一个协程中去读context的值
fmt.Println(c.Value("a"))
}(ctx)
/*
上面代码的应用场景:比如在http server中要初始化一些值,
我们就可以在主goroutine中初始化,起其他goroutine时就把context往下传
*/
timeoutCtx, cancel := context.WithTimeout(baseCtx, time.Second) // 在主context基础之上定义了一个超时时间为1秒的context
defer cancel()
go func(ctx context.Context) { // 起新的协程把timeoutCtx传进去
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-ctx.Done(): // 超过Timeout context设置的超时时间就中断
fmt.Println("child process interrupt...")
return
default:
fmt.Println("enter default")
}
}
}(timeoutCtx)
/*
上面代码的应用场景:用来处理作业时间比较长,超时我就不想等的场景。
*/
select {
case <-timeoutCtx.Done():
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
// time.Sleep(time.Second * 5)
}
/*
b
enter default
main process exit!
child process interrupt...
*/
- 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())
线程加锁(Sync包)
理解线程安全
除了Go语言提供的channel机制(基于 CSP 的通讯模型),Go语言本身还有另外的保证线程安全的机制(基于共享内存的多线程数据访问)。
现在的机器一般都是多核的CPU,其特性是当我们的应用启用了多线程,那么一部分线程会被第一个CPU去运行,一部分线程会被第二个CPU去运行。
那么就会存在一个情况,即 ”多个线程同时访问同一个内存地址“ :如果我们所有的数据都去访问内存,整个程序的效率就会降低,因为内存的速度肯定是不如CPU的,所以在当下的CPU架构里面,每一个CPU里面都会有缓存机制即L1、L2、L3缓存,当多个线程去访问内存中的一个变量,那么这个数据会被放在CPU的缓存里面,即此线程只有在第一次访问数据的时候会去内存加载,后面所有的数据读写都是通过CPU的本地缓存去做的,那么就会遇到一个“可见性”的问题。
举一个例子,我们内存里面放了一个key value,值是value1,此时线程1和线程2同时对其读写,那么在T0时间也就是最开始的时两个线程读到的数据是一样的,那么在某一个时间点,线程1把value1改成value2了,那么此时在线程2中还是value1,那么就会出现一个问题,你觉得你在一个线程中去改了变量,但是在另外一个线程中去读出来的时候还是原来的值,这就是线程不安全了。
锁
所以,我们就要通过锁来解决上述说的问题。锁其实就是保护对变量的访问,当有多个线程去访问一份数据的时候,可以在一个线程里面对其加锁,那么另外一个线程在尝试读取或修改的时候一样要尝试获取这个锁,如果第一个线程没有把锁放掉,那么第二个线程本身是访问不到这个数据的,它会一直去等去请求。
- Go 语言不仅仅提供基于 CSP 的通讯模型,也支持基于共享内存的多线程数据访问
- Sync 包提供了锁的基本原语
sync.Mutex
互斥锁(无论读写都是互斥的)
- Lock()加锁,Unlock 解锁
sync.RWMutex
读写分离锁
- 不限制并发读,只限制并发写和并发读写
- 是对互斥锁进一步优化,不阻止去并发读。
sync.WaitGroup
- 等待一组 goroutine 返回
sync.Once
- 保证某段代码只执行一次
sync.Cond
- 让一组 goroutine 在满足特定条件时被唤醒
Mutex示例
package main
import (
"sync"
"time"
)
func main() {
//unsafeWrite() // 报错:fatal error: concurrent map writes
safeWrite()
time.Sleep(time.Second)
}
func unsafeWrite() {
conflictMap := map[int]int{} // 创建一个map
for i := 0; i < 100; i++ {
go func() { // 启用多线程去修改map的值
conflictMap[1] = i
}()
}
}
type SafeMap struct { // 定义一个struct,把map放到struct里面
safeMap map[int]int
sync.Mutex // Mutex对象,是互斥锁对象
}
func safeWrite() {
s := SafeMap{ // 初始化并创建SafeMap对象
safeMap: map[int]int{},
Mutex: sync.Mutex{},
}
for i := 0; i < 100; i++ {
go func() { // 每一次循环起一个新的goroutine
s.Write(1, 1)
}()
}
}
func (s *SafeMap) Read(k int) (int, bool) {
s.Lock()
defer s.Unlock()
result, ok := s.safeMap[k]
return result, ok
}
func (s *SafeMap) Write(k, v int) {
s.Lock() // 加锁
defer s.Unlock() //释放锁
s.safeMap[k] = v
}
Kubernetes 中的 informer factory
使用的是Mutex
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
RWMutex示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
go rLock()
go wLock()
go lock()
time.Sleep(5 * time.Second)
}
func lock() {
lock := sync.Mutex{} // 互斥锁
for i := 0; i < 3; i++ {
lock.Lock()
defer lock.Unlock()
fmt.Println("lock:", i)
}
}
func rLock() {
lock := sync.RWMutex{} // 读写分离锁
for i := 0; i < 3; i++ {
lock.RLock() // 加读锁,允许并发读
defer lock.RUnlock()
fmt.Println("rLock:", i)
}
}
func wLock() {
lock := sync.RWMutex{} // 读写分离锁
for i := 0; i < 3; i++ {
lock.Lock()
defer lock.Unlock()
fmt.Println("wLock:", i)
}
}
/*
lock: 0
rLock: 0
wLock: 0
rLock: 1
rLock: 2
*/
WaitGroup 示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
waitBySleep()
waitByChannel()
}
func waitBySleep() {
for i := 0; i < 100; i++ {
go fmt.Println(i) // 每一次循环起一个新的goroutine
}
// waitBySleep是主线程,上面的go关键字是新线程,我们无法保证上面能一定先执行完,所以我们要等待一下
time.Sleep(time.Second)
}
// 所以time.Sleep方式不好,我们可以通过channel的特性是阻塞的去实现,来保证子进程可以完整的执行完,主线程才退出
func waitByChannel() {
c := make(chan bool, 100)
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i)
c <- true
}(i)
}
// 这样可以确保上面100个goroutine的数据写完了,主线程才退出
for i := 0; i < 100; i++ {
<-c
}
}
// 如果我们有一百万个线程呢?
func waitByWG() {
wg := sync.WaitGroup{} // 创建一个WaitGroup
wg.Add(100) // 向WaitGroup里面去add 100个,即这个组里有100个线程
for i := 0; i < 100; i++ {
go func(i int) { // 起新的goroutine
fmt.Println(i)
wg.Done() // 每执行完一个goroutine我们就Done一个
}(i)
}
wg.Wait() // 在主线程里面去调用,即组内所有的都Done了,才会结束阻塞
}
Kubernetes 中的 端对端测试的源码
// CreateBatch create a batch of pods. All pods are created before waiting.
func (c *PodClient) CreateBatch(pods []*v1.Pod) []*v1.Pod {
ps := make([]*v1.Pod, len(pods))
var wg sync.WaitGroup
for i, pod := range pods {
wg.Add(1)
go func(i int, pod *v1.Pod) {
defer wg.Done()
defer GinkgoRecover()
ps[i] = c.CreateSync(pod)
}(i, pod)
}
wg.Wait()
return ps
}
Once示例
单例模式:即可能某些代码被多个线程同时执行,但我只希望某些代码的初始化动作只执行一次。那么就需要Once
package main
import (
"fmt"
"sync"
)
type SliceNum []int
func NewSlice() SliceNum {
return make(SliceNum, 0)
}
// 为SliceNum定义Add方法,给我一个elem,将其append进去
func (s *SliceNum) Add(elem int) *SliceNum {
*s = append(*s, elem)
fmt.Println("add", elem)
fmt.Println("add SliceNum end", s)
return s
}
func main() {
var once sync.Once // 创建一个Once
s := NewSlice()
// 看源代码理解once的行为
once.Do(func() { // 通过once.Do去执行的func是被Go确保此func只会被执行一次的
s.Add(16)
})
once.Do(func() {
s.Add(17)
})
once.Do(func() {
s.Add(18)
})
}
/* 虽然func Add被调用了3次,但是真正只执行了1次
add 16
add SliceNum end &[16]
*/
Cond示例
cond一般都是需要通过sync.NewCond(&sync.Mutex{})
去定义
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
queue []string // 队列,一般是用切片来代表,即可变的连续内存段
cond *sync.Cond
}
func main() {
q := Queue{
queue: []string{}, // 初始化队列
cond: sync.NewCond(&sync.Mutex{}), // 初始化condition
}
go func() { // 生产者,每2秒钟生产一个
for {
q.Enqueue("a")
time.Sleep(time.Second * 2)
}
}()
for { // 消费者,每1秒钟消费一个
q.Dequeue()
time.Sleep(time.Second)
}
}
// 为Queue定义Enqueue方法
func (q *Queue) Enqueue(item string) {
q.cond.L.Lock() // 加锁
defer q.cond.L.Unlock()
q.queue = append(q.queue, item) // 把传入的item塞入到队列里
fmt.Printf("putting %s to queue, notify all\n", item)
q.cond.Broadcast()
}
// 为Queue定义Dequeue方法
func (q *Queue) Dequeue() string {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.queue) == 0 { // 如果队列的len是0,就需要等待
fmt.Println("no data available, wait")
q.cond.Wait()
}
result := q.queue[0] // 取出队列的第一个数据
q.queue = q.queue[1:] // 删除队列的第一个数据
return result
}
Kubernetes 中的队列,标准的生产者消费者模式
// Add marks item as needing processing.
func (q *Type) Add(item interface{}) { // 生产者
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.shuttingDown {
return
}
if q.dirty.has(item) {
return
}
q.metrics.add(item)
q.dirty.insert(item)
if q.processing.has(item) {
return
}
q.queue = append(q.queue, item)
q.cond.Signal()
}
// Get blocks until it can return an item to be processed. If shutdown = true,
// the caller should end their goroutine. You must call Done with item when you
// have finished processing it.
func (q *Type) Get() (item interface{}, shutdown bool) { // 消费者
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.queue) == 0 && !q.shuttingDown {
q.cond.Wait()
}
if len(q.queue) == 0 {
// We must be shutting down.
return nil, true
}
item, q.queue = q.queue[0], q.queue[1:]
q.metrics.get(item)
q.processing.insert(item)
q.dirty.delete(item)
return item, false
}
线程调度
Linux内核原理
PID为1的进程就是整个Linux操作系统的根,是所有进程的父亲,所有的其他进程都是由父进程fork出来的。任何进程都是fork出来的,子进程可以继续fork出子进程,所以最终整个操作系统的进程就是一颗父子关系树。
fork进程的时候除了会创建新的Process,还会把之前进程所持有的内存状态、打开文件、文件系统、信号量都会复制一份,接下来这两个进程除了父子关系外就没有任何关系了,新的子进程会维护自己的内存模型等等。
Linux操作系统本身是没有线程概念的,提供了pthread_create调用,实际上是clone了一个进程,但是这个新clone出来的进程和之前的父进程是共享内存、共享操作系统、共享打开文件、共享signal。所以它们之间是有关系的。
对于线程来讲,从用户的角度是通过pthread_create去创建线程,但是从Linux内核来讲,都是一个task_struct,这个task_struct 和之前的进程是没有任何区别的,只不过这个task_struct 没有独立的资源。其实所谓的线程就是一个没有独立资源的进程。
- 进程:资源分配的基本单位
- 线程:调度的基本单位,可以理解为没有独立资源的进程
- 无论是线程还是进程,在 linux 中都以 task_struct 描述,从内核角度看,与进程无本质区别
- Glibc 中的 pthread 库提供 NPTL(Native POSIX Threading Library)支持
mm:虚拟内存
fs:文件系统
files:打开文件
signal:信号量
pthread_create(clone flags)
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID
其实,kubernetes很多的设计思想和Linux是一模一样的,所以对Linux有了解对kubernetes也会更深入。
Linux 进程的内存使用
我们要理解Linux进程的开销,包括切换进程的开销是什么,进而理解Go语言为什么要重新写一个goroutine出来。
一台计算机我们插上内存条,就是物理内存。那么,针对每个应用程序一般来说在早期的时候如果应用程序要访问内存就直接去访问物理地址了,那么这样就会有一个问题:“当我们有多个线程或多个进程的时候,它们之间会去抢同一块物理地址”,所以中间会有问题的。随着技术的迭代,就出现了虚拟地址这样的技术手段。
虚拟地址的理解:比如该计算机有4G的物理内存,物理内存的这些物理地址是固定的,每起一个应用进程的时候,对这个应用进程虚拟4G的内存空间出来,那么这4G虚拟内存空间里面的地址都是虚拟地址,以32位机器为例,虚拟地址包含:1G的内核空间、3G的用户空间。用户空间包含了程序代码、初始化数据、未初始化数据、堆和栈。通过size命令
可以看到这些空间的大小。
虚拟内存的诞生是为了内存的复用,一台机器有4G物理内存,但是每起一个程序都会初始化4G的虚拟内存,这就涉及到虚拟内存和物理内存中间的转化关系,这个转化关系就是页表,页表就是物理内存和虚拟内存的对应关系,可以理解为索引,所有的物理内存都是分页的,页有多大可以通过getconf PAGE_SIZE
命令去得知,比如每页大小是4096也就是4k,所以也就是说对于所有的物理内存都按照4k大小去分页,最终访问的内存的物理地址就是页的位置再加上一个偏移量。
对虚拟内存来讲,如果最求最高的性能就是一个物理内存对一个虚拟内存,如果是这样的话,就需要有很多的页表去记录这种对应关系,就会占用很多的内存,为了解决这个问题,Linux现在就发展成 多级页表了,常见的有4级页表PGD、PUD、PMD、PT这四级。相当于说我们先有一个顶级的索引PGD然后有次级索引有两层:PUD、PMD,最后是PT,真正的物理内存地址是通过这4级索引找到PT在 加上偏移量就是真正的物理地址了。
另外Linux有交换机制,会把一些数据写入磁盘里就是SWAP。
Text:一个应用程序的程序代码,这些代码在应用启动的时候会加载到Text空间里。
Data:代码中的静态变量会放在Data里面。
BSS:代码中定义了变量,但是在初始化的时候这些变量没有赋值的会放在BSS里面。
dec:是Text+Data+BSS三个值相加。
我们可以通过objdump -x 命令
去看一下
CPU 对内存的访问
CPU上有MMU,CPU要访问内存,会先给MMU发虚拟地址,MMU负责返回给CPU物理地址,所以MMU要知道虚拟地址和物理地址的映射关系。在CPU里面还有一个缓存叫TLB,TLB存储的就是虚拟地址和物理地址的映射关系的缓存。
假如CPU要访问内存地址,要先问MMU我给你虚拟地址你给我真实的数据,在第一次访问时,MMU是不知道的,那么MMU就要去真正的物理内存里面去查页表,刚才说过4级页表+偏移量就可以找到真实的物理地址了,那么这个时候就会把映射信息存在TLB中,相当于缓存下来了,当CPU下一次再访问这块地址的时候,就会先去查TLB,如果有的话就直接拿过来,如果没有MMU再去查页表。所以TLB就是快速缓存,当我们访问过内存的某块地址后,就会存在TLB里,因为程序访问过某地址很可能会在接下来的操作中继续访问,该块地址是活跃的,所以TLB本身是做加速的。TLB本身就是在CPU里面,所以比CPU的L1、L2、L3还要快。
- CPU 上有个 Memory Management Unit(MMU) 单元
- CPU 把虚拟地址给 MMU,MMU 去物理内存中查询页表,得到实际的物理地址
- CPU 维护一份缓存 Translation Lookaside Buffer(TLB),缓存虚拟地址和物理地址的映射关系
进程切换开销
所以,通过上述的介绍,我们就会理解,其实进程的切换是有开销的。
- 直接开销
- 切换页表全局目录(PGD)
- 切换内核态堆栈
- 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
- 刷新 TLB
- 系统调度器的代码执行
- 间接开销
- CPU 缓存失效导致的进程需要到内存直接访问的 IO 操作变多
线程切换开销
- 线程本质上只是一批共享资源的进程,线程切换本质上依然需要内核进行进程切换
- 一组线程因为共享内存资源,因此一个进程的所有线程共享虚拟地址空间,线程切换相比进程 切换,主要节省了虚拟地址空间的切换
用户线程
无需内核帮助,应用程序在用户空间创建的可执行单元,创建销毁完全在用户态完成。
Goroutine
Go 语言基于 GMP 模型实现用户态线程
- G:表示 goroutine,每个 goroutine 都有自己的栈空间,定时器, 初始化的栈空间在 2k 左右,空间会随着需求增长。
- M:表示 Machine,抽象化代表内核线程,记录内核线程栈信息,当 goroutine 调度 到线程时,使用该 goroutine 自己的栈信息。
- P:表示Process,代表调度器,负责调度 goroutine,维护一个本地 goroutine 队 列,M 从 P 上获得 goroutine 并执行,同时还负责部分内存的管理。
MPG的对应关系
GMP 模型细节
P的状态
G的状态
G的状态转换图
G 所处的位置
- 进程都有一个全局的 G 队列
- 每个 P 拥有自己的本地执行队列
- 有不在运行队列中的 G
- 处于 channel 阻塞态的 G 被放在 sudog
- 脱离 P 绑定在 M 上的 G,如系统调用
- 为了复用,执行结束进入 P 的 gFree 列表中的 G
Goroutine 创建过程
- 获取或者创建新的 Goroutine 结构体
- 从处理器的 gFree 列表中查找空闲的 Goroutine
- 如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体
- 将函数传入的参数移到 Goroutine 的栈上
- 更新 Goroutine 调度相关的属性,更新状态为_Grunnable
- 返回的 Goroutine 会存储到全局变量 allgs 中
将 Goroutine 放到运行队列上
- Goroutine 设置到处理器的 runnext 作为下一个处理器 执行的任务
- 当处理器的本地运行队列已经没有剩余空间时,就会把 本地队列中的一部分 Goroutine 和待加入的 Goroutine 通过 runtime.runqputslow 添加到调度器持有的全局运行队列上
调度器行为
- 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定几率会从全局的运行队列中查找对应的 Goroutine
- 从处理器本地的运行队列中查找待执行的 Goroutine
- 如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找 Goroutine
- 从本地运行队列、全局运行队列中查找
- 从网络轮询器中查找是否有 Goroutine 等待运行
- 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine
内存管理
关于内存管理的争论
堆内存管理
栈:和线程相关,任何函数调用的时候,相当于把函数的局部变量和函数本身都压入栈里,一旦这个线程结束了,本地的这些变量都会被释放掉,栈的内存管理是很简单的。
堆:内存管理是比较复杂的。
Allocator:有两个职责
- 先和操作系统要一块内存空间。java在启动的时候按整个操作系统一定的百分比去开辟,是比较占内存的。比如上来就占用了10G内存,当我们的代码需要内存的时候,再划给我们,即Mutator(也就是我们的应用程序)找Allocator要内存,比如我们要建16K的对象,Allocator就会在这10G的内存中分配一个16K的空间给Mutator。
- 为16K的空间写一些metadata,metadata里一般都记录了数据的长度是多少,是不是在使用等等。
Collector:垃圾回收器,定期的去扫瞄整个Heap,有哪些内存空间的对象还活跃,有哪些不活跃,不活跃的就会把内存回收回来。
- 初始化连续内存块作为堆
- 有内存申请的时候,Allocator 从堆内存的未分配区域分割小内存块
- 用链表将已分配内存连接起来
- 需要信息描述每个内存块的元数据:大小,是否使用,下一个内存块的地址等
堆内存管理的挑战
内存分配需要系统调用,在频繁内存分配的时候,系统性能较低。
多线程共享相同的内存空间,同时申请内存时,需要加锁,否则会产生同一块内存被多个线程访问的情况。
内存碎片的问题,经过不断的内存分配和回收,内存碎片会比较严重,内存的使用效率降低。
ThreadCacheMalloc 概览(TCMalloc)
TCMalloc是Google的方法。
假如我们的Application要8字节的空间,会先去ThreadCache找8字节的空间,如果没有的话,ThreadCache会找CentoralCache要,但是不是要8字节,是按页要或者说是按span要,所以说要来的空间会更多,也就是会把加锁的频率大大降低了。
- page:内存页,一块 8K 大小的内存空间。Go 与操作系统之间的内存申请和释放,都是以 page 为单位的
- span:内存块,一个或多个连续的 page 组成一个 span
- sizeclass:空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何 使用
- object:对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大 的 object ;假设 object 的大小是 16B ,span 大小是 8K ,那么就会把 span 中的 page 就会 被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去
- 对象大小定义
- 小对象大小:0~256KB
- 中对象大小:256KB~1MB
- 大对象大小:>1MB
- 小对象的分配流程
- ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache 缓存都是足够的,不需要去访问 CentralCache 和 HeapPage,无系统调用配合无锁分配,分配效率是非常高的
- 中对象分配流程
- 直接在 PageHeap 中选择适当的大小即可,128 Page 的 Span 所保存的最大内存就是 1MB
- 大对象分配流程
- 从 large span set 选择合适数量的页面组成 span,用来存储数据
Go 语言内存分配
由TCMalloc延伸过来的,但是和TCMalloc有一些不同的。
- mcache:小对象的内存分配直接走
- size class 从 1 到 66,每个 class 两个 span
- Span 大小是 8KB,按 span class 大小切分
- mcentral
- Span 内的所有内存块都被占用时,没有剩余空间继续分配对象,mcache 会向 mcentral 申请1个 span,mcache 拿到 span 后继续分配对象
- 当 mcentral 向 mcache 提供 span 时,如果没有符合条件的 span,mcentral 会向 mheap 申请 span
- mheap
- 当 mheap 没有足够的内存时,mheap 会向 OS 申请内存
- Mheap 把 Span 组织成了树结构,而不是链表
- 然后把 Span 分配到 heapArena 进行管理,它包含地址映射和 span 是否包含指针等位图
- 为了更高效的分配、回收和再利用内存
内存回收
- 引用计数(Python,PHP,Swift)
- 对每一个对象维护一个引用计数,当引用该对象的对象被销毁的时候,引用计数减 1,当引用计数为 0 的时候,回收该对象
- 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收
- 缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价
- 标记-清除(Golang)
- 从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收
- 优点:解决引用计数的缺点
- 缺点:需要 STW(stop the word),即要暂停程序运行
- 分代收集(Java)
- 按照生命周期进行划分不同的代空间,生命周期长的放入老年代,短的放入新生代,新生代的回收频率高于老年代的频率
mspan
- allocBits
- 记录了每块内存分配的情况
- gcmarkBits
- 记录了每块内存的引用情况,标记阶段对每块内存进行标记,有对象引用的内存标记为1,没有的标 记为 0
- 这两个位图的数据结构是完全一致的,标记结束则进行内存回收,回收的时候,将 allocBits 指 向 gcmarkBits,标记过的则存在,未进行标记的则进行回收
GC 工作流程
Golang GC 的大部分处理是和用户代码并行的
- Mark:
- Mark Prepare: 初始化 GC 任务,包括开启写屏障 (write barrier) 和辅助 GC(mutator assist),统计root对象的任 务数量等。这个过程需要STW
- GC Drains: 扫描所有 root 对象,包括全局指针和 goroutine(G) 栈上的指针(扫描对应 G 栈时需停止该 G),将其 加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行
- Mark Termination:完成标记工作,重新扫描(re-scan)全局指针和栈。因为 Mark 和用户程序是并行的,所以在 Mark 过 程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan 再检查一下,这 个过程也是会 STW 的
- Sweep:按照标记结果回收所有的白色对象,该过程后台并行执行
- Sweep Termination:对未清扫的 span 进行清扫, 只有上一轮的 GC 的清扫工作完成才可以开始新一轮的 GC
三色标记
- GC 开始时,认为所有 object 都是 白色,即垃圾。
- 从 root 区开始遍历,被触达的 object 置成 灰色。
- 遍历所有灰色 object,将他们内部的引用变量置成 灰色,自身置成 黑色
- 循环第 3 步,直到没有灰色 object 了,只剩下了黑白两种,白色的都是垃圾。
- 对于黑色 object,如果在标记期间发生了写操作,写屏障会在真正赋值前将新对象标记为 灰色。
- 标记过程中,mallocgc 新分配的 object,会先被标记成 黑色 再返回。
垃圾回收触发机制
- 内存分配量达到阀值触发 GC
- 每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动 GC。
- 阀值 = 上次 GC 内存分配量 * 内存增长率
- 内存增长率由环境变量 GOGC 控制,默认为 100,即每当内存扩大一倍时启动 GC。
- 定期触发 GC
- 默认情况下,最长 2 分钟触发一次 GC,这个间隔在 src/runtime/proc.go:forcegcperiod 变量中 被声明
- 手动触发
- 程序代码中也可以使用 runtime.GC()来手动触发 GC。这主要用于 GC 性能测试和统计。
网络
理解网络协议层
链路层:用来存MAC地址的,每一块网卡都有自己的MAC地址的,MAC地址是网卡硬件商出厂就设置好的。MAC地址在一个子网中是几乎不会重复的。
网络层:IP
传输层:IP+Port
比如用户在浏览器里去访问www.baidu.com,事实上是在浏览器里组装了一个数据包,这个数据包可能是空的,但是会塞入HTTP header,在header会写上我们访问的域名是www.baidu.com,我们的请求的method是GET还是POST等,那么我们的数据包就到此组装好了。
数据包是要向外发送的,那么就要交给操作系统,通过Socket从用户态转成了内核态。
数据包要传输,实际上浏览器是要做一次DNS解析的,要知道www.baidu.com的IP地址是什么,那么操作系统就会去封装咱们的HTTP数据包,加上源IP、目标IP。。
最后这个数据包经过路由器 互联网层层转发就到了server这端了。即下图再从下到上走一遍。
理解Socket
HTTP是基于TCP,TCP的实现是Socket。
- socket被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。
- Linux中的一切都是文件,为了表示和区分已经打开的文件,UNIX/Linux会给每个文件分配一个ID,这个ID就是一个整数,被称为文件描述符(FD file descriptor)。
- 网络连接也是一个文件,它也有文件描述符。
- 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
- 在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
- 服务端的Accept接收到请求以后,会生成连接FD,借助这个FD我们就可以使用普通的文件操作函数来传输数据了,例如:
- 用read()读取从远程计算机传来的数据
- 用write()向远程计算机写入数据
理解 net.http 包
- 注册 handle 处理函数
// 如果请求访问了/healthz路径,那么我们就用healthz函数去处理
http.HandleFunc("/healthz", healthz)
- ListenAndService
err := http.ListenAndServe(":80", nil)
if err != nil {
log.Fatal(err)
}
- 定义handle处理函数
func healthz(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok")
}
阻塞 IO 模型
非阻塞 IO 模型
IO 多路复用
异步IO
Linux epoll
Go 语言高性能 httpserver 的实现细节
- Go 语言将协程与 fd 资源绑定
- 一个 socket fd 与一个协程绑定
- 当 socket fd 未就绪时,将对应协程设置为 Gwaiting 状态,将 CPU 时间片让给其他协程
- Go 语言 runtime 调度器进行调度唤醒协程时,检查 fd 是否就绪,如果就绪则将协程置为 Grunnable 并加入执行队列
- 协程被调度后处理 fd 数据
调试
debug
- gdb:
- Gccgo 原生支持 gdb,因此可以用 gdb 调试 Go 语言代码,但 dlv 对 Go 语言 debug 的支持比 gdb 更好
- Gdb 对 Go 语言的栈管理,多线程支持等方面做的不够好,调试代码时可能有错乱现象
- dlv:IDE基本都是通过dlv去调试的
- Go 语言的专有 debugger
dlv 的配置
- 配置
- 在 vscode 中配置 dlb
- 菜单:View -> Command Palette
- 选择 Go : Install/Update Tools,选择安装
- 安装完后,从改入口列表中可以看到 dlv 和 dlv-dap 已经安装好
- Debug 方法
- 在代码中设置断点
- 菜单中选择 Run -> Start Debugging 即可进入调试
更多 debug 方法
- 添加日志
- 在关键代码分支中加入日志
- 基于fmt包将日志输出到标准输出 stdout
- fmt.Println()
- fmt 无日志重定向,无日志分级
- 即与日志框架将日志输出到对应的 appender
- 比如可利用 glog 进行日志输出
- 可配置 appender,将标准输出转至文件
- 支持多级日志输出,可修改配置调整日志等级
- 自带时间戳和代码行,方便调试
Glog 使用方法示例
go get -u github.com/golang/glog
import (
"flag"
"github.com/golang/glog"
"log"
"net/http"
)
func main() {
flag.Set("v", "4")
glog.V(2).Info("Starting http server...")
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
err := http.ListenAndServe(":80", mux)
if err != nil {
log.Fatal(err)
}
}
性能分析(Performance Profiling)
- CPU Profiling: 在代码中添加 CPUProfile 代码,
runtime/pprof
包提供支持
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime/pprof"
"time"
)
// 定义 flag cpuprofile
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
flag.Parse()
// 如果命令行设置了 cpuprofile
if *cpuprofile != "" {
// 根据命令行指定文件名创建 profile 文件
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
// 开启 CPU profiling
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
for i := 0; i < 10; i++ {
fmt.Println("程序员麻辣烫")
time.Sleep(time.Second)
}
}
go run main.go --cpuprofile /tmp/cpuprofile
分析 CPU 瓶颈
- 运行上述代码后,会在 /tmp/cpuprofile 中记录 cpu 使用时间
- 运行
go tool pprof /tmp/cpuprofile
进入分析模式 - 运行 top10 查看 top 10线程,显示 30ms 花费在 main.main
- (pprof) list main.main 显示 30 毫秒都花费在循环上
- 可执行 web 命令生成 svg 文件,在通过浏览器打开 svg 文件查看图形化分析结果
其他可用 profiling 工具分析的问题
- CPU profile
- 程序的 CPU 使用情况,每 100 毫秒采集一次 CPU 使用情况
- Memory Profile
- 程序的内存使用情况
- Block Profiling
- 非运行态的 goroutine 细节,分析和查找死锁
- Goroutine Profiling
- 所有 goroutines 的细节状态,有哪些 goroutine,它们的调用关系是怎样的
针对 http 服务的 pprof
- net/http/pprof 包提供支持
- 如果采用默认 mux handle,则只需 import _ “net/http/pprof”
- 如果采用自定义 mux handle,则需要注册 pprof handler
import (
"net/http/pprof"
)
func startHTTP(addr string, s *tnetd.Server) {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
server := &http.Server{
Addr: addr,
Handler: mux,
}
server.ListenAndServe()
}
分析 go profiling 结果
在运行了开启 pprof 的服务器以后,可以通过访问对应的 URL 获得 profile 结果
- allocs: A sampling of all past memory allocations
- block: Stack traces that led to blocking on synchronization primitives
- cmdline: The command line invocation of the current program
- goroutine: Stack traces of all current goroutines
- heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.
- mutex: Stack traces of holders of contended mutexes
- profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.
- threadcreate: Stack traces that led to the creation of new OS threads
- trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.
结果分析示例
- 分析 goroutine 运行情况
- curl localhost/debug/pprof/goroutine?debug=2
- 分析堆内存使用情况
- curl localhost/debug/pprof/heap?debug=2