Go 基础与语法特性 🚪

基本语法 + 函数式编程 + 面向接口 + 并发编程
参考资料
Gogogo!
1 | Windows |
1 | go install golang.org/x/tools/cmd/goimports@latest |
在 File Watchers 插件(文件修改时触发任务)中添加 go fmt,goimports 工具

GoLand 2023.1.3 使用 ideaVim 2.6.1 插件在普通模式下
u
按键无效,整了半天,最后降级插件版本问题消失。
新建模板

1 | // Package ${GO_PACKAGE_NAME} ----------------------------- |
基础语法
- 变量定义
- 变量类型写在变量名之后
- 编译器可推测变量类型
- 没有 char,只有 rune
- 原生支持复数类型
任何地方都可通过
_
省略变量
- 条件语句
- 没有 while
- if 的条件里可以定义变量,作用域就在这个 if 语句里
- switch 会自动 break,除非使用 fallthrough
- switch 后可以没有表达式
- 循环
- for, if 后面的条件没有括号
- for 的条件里可以省略初始条件,结束条件,递增表达式
- for 省略初始条件,相当于 while
- for 省略全部,相当于无限循环
- 函数
- 返回值类型写在最后面
- 函数可返回多个值
- 函数返回多个值时可以起名字,仅用于非常简单的函数
- 对于调用者而言没有区别
- 函数作为参数
- 没有默认参数,可选参数
- 支持可变参数列表
- 指针
- 指针不能运算
- go 只有值传递一种方式
内建容器
- 数组
- 数量写在类型前
- 如果只要 i,可写成
for i := range numbers
- 要下表和值,
for i, v := range numbers
- 只要值,
for _, v := ranger numbers
[10]int
和[20]int
是不同类型- 数组是值类型,调用
func f(arr [10] int)
会拷贝数组 - 在 go 语言中一般不直接使用数组
- Slice (切片)
- Slice 本身没有数据,是对底层 array 的一个 view
- Slice 可以直接修改 array 的值
- Reslice
s := arr[2;6] s = s[:3]
- Slice 的扩展(slice 包含三个变量 ptr len cap)
- Slice 可以向后扩展,不可以向前扩展
- s[i] 不可以超越 len(s),向后扩展不可以超越底层数组 cap(s)
- 添加元素时如果超越 cap,系统会重新分配更大的底层数组
- 由于值传递的关系,必须接收 append 的返回值,
s = append(s, val)
- Map
- 创建:
make(map[string]int)
- 获取元素:
m[key]
- key 不存在时,获得 value 类型的初始值
- 用
value,ok := m[key]
来判断是否存在 key - 用 delete 删除一个 key
- 使用 range 遍历 key,或者遍历 key, value 对
- 不保证遍历顺序,如需顺序,需手动对 key 排序
- 使用 len 获取元素的个数
- map 使用哈希表,必须可以比较相等
- 除了 slice, map, function的内建类型都可以作为 key
- Struct 类型不包含上述字段,也可作为 key
- 创建:
- String
- 使用 range 遍历 pos, rune 对
- 使用
utf8.RuneCountInString
获得字符数量 - 使用
len()
获得字节长度 - 使用
[]byte
获得字节 - 字符串的相关方法在 strings 包下
面向“对象”
结构体和方法
- go 语言仅支持封装,不支持继承和多态
- go 语言没有 class,只有 struct
- 结构可以使用自定义工厂函数
- 定义方法接收者
- 只有使用指针才可以改变结构内容
- nil 指针也可以调用方法
- 值接收者 是 go 语言特有的
- 值/指针接收者均可接收值/指针
注意可以返回局部变量的地址给别人用!结构创建在堆上还是栈上?不需要知道
包和封装
- 命名一般使用 CamelCase
- 首字母大写:public,首字母小写:private(针对包)
- 每个目录一个包
- main 包包含可执行入口
- 为结构定义的方法必须放在同一个包内,可以是不同文件
- 扩充系统类型或者别人的类型:定义别名(最简单)、组合(最常用)、内嵌(语法糖,省代码)
依赖管理
依赖管理的三个阶段(三种方式):GOPATH,GOVENDOR,GOMODULE
- GOPATH
- 默认在
~/go
(unix, linux),%USERPROFILE%\go
(windows) - 依赖查找顺序:
GOROOT/src
→GOPATH/src
- 默认在
- GOVERDOR
- 每个项目有自己的vendor目录,存放第三方库
- 大量第三方依赖管理工具:glide, dep, go dep, …
- 依赖查找顺序:
verdor
→GOROOT/src
→GOPATH/src
- GOMODULE(go.mod)
- 由 go 命令统一的管理,用户不必关心目录结构
go get [@v...]
add dependencies to current module and install themgo mod tidy
add missing and remove unused modules- go.mod, go.sum 文件
- 依赖的位置在:
GOPATH/pkg/mod
- 旧方式迁移到 go.mod
go mod init
initialize new module in current directorygo build ./...
compile packages and dependencies
- 目录的整理
go build ./...
编译多个目录的时候不产生 exego install ./...
compile and install packages and dependencies- 生成的文件在
GOPATH/bin
面向接口
类似 duck typing,不关注内部细节,只关注行为
- java 编码时检查
- c++ 编译时检查
- python 运行时检查
- go 运行时类型检查 + 灵活性
接口由使用者定义
接口里面全是函数,不需要加 func
接口的实现是隐式的,只要实现接口里的方法
接口变量:实现者的类型 + 实现者的值(指针)
接口变量同样采用值传递(里面一般是实现者的指针,占用很小),几乎不需要使用接口的指针
指针接收者实现只能以指针方式使用,值接收者都可
查看接口变量
- 表示任何类型:interface{}
- Type Assertion
- Type Switch
常用系统接口
- Stringer
- Reader
- Writer
函数式编程
- 函数式编程 vs 函数指针
- 函数是一等公民:参数,变量,返回值都可以是函数
- 高阶函数(函数的参数是函数)
- 函数 → 闭包
- 闭包
- 函数体:局部变量 + 自由变量(函数所处环境的变量)
- “函数类型”也可以作为接收者,实现接口
- go 的闭包不需要修饰如何访问自由变量
- go 没有Lambda表达式,但是有匿名函数
资源管理与错误处理
- 资源管理 - defer 调用
- 确保调用在函数结束时发生
- 参数在 defer 语句时计算
- defer 列表为后进先出(栈)
- 何时使用
- Open / Close
- Lock / Unlock
- PrintHeader / PrintFooter
- 错误处理
- 服务器统一错误处理
- panic
- 停止当前函数执行
- 一直向上返回,执行每一层的 defer
- 如果没有遇见recover,程序退出
- recover
- 仅在 defer 调用中使用
- 获取 panic 的值
- 如果无法处理,可重新 panic
- error vs panic
- 意料之中的,使用error。如:文件打不开
- 意料之外的,使用panic。如:数组越界
- 错误处理综合示例
- defer + panic + recover
- Type Assertion
- 函数式编程的灵活应用
测试与性能调优
传统测试 vs 表格驱动测试
- 传统测试(assertEquals)
- 测试数据和测试逻辑混在一起
- 出错信息不明确
- 一旦一个数据出错测试全部结束
- 表格驱动测试
- 分离测试数据和测试逻辑
- 明确的出错信息
- 可以部分失败
- go 语言的语法使得我们更易实践表格驱动测试
- 传统测试(assertEquals)
go 测试
正确性测试
- *testing.T
go test .
命令行运行测试
代码覆盖率
性能测试
- *testing.B
go test -bench .
性能调优
go test -bench . -cpuprofile cpu.out
go tool pprof cpu.out
输入 web 命令报错,需要安装 Gvedit
1 | PS D:\HiddenTrack\LearnGo\01_foundation\code\container\nonrepeatingsubstr> go test -bench . -cpuprofile cpu.out |

