发布于 

Go 基础与语法特性 🚪

基本语法 + 函数式编程 + 面向接口 + 并发编程

参考资料

Gogogo!

国内镜像

1
2
3
4
5
# Windows
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
$env:GO111MODULE = "on"
$env:GOPROXY = "https://goproxy.cn"

goimports

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
2
3
4
5
6
7
// Package ${GO_PACKAGE_NAME} -----------------------------
// @file : ${FILE_NAME}
// @author : hcjjj
// @contact : hcjjj@foxmail.com
// @time : ${DATE} ${TIME}
// -------------------------------------------
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/srcGOPATH/src
  • GOVERDOR
    • 每个项目有自己的vendor目录,存放第三方库
    • 大量第三方依赖管理工具:glide, dep, go dep, …
    • 依赖查找顺序: verdorGOROOT/srcGOPATH/src
  • GOMODULE(go.mod)
    • 由 go 命令统一的管理,用户不必关心目录结构
    • go get [@v...] add dependencies to current module and install them
    • go 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 directory
    • go build ./... compile packages and dependencies
  • 目录的整理
    • go build ./... 编译多个目录的时候不产生 exe
    • go 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 语言的语法使得我们更易实践表格驱动测试
  • go 测试

    • 正确性测试

      • *testing.T
      • go test . 命令行运行测试
    • 代码覆盖率

    • 性能测试

      • *testing.B
      • go test -bench .
  • 性能调优

    • go test -bench . -cpuprofile cpu.out
    • go tool pprof cpu.out

输入 web 命令报错,需要安装 Gvedit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS D:\HiddenTrack\LearnGo\01_foundation\code\container\nonrepeatingsubstr> go test -bench . -cpuprofile cpu.out
goos: windows
goarch: amd64
pkg: code/container/nonrepeatingsubstr
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkSubstr-16 396 3045160 ns/op
--- BENCH: BenchmarkSubstr-16
nonrepeating_test.go:41: len(s) = 491520
nonrepeating_test.go:41: len(s) = 491520
nonrepeating_test.go:41: len(s) = 491520
PASS
ok code/container/nonrepeatingsubstr 1.701s
PS D:\HiddenTrack\LearnGo\01_foundation\code\container\nonrepeatingsubstr> go tool pprof cpu.out
Type: cpu
Time: Nov 18, 2023 at 2:01pm (CST)
Duration: 1.66s, Total samples = 1.42s (85.78%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web

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

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 服务器性能分析
  • fmt, log, erors

  • io, bufio, time

  • charset, encoding, unicode, utf8

  • strings, bytes, strconv

  • regexp, flag, math

  • os, pprof, runtime

  • reflect, testing

  • 查看标准库文档

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 的元素来搭建系统。