新手——>高手,深入学习层面
编程思维:Go的设计哲学,用Go思维写Go
实践技巧:改善Go代码质量(Go语言标准库和优秀Go开源库的项目结构、代码风格、实现、接口、并发、错误处理等)
1、熟知Go的一切
2、项目结构、代码风格与标识符命名
3、声明、类型、语句与控制结构
4、函数与方法
5、接口
6、并发编程
7、错误处理
需求:
总结:
Go标准库代码:简单且一致,利用上下文辅助命名
strings.Reader() //[good]
strings.StringReader() //[bad]
变量、类型、函数、方法
userSlice []*User [bad] // 变量名字不要带类型信息
users []*User [good]
for i,v := range s{ // 利用上下文,让短名字携带更多信息
}
t := time.Now()
b := make([]byte,10)
声明一致性,声明聚类、就近原则
var(
a = 1
f = float32(3.14) // VS f float32 = 3.14
)
零值可用
var temp []int
temp = append(temp,2)// 切片零值可用
var m map[string]int
m = make(map[string]int,0) // map零值不可用
推荐使用field:value 的复合字面值进行结构体、map构造
p := &pipe{
wrCh:xxxx,
rdCh:xxx,
}
切片:尽量使用cap参数创建切片,动态扩容
var num []int
num := make([]int,23)
map:判断key是否存在 comma ok
if v, ok := Task[username]; !ok {
//
}
string的构造方法
s + "hello",最直观
fmt.Sprintf("%s%s",s,"hello"),性能最差,可构造多种不同变量构建字符串
strings.join(s,"hello"),平均性能佳
strings.Builder(),能预估最终字符串长度时效率最高
bytes.Buffer(bytes),类似join
turn to 第19条
turn to 17条
一只由 Rob Pike 的夫人 Renee French 设计的地鼠,从此地鼠成为世界各地 Go 程序员的象征。
2018 年 8 月 25 日,Go 1.11 版本发布。Go 1.11 是 Russ Cox 在 GopherCon 2017 大会上发表题为 “Toward Go 2” 的演讲之后的第一个 Go 版本,它与 Go 1.5 版本一样也是具有里程碑意义的版本,因为它引入了新的 Go 包管理机制:Go module。
Go 设计者推崇 “最小方式” 思维,即一件事情仅有一种方式或数量尽可能少的方式去完成
Go 语言遵从的设计哲学也是组合。Go 语言提供的最为直观的组合的语法元素是类型嵌入,实现功能的垂直扩展。
“高内聚、低耦合” 是软件开发领域亘古不变的管理复杂性的准则。Go 在语言设计层面也将这一准则发挥到极致。Go 崇尚通过组合的方式将正交的语法元素组织在一起来形成应用程序骨架,接口就是在这一哲学下诞生的语言精华。
goroutine 各自执行特定的工作,通过 channel+select 将 goroutine 组合连接起来。goroutine 调度器进行CPU调度,并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让 Go 语言更适应现代计算环境。
Go 在标准库中提供了各类高质量且性能优良的功能包,其中的 net/http、crypto/xx、encoding/xx 等包充分迎合了云原生时代关于 API/RPC Web 服务的构建需求。 gofmt 统一了 Go 语言的编码风格。
命名使用单字母,特定场景能理解,例如循环里的 i,遍历 map 的 k,v
要善用并发去解决问题,利用并发解决问题的思维需加强
func Generate(ch chan<- int) {
for i := 2; ; i++ {
ch <- i
}
}
func Filter(in <-chan int, out chan<- int, prime int) {
for {
i := <-in
if i%prime != 0 {
out <- i
fmt.Printf("prime=%d,i=%d\n", prime, i)
}
}
println("Filter over")
}
func main() {
ch := make(chan int)
go Generate(ch)
for i := 0; i < 10; i++ {
prime := <-ch
print(prime, "\n")
ch1 := make(chan int)
go Filter(ch, ch1, prime)
ch = ch1
}
}
cmd 目录:存放项目要构建的可执行文件对应的 main 包的源文件。
pkg 目录:存放项目自身要使用并且同样也是可执行文件对应 main 包要依赖的库文件。
Makefile:这里的 Makefile 是项目构建工具所用脚本的 “代表”,它可以代表任何第三方构建工具所用的脚本。
go.mod 和 go.sum:Go 语言包依赖管理使用的配置文件。
vendor 目录(可选):vendor 是 Go 1.5 版本引入的用于在项目本地缓存特定版本依赖包的机制。
gofmt、 goimports、 将 gofmt/goimports 与 IDE 或编辑器工具集成
Go 命名惯例选择了简洁命名 + 注释辅助解释的方式,而不是一个长长的名字。
- 对包导出标识符命名时,在名字中不要再包含包名,比如:strings.Reader VS strings.StringReader
- 变量名字中不要带有类型信息
- 变量声明与使用之间的距离越近越好,或者在第一次使用变量之前声明该变量。
- 利用上下文环境,让最短的名字携带足够多的信息,t代表time、b代表byte、i代表index,v代表value
- 常量使用多单词组合,如defaultMaxMemory
- 函数、类型多以多单词的复合词进行命名。
- 接口,Go 语言的惯例是用 “方法名 + er” 命名。比如:Reader、Writer
要想做好代码中变量声明的一致性,需要明确要声明的变量是包级变量还是局部变量、是否要延迟初始化、是否接受默认类型、是否为分支控制变量,并结合聚类和就近原则。
显式声明推荐方式
- var a = int32 (17)
- var f = float32 (3.14)
Go 的无类型常量恰恰就拥有像字面值这样的特性,该特性使得无类型常量在参与变量赋值和计算过程时无须显式类型转换,从而达到简化代码的目的
iota 是 Go 语言的一个预定义标识符,它表示的是 const 声明块(包括单行声明)中每个常量所处位置在块中的偏移值(从零开始)。
- 位于同一行的 iota 即便出现多次,其值也是一样的
- 如果要略过 某个iota值,_ = itota,操作
零值可用,如切片
Go 推荐使用 field:value 的复合字面值形式对 struct 类型变量进行值构造
切片的底层结构是数组,相当于数组的描述符(array、len、cap)
初始零值,零值可用
createData := make(map[string]interface{}) var verifyTemp *entity.ConvergeTaskStatics
- 1
- 2
- 3
动态扩容:
defects := make([]model.Defect, 0) defects = append(defects, defect) snapshots := make([]string, 0) snapshots = append(snapshots, path)
- 1
- 2
- 3
- 4
- 5
- 注意:一旦切片cap触碰数组的上界,再对切片进行append操作,切片会创建新的数组与原数组解除绑定
- 如果能预估出切片底层数组元素数量,尽量使用cap参数创建切片,减少动态扩容带来的性能消耗
- 不支持零值可用,未赋值未nil,使用报panic
var m map[string]int m["key"] = 1 //panic:assignment to entry in nil map
- 1
- 2
- 增删查改的操作,遍历无序
- 注意查key不存在会默认返回零值,此时无法确定对应的key是否存在,使用comma ok用法读取map中的值
if v, ok := TaskStatics[staticInVer.UserName]; !ok { // }
- 1
- 2
- 3
- map的内部实现与java类似,map扩容,非线程安全不支持并发写
- 同样为减少频繁扩容带来的损耗,建议使用cap参数创建map
- string只读,进行切片之后会创建新的底层存储,切片修改对原string没有影响
- 标准库:strings、strconv
- 内部表示,底层的描述符
type stringStruct struct{ str unsafe.Pointer len int }
- 1
- 2
- 3
- 4
- string的构造方法
s + "hello",最直观 fmt.Sprintf("%s%s",s,"hello"),性能最差,可构造多种不同变量构建字符串 strings.join(s,"hello"),平均性能佳 strings.Builder(),能预估最终字符串长度时效率最高 bytes.Buffer(bytes),类似join
- 1
- 2
- 3
- 4
- 5
import后的部分是路径
编译过程使用的是编译单元所依赖的包源码
包源码搜索路径:基本搜索路径GOROOT/GOPATH/module-aware+包导入路径import
n0,n1 = 1,2
n0,n1 = n0+n1,n0
包级变量初始化按照变量声明顺序进行,每一轮寻找ready for initialization
var ( a = c + b b = f() _ = f() c = f() d = 3 ) func f() int{ d++ return d } func main(){ fmt.Println(a,b,c,d) }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
1、第一阶段:等号两端表达式求值,上述问题中,只有右端有n0+n1和n0两个表达式,因此直接将值带入,得到求值结果。求值后,语句可以看成:n0, n1 = 3, 1;
2、第二阶段:赋值。n0 =3, n1 = 1
- 使用 if 控制语句时应遵循“快乐路径”原则,“快乐路径”即成功逻辑的代码执行路径。原则要求:
- 当出现错误时,快速返回;
- 成功逻辑不要嵌入 if-else 语句中;
- “快乐路径”的执行逻辑在代码布局上始终靠左,这样读者可以一眼看到该函数的正常逻辑流程;
- “快乐路径”的返回值一般在函数最后一行。
扁平,可读性强 func doSomething() error{ if 错误逻辑1{// 出现错误时快速返回 return err } 成功逻辑2// 成功逻辑不要嵌入 if-else 语句中; if 错误逻辑2{ return err } 成功逻辑2 } 嵌套,可读性不佳 func doSomething() error{ if 成功逻辑1{ if 成功逻辑2{ return err }else{ // 错误逻辑1 } }else{ // 错误逻辑2 } }
- 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
- for range的坑
迭代变量在 for range 的每次循环中都会被重用,参与循环的是数组的副本
var m = [...]int{1, 2, 3, 4, 5} for i, v := range m' { } // 这样使用 for i, v := range &m { }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
导包遵循深度优先,把所有依赖的加载
init函数做包级数据的初始化和初始状态检查工作
函数可以像普通整型值一样被创建和使用
待释放的资源个数很多,defer让资源释放优雅且不易出错
绑定方法,建议绑定指针类型
func(reveiver T\*T)MethodName(参数列表)(返回值列表){ }
- 1
- 2
- 3
隐式的接口实现方式
- T 的方法集合 = reveiver为T
- *T 的方法集合 = receiver为 T 和 *T的方法
// CheckCollectTask 检查采集上传数据 func (a *appCollectTaskSrvStruct) CheckCollectTask(){}
- 1
- 2
接口嵌接口,实现复杂功能的组合,当有重名接口的时候就近原则,也可以使用别名
现状:很少使用变长参数设计实现函数或方法
func fmt.Println(a ...interface{}) (n int, err error) func append(slice []Type,elems ...Type) []Type
- 1
- 2
- 利用变长参数实现函数重载
- 实现功能选项(现在多用的option封装参数)
type iface struct {//有方法的接口类型 tab *itab data unsafe.Pointer } type eface struct {// 无方法的接口类型 _type *_type // 存储接口的信息 data unsafe.Pointer }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
空接口类型变量:只有在 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能画等号
接口越小,抽象程度越高,越易于实现、复用,越偏向业务层,抽象难度越高
type Reader interface{ Read(...) }
- 1
- 2
- 3
先抽象出接口,再拆分小接口
空接口没有任何入参的信息,避开类型检查,导致运行时才发现问题
深入理解接口的组合逻辑,使用垂直组合、水平组合
抽取接口,让接口称为代码与单元测试的桥梁(TDD)
并发关乎结构(生产线条数),并行关乎执行(执行人数)
进行代码执行单元的分解,代码=代码片段的组合,每个片段并发执行
G-P-M调度模型
- G保存Goroutine的运行堆栈,即并发任务状态。G并非执行体,每个G需要绑定到P才能被调度执行。
- 对G来说,P相当于CPU核,G只有绑定到P(在P的local runq中)才能被调度。对M来说,P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)
- M:OS线程抽象,负责调度任务,和某个P绑定,从P中不断取出G,切换堆栈并执行
- 基于共享内存
- 基于通信顺序进程模型 CSP
- goroutine,对应P,封装数据的处理逻辑,作为基本执行单元
- channel,用于goroutine的通信和同步
- select,协调多路channel
- channel + select
- 无缓冲channel在信号传递、替代锁同步的应用
- 有缓冲channel在消息对列、计数信号的应用
- 构造错误值:error.New,fmt.Errorf
DatabaseError error = errors.New("数据库错误,请联系开发人员") ServerError error = errors.New("服务器错误,请联系开发人员")
- 1
- 2
- 透明错误处理策略:只要一发生错误就进入错误处理逻辑,降低错误与错误处理之间的耦合
err := doSomething() if err != nil { return err }
- 1
- 2
- 3
- 4
- 错误哨兵策略,swtich + 自定义err 判断选择路径
- 错误值类型检视策略,获取更多的错误值上下文信息
err.(type) errors.As(err,&错误类型) // 判断两个err是否一致 errors.Is(err,错误类型)
- 1
- 2
- 3
- 错误行为特征检视策略,公共接口定义的错误信息 net.Error
type Error interface{ error Timeout() bool Temporary() bool }
- 1
- 2
- 3
- 4
- 5
显示错误处理是go的特点
- 改善视觉呈现
- 减少重复次数
- 重构
- check/handle机制,另外封装成函数
- 使用内置error
尽可能少使用panic
典型应用
- 断言角色,提示潜在bug,
switch 表达式{ case 1: case 2: panic xxx //代码逻辑不会走到这里,但是走到了这里,说明有潜在bug }
- 1
- 2
- 3
- 4
- 5
- 简化错误处理逻辑(check/hadle机制)
- 使用recover捕获panic,防止goroutinue意外退出 http server
理解panic的输出栈帧信息,帮助快速定位bug