- http 测试
- 通过使用假的 Request / Response
- 通过起服务器 httptest.NewServer
- 用注释写文档
- 自带的CLI
go doc
查看文档 - 在 []_test.go 文件中加入 Example
godoc
生成文档go get golang.org/x/tools/cmd/godoc
godoc -http :6060
- 自带的CLI

Goroutine
- 协程 Coroutine
- 轻量级“线程”
- 非抢占式多任务处理,由协程主动交出控制权
- 编译器/解释器/虚拟机层面的多任务,操作系统层面只有线程
- 多个协程可能在一个或多个线程上运行
- 子程序是协程的一个特例,普通函数是单向,协程是双向
- Goroutine
- Go 中的协程
- 任何函数只需加上 go 就能送给调度器运行
- 不需要在定义时区分是否是异步函数
- 调度器在合适的点进行切换
- 使用
go run -race
来检测数据访问冲突
- goroutine 可能的切换点
- I/O, select
- channel
- 等待锁
- 函数调用(有时)
- runtime.Gosched() (手动)
- 只是参考,不能保证切换,不能保证在其他地方不切换
- 运行的机制还是非抢占式的
Channel
- 基础使用
- channel
- buffered channel
- close, range
- 理论基础:Communication Sequential Process (CSP)
- Don’t communicate by sharing memory; share memory by communicating
- 使用 Channel 来等待 goroutine 结束
- 使用 Channel 来实现树的遍历(遍历结果序列化)
- 使用 Select 来进行调度(可以让 chan 变为非阻塞)
- 传统同步机制(很少使用,他们是用共享内存来通信)
- WaitGroup
- Mutex(lock/unlock)
- Cond
标准库
html/template, net/rpc
net/http
- 使用 http客户端发送请求
- 使用 http.Client 控制请求头部等
- 使用 httputil 简化工作
- http 服务器性能分析
- import _ “net/http/pprof”
- 访问 http://localhost:8888/debug/pprof/
- 或
go tool pprof http://localhost:8888/debug/pprof/ ...
fmt, log, erors
io, bufio, time
charset, encoding, unicode, utf8
strings, bytes, strconv
regexp, flag, math
os, pprof, runtime
reflect, testing
查看标准库文档
- 方式一:
godoc -http :8888
- 方式二:https://studygolang.com/pkgdoc
- 方式一:
Go 没有的元素
- 类,继承,多态,重载
- go语言拥有不同的世界观
- 在面向对象界,也流行变继承为组合的思维
- 这些面向对象的元素太容易被滥用
- go语言为组合提供了便捷的支持
- try/catch/finally
- 太多错误被当做异常
- 很多 c++ 项目组本身禁用 try/catch
- 正确的使用 try/catch 处理错误,导致代码混乱
- try/catch 在产品代码中并不能减小开发人员负担
- go 使用 defer/panic/recover 模式
- 构造函数/析构函数/RAII
- 大型项目中很少使用构造函数,多使用工厂函数
- 值类型的构造由结构体初始化语法实现
- RAII技巧性太强,隐藏了意图
- 析构函数与垃圾回收不匹配
- 泛型
- 泛型作为模板类型
- 实际想实现 duck typing
- go语言提供了对 duck typing,及接口组合的支持
- 泛型约束参数类型
- 本身非常复杂:类型通配符,covariance 等问题
- go 语言本身自带强类型的 slice, map, channel
- 使用 type assertion 甚至 go generate 来实现自己的泛型
- 泛型支持是作者唯一态度不强硬的点
- 泛型作为模板类型
- 操作符重载,assert
要抛弃“模拟”的思想,直接使用 Go 的元素来搭建系统。