golang 学习笔记

我的 golang 学习笔记。好几年前就说要学了,现在终于兑现。

开发环境

安装

https://studygolang.com/dl

go version
go env

国内镜像

https://goproxy.cn/

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

goimports

go get -v golang.org/x/tools/cmd/goimports

IDE

  • IDEA 安装 go 和 file watcher 插件
  • 新建项目使用 goimports 模板
  • filewather 增加 goimports,配置默认
  • 快速生成变量快捷键:ctrl+alt+v

基础语法

变量定义

  • 变量类型写在后面,名字写在前面,形似 typescript
  • 类型可以推断
  • 没有 char,只有 rune
  • 原生支持复数类型

var

var a ,b, c bool
var s1, s2 string = "hello", "world"
  • 可放在包内或函数内
  • 可以用 var() 集中定义变量
  • 类型可以自动推断

:=

只能在函数内使用

内建变量类型

  • bool, string
  • (u)int, (u)int8, … (u)int64, uintptr (指针)
  • byte, rune (char)
  • float32, float64, complex64, complex128 (复数)

类型转换是强制的,没有隐式转换。

var c int = int(math.Sqrt(float64(a*a + b*b)))

常量

const

const filename = "abc.txt"

常量数值可以作为各种类型使用:

const (
a, b = 3, 4
)
var c int = int(math.Sqrt(a*a + b*b))

枚举

  • 用 const 定义枚举。
  • 可以是固定值,也可以用 iota 自增。iota 可以参与运算。
const (
b = 1 << (10 * iota)
kb
mb
gb
tb
pb
)

条件

if

  • if 不需要括号
  • 条件内可以定义变量,变量的作用域局限于 if
const filename = "abc.txt"
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}

switch

  • switch **默认 break**,除非加 fallthrough
  • switch 可以没有表达式,条件写在 case
func grade(score int) string {
g := ""
switch {
case score < 0 || score > 100:
panic(fmt.Sprintf("Wrong score: %d", score))
case score < 60:
g = "F"
case score < 80:
g = "C"
case score < 90:
g = "B"
case score <= 100:
g = "A"
}
return g
}

循环

for

  • for 不需要括号
  • for 可以省略初始条件(相当于 while)、结束条件、递增表达式
for n := 100 ; n > 0; n /= 2 {
// todo
}
for scanner.Scan() {
fmt.Println(scanner.Text())
}

函数

  • 返回值类型写在最后面(类似 typescript)
  • 函数可以返回多个值(一般用法为第二个参数返回 error)
  • 函数返回多个值时可以起名
  • 函数可以作为参数
  • 有可变参数列表
  • 有匿名函数
  • 没有默认参数、可选参数、函数重载等
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 sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}

func apply(op func(int, int) int, a, b int) int {
p := reflect.ValueOf(op).Pointer()
opName := runtime.FuncForPC(p).Name()
fmt.Printf("Calling func %s with args (%d, %d)\n", opName, a, b)
return op(a, b)
}

fmt.Println(apply(func(a int, b int) int {
return int(math.Pow(float64(a), float64(b)))
}, 3, 4))

指针

  • go 指针不能运算
  • 相比于其它语言的基础类型值传递、复杂类型引用传递,go 语言只能进行值传递,引用传递要显式声明
func swap(a, b *int) {
*b, *a = *a, *b
}

swap(&a, &b)

内建容器

Array 数组

  • [10]int[20]int不同的类型
  • 数组传入函数中的是,不是引用,值会进行拷贝
  • go 语言中一般不直接使用数组,而是使用 slice 切片

定义

数量写在类型前

var arr1 [5]int
arr2 := [3]int{1, 2, 3}
arr3 := [...]int{2, 4, 6, 8, 10}

var grid [4][5]int

遍历

for i := 0; i < len(arr3); i++ {
fmt.Println(arr3[i])
}

for i, v := range arr3 {
fmt.Println(i, v)
}

Slice 切片

  • slice 不是值类型,它是 array 的一个视图 (view),对 slice 的改动会反映到 array
  • slice 可以向后扩展,但不能向前扩展
  • s[i] 不可以超越 len(s),向后拓展可以超越 len(s) 但不能超越 cap(s)
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

fmt.Println("arr[2:6] =", arr[2:6])
fmt.Println("arr[:6] =", arr[:6])
fmt.Println("arr[2:] =", arr[2:])
fmt.Println("arr[:] =", arr[:])

func updateSlide(s []int) {
s[0] = 100
}
updateSlide(s1)
fmt.Println("Extending slide")
arr[0], arr[2] = 0, 2
fmt.Println("arr =", arr)
s1 = arr[2:6]
s2 = s1[3:5]
//fmt.Println(s1[4])
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))

create

var s []int // nil

这种方式创建的 slice,初始值等于 nil

往里面添加元素时,lencap 是动态的。

另一种方法:

// 指定初始 len
s2 := make([]int, 16)
// 指定初始 len cap
s3 := make([]int, 10, 32)

append

有内建函数:

s = append(s, val)

添加元素时如果超越了 cap,系统会重新分配更大的底层数组

由于值传递的关系,必须接收 append 的返回值

copy

copy(s2, s1)

delete

删除下标为 3 的元素:

s2 = append(s2[:3], s2[4:]...)

shift/pop

// shift
front := s2[0]
s2 = s2[1:]
// pop
tail := s2[len(s2)-1]
s2 = s2[:len(s2)-1]

Map

操作

  • 创建:make(map[string]int)
  • 获取:m[key],字符不存在返回 zero value
  • 判断 key 是否存在:value, ok := m[key]
  • 删除:delete(m, key)
  • 遍历:for k, v := range m,无序的
  • 获取长度:len(m)
  • map 使用哈希表,key 必须可以比较相等,除了 slice map function 以外的内建类型都可以作为 key,不包含上述字段的 struct 也可以

例:寻找最长的不含有重复字符的子串

func longest(s string) int {
lastOccurred := make(map[byte]int)
start := 0
maxLength := 0
for i, c := range []byte(s) {
if last, ok := lastOccurred[c]; ok && last >= start {
start = last + 1
}
if (i - start + 1) > maxLength {
maxLength = i - start + 1
}
lastOccurred[c] = i
}
return maxLength
}

String

  • for i, b := range []byte(s) 得到的是 8 位 byte
  • for i, b := range []rune(s) 得到的是 utf8 解码后的字符
  • 获取 utf8 字符串长度:utf8.RuneCountInString(s)
  • 字符串操作库:strings.ToUpper / strings.xxx

面向对象

  • 仅支持封装,不支持继承和多态
  • 没有 class,只有 struct

struct

  • 无需关注结构体是储存在栈还是堆上
  • 知识点:nil 指针也调用方法

定义

  • 值定义与成员方法的定义方式与传统方式有区别
  • 成员方法定义只有使用指针接收者(引用传递)才能改变结构的内容
  • 结构过大要考虑使用指针接收者(拷贝成本)
  • 注意方法的一致性:最好要么都是指针接收者,要么都是值接收者
type TreeNode struct {
value int
left, right *TreeNode
}

// 这里是值传递
func (node TreeNode) print() {
fmt.Println(node.value)
}

// 这里是引用传递
func (node *TreeNode) setValue(value int) {
// 不是 node->value
node.value = value
}

root.print()
root.setValue(1)

创建

var root TreeNode
root.left = &TreeNode{}
// 指针也可以直接“点”
root.left.right = &TreeNode{4, nil, nil}
root.right = &TreeNode{value: value}
func createNode(value int) *TreeNode {
return &TreeNode{value: value}
}

root.right = createNode(3)

例子:遍历树

func (node *TreeNode) travel() {
if node == nil {
return
}
// 即使 node.left 是 nil,它也能调用方法!
node.left.travel()
node.print()
node.right.travel()
}

包与封装

  • 每个目录是一个包
  • “main 包”包含可执行入口
  • 为结构定义的方法必须放在同一个包内,可以是不同的文件

封装

  • 名字 CamelCase
  • 首字母大写代表 public
  • 首字母小写代表 private
// node.go
package tree

import "fmt"

type Node struct {
Value int
Left, Right *Node
}

func CreateNode(value int) *Node {
return &Node{Value: value}
}

// ...
// travalsal.go
package tree

func (node *Node) Travel() {
// ...
}
// main.go
package main

import (
"fmt"
"learngo/tree"
)

func main() {
var root tree.Node
// ...
}

扩展

方法一:别名(简单)
package queue

type Queue []int

func (q *Queue) Push(value int) {
*q = append(*q, value)
}

func (q *Queue) Pop() int {
pop := (*q)[0]
*q = (*q)[1:]
return pop
}

func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
方法二:组合(常用)

与 js 的 {node: ...node} 类似,没有其它处理:

type myTreeNode struct {
node *tree.Node
}

func (node *myTreeNode) postOrder() {
if node == nil || node.node == nil {
return
}

left := myTreeNode{node.node.Left}
left.postOrder()

right := myTreeNode{node.node.Right}
right.postOrder()

node.node.Print()
}

myNode := myTreeNode{&root}
myNode.postOrder()
方法三:内嵌(少写代码)
  • 其实是一个语法糖,编译器自动将字段以 Node 命名了。并且:Node 的属性和方法会自动提升到顶层。
  • 与继承类似,可以看作继承行为的模拟,但有本质区别。
  • 可以重写方法,重写的方法称作 shallowed method,而非 override,调用原 struct 方法使用 root.Node.xxx,相当于 super
type myTreeNodeEmbedded struct {
*tree.Node
}

func (node *myTreeNodeEmbedded) postOrder() {
// 必须!
if node == nil || node.Node == nil {
return
}

left := myTreeNodeEmbedded{node.Left}
left.postOrder()

right := myTreeNodeEmbedded{node.Right}
right.postOrder()

node.Print()
}

依赖管理

三个阶段 GOPATH/GOVENDOR/go mod

GOPATH

  • 默认在 ~/go (linux, unix) %USERPROFILE%\go (windows)
  • 目录下面必须有 src 文件夹,作为所有依赖与项目的根目录(Google 将 20 亿行代码,9 百万个文件放在了一个 repo 里)
  • GOPATH 可以更改
  • GOPATH 内的两个项目无法依赖同一个库的不同版本
export GOPATH=/path/to/go
export GO111MODULE=off
// in src/proj folder
go get -u go.uber.org/zap

GOVENDER

GOPATH 内的两个项目无法依赖同一个库的不同版本

  • 为了解决这个问题诞生了 GOVENDER,只需要在 project 里面新建 vender 文件夹,依赖就会从首先 vender 文件夹内查找
  • 有许多配套的依赖管理工具

GO MOD

go 命令统一管理,不必关心目录结构

初始化

相当于 npm init

go mod init modname

安装/升级依赖

与 GOPATH 一样:

go get -u go.uber.org/zap@1.12.0

依赖管理 go.mod

相当于 package.json

module learngo

go 1.16

require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.0 // indirect
)

依赖锁 go.sum

相当于 package-json.lock

github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
// ...

依赖锁瘦身

(也许相当于 npm uninstall

命令:

go mod tidy

构建

注意后面是 3 个点

go build ./...

旧项目迁移到 gomod

go mod init
go build ./...

接口

type retriever interface {
Get(string) string
}

duck typing

  • “长得像鸭子,那么就是鸭子”
  • 描述事物的外部行为,而非内部结构
  • go 属于结构化类型系统,类似 duck typing(但本质上不是)
  • 同时具有 python c++ 的 duck typing 灵活性
  • 又具有 java 的类型检查

接口的定义

  • 接口由使用者定义
  • 接口的实现是隐式的,只要实现了里面的内容即可

接口变量

  • 接口变量自带指针
  • 接口变量同样采用值传递
  • 指针接收者实现只能以指针方式使用,值接收者都可
  • 接口变量里面有实现者的类型和值。
fmt.Printf("%T %v\n", r, r)
// test.Retriever {EMT}

接口的真实类型可以通过 switch 获取:

switch v := r.(type) {
case *infra.Retriever:
fmt.Println(v.TimeOut)
case test.Retriever:
fmt.Println(v.Content)
}

也可以通过 type assertion 获取:

// type assertion
if realRetriever, ok := r.(*infra.Retriever); ok {
fmt.Println(realRetriever.UserAgent)
} else {
fmt.Println("type incorrect")
}

实现者的值也可以换成实现者的指针:

type Retriever struct {
UserAgent string
TimeOut time.Duration
}

func (r *Retriever) Get(url string) string {
// ...
}

type retriever interface {
Get(string) string
}

var r retriever = &infra.Retriever{
TimeOut: time.Minute,
UserAgent: "Mozilla",
}

fmt.Printf("%T %v\n", r, r)
// *infra.Retriever &{Mozilla 1m0s}

表示任意类型的接口

类似 any

interface{}

// for example
type Queue []interface{}

接口的强制类型转换

type Queue []interface{}

func (q *Queue) Push(value interface{}) {
*q = append(*q, value.(int))
}

接口的组合

type retriever interface {
Get(string) string
}

type poster interface {
Post(string, map[string]string) string
}

type retrieverPoster interface {
retriever
poster
}

常用内置接口

stringer

相当于 toString

type Retriever struct {
Content string
}

func (r *Retriever) String() string {
return fmt.Sprintf("Test Retriever: {Content=%s}", r.Content)
}

fmt.Printf("%T %v\n", r, r)
// *test.Retriever Test Retriever: {Content=EMT}

reader/writer

func printFile(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
printFileContent(file)
}

func printFileContent(reader io.Reader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}

printFile("./basic/abc.txt")

printFileContent(strings.NewReader(`EMT
YES!
`))

函数式编程

例子:累加器

func adder() func(v int) int {
sum := 0
return func(v int) int {
sum += v
return sum
}
}

func main() {
add := adder()
for i := 0; i < 10; i++ {
fmt.Println(add(i))
}
}

例子:斐波那契数列

func fib() func() int {
before, after := 0, 1
return func() int {
fmt.Printf("before=%d, after=%d\n", before, after)
before, after = after, after+before
return after
}
}

func main() {
f := fib()
for i := 0; i < 10; i++ {
f()
}
}

例子:二叉树遍历

func (node *Node) TravelFunc(f func(*Node)) {
if node == nil {
return
}

node.Left.TravelFunc(f)
f(node)
node.Right.TravelFunc(f)
}

func (node *Node) Count() int {
count := 0
node.TravelFunc(func(node *Node) {
count++
})
return count
}

错误处理和资源管理

defer 调用

  • defer 调用确保在函数结束时发生
  • defer 先进后出(栈)
  • defer 参数在语句时计算(非结算时)

何时使用 defer:

  • open/close
  • lock/unlock
  • print header/footer

错误处理

file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
if err != nil {
if e, ok := err.(*os.PathError); !ok {
fmt.Println(err)
} else {
fmt.Printf("Op: %s, Path: %s, Err: %s\n", e.Op, e.Path, e.Err)
}
}

统一的错误处理

以 http server 为例,思路是让 controller 可以直接返回 error,而 error 在外层的包裹函数内统一处理:

type handler func(w http.ResponseWriter, r *http.Request) error

func errWrapper(h handler) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err != nil {
log.Printf("%s\n", err.Error())
code := http.StatusInternalServerError
switch {
case os.IsNotExist(err):
code = http.StatusNotFound
}
http.Error(w, http.StatusText(code), code)
}
}
}

func main() {
http.HandleFunc("/list/", errWrapper(controller.ListFile))

err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}

panic

  • 停止当前函数执行
  • 一直向上返回,执行每一层的 defer
  • 如果没有遇见 recover,程序退出

recover

  • 仅在 defer 中使用
  • 获取 panic 的值
  • 如果无法处理,可以重新 panic
defer func() {
e := recover()
if err, ok := e.(error); ok {
fmt.Println("catch error: ", err)
} else {
panic(errors.New("don't know what to do: " + fmt.Sprintf("%v", e)))
}
}()

//panic(errors.New("..."))
//panic(123)
b := 0
a := 5 / b
fmt.Println(a)
//catch error: runtime error: integer divide by zero

error vs. panic

  • 尽量使用 error
  • 意料之中的:error。如:文件打不开
  • 意料之外的:panic。如:数组越界

自定义错误

type UserError string

func (u UserError) Error() string {
return u.Message()
}

func (u UserError) Message() string {
return string(u)
}

func ListFile(writer http.ResponseWriter, request *http.Request) error {
if strings.Index(request.URL.Path, prefix) < 0 {
// 外部可以使用 type assertion 判断,并输出自定义 message
return UserError("path muse starts with " + prefix)
}

// ...
}

统一的错误处理(进阶)

func errWrapper(h handler) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
log.Printf("%s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} else {
log.Printf("%v\n", r)
}
}
}()

err := h(w, r)
if err != nil {
log.Printf("%s\n", err.Error())
if userError, ok := err.(controller.UserError); ok {
http.Error(w, userError.Message(), http.StatusBadRequest)
return
}

code := http.StatusInternalServerError
switch {
case os.IsNotExist(err):
code = http.StatusNotFound
}
http.Error(w, http.StatusText(code), code)
}
}
}

测试

单元测试

$ go test .
ok learngo/basic/basic 0.087s

单元测试文件以 _test.go 结尾,如 some_function_test.go,ide 可自动识别单元测试文件

如果要引用私有方法,需要跟方法在同一个 package

命令行执行:go test .

package main

import (
"testing"
)

func TestTriangle(t *testing.T) {
tests := []struct{ a, b, c int }{
{3, 4, 5},
{5, 12, 13},
{8, 15, 17},
{12, 35, 37},
{30000, 40000, 50000},
}
for _, tt := range tests {
if c := calcTriangle(tt.a, tt.b); c != tt.c {
t.Errorf("calcTriangle(%d,%d), expected %d, got %d", tt.a, tt.b, tt.c, c)
}
}
}

覆盖率

go 自带覆盖率工具

# 生成报告
go test . -coverprofile=c.out
# 查看报告
go tool cover -html=c.out

性能测试

性能测试方法需要以 Benchmark 开头:

func BenchmarkNonRepeating(b *testing.B) {
// 运行一秒钟,具体次数由 go 决定
for i := 0; i < b.N; i++ {
assert.Equal(b, 8, longest("黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花"))
}
}
go test -bench .

pprof

web 报告需要安装 https://www.graphviz.org/download/

go test -bench . -cpuprofile cpu.out
go tool pprof cpu.out
# 打开基于网页的性能报告
(pprof) web

http 单元测试

func errorPanic(w http.ResponseWriter, r *http.Request) error {
panic(123)
}

func errorNotExist(w http.ResponseWriter, r *http.Request) error {
return os.ErrNotExist
}

// ...

var tests = []struct {
h handler
code int
message string
}{
{errorNotExist, http.StatusNotFound, http.StatusText(http.StatusNotFound)},
// ...
}

方式一:测代码逻辑

使用 fake req\res mock 测试:

func TestErrorWrapper(t *testing.T) {
for _, test := range tests {
h := errWrapper(test.h)
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "https://google.com", nil)
h(res, req)
all, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, test.message, strings.TrimSpace(string(all)))
assert.Equal(t, test.code, res.Code)
}
}

方式二:真实服务器测试

func TestServer(t *testing.T) {
for _, test := range tests {
h := errWrapper(test.h)
s := httptest.NewServer(http.HandlerFunc(h))
res, _ := http.Get(s.URL)
all, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, test.message, strings.TrimSpace(string(all)))
assert.Equal(t, test.code, res.StatusCode)
}
}

生成文档

go get golang.org/x/tools/cmd/godoc
// 启动文档服务器
godoc -http :6060

编写注释

// Queue is a FIFO queue
// q := Queue{1,2,3}
type Queue []interface{}

// Push add an item into queue
func (q *Queue) Push(value interface{}) {
*q = append(*q, value)
}

// Pop remove an item from queue
func (q *Queue) Pop() interface{} {
pop := (*q)[0]
*q = (*q)[1:]
return pop
}

// IsEmpty check if queue is empty
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}

编写样例

样例代码也是一种单元测试,以 Example 开头,并且以 Output 表示输出。输出不正确时 test 会不通过。

(我在想,go 的约定大于配置是不是做得有点太激进了?)

func ExampleQueue_Push() {
s := Queue{1, 2}

s.Push(3)
fmt.Println(s.Pop())
fmt.Println(s.Pop())
fmt.Println(s.IsEmpty())
fmt.Println(s.Pop())
fmt.Println(s.IsEmpty())

// Output:
// 1
// 2
// false
// 3
// true
}

goroutine

  • go 语言使用 goroutine 来实现并发编程。
  • 任何函数加入 go 关键字就能送给调度器运行
  • 不需要在定义时区分是否是异步函数
  • 调度器在合适的时机自动进行切换
  • 开多少个线程、goroutine 分布在哪个线程上是由调度器自动决定的

协程 Coroutine

  • 轻量级“线程”
  • 非抢占式多任务处理,由协程主动交出控制权
  • 编译器、解释器、虚拟机层面的多任务
  • 多个协程可以在一个或多个线程上运行

race condition

检查数据读写冲突:

go run -race gorouting.go

goroutine 可能的切换点

只是参考,不能保证切换,不能保证在其他地方不切换

  • io, select
  • channel
  • waiting for lock
  • function call
  • runtime.Gosched()

channel

不要通过共享内存来通信——通过通信来共享内存。

  • channel 是 goroutine 之间通信的桥梁。
  • channel 可以定义可收发,也可以定义仅收、仅发
  • channel 收值可以用死循环,也可以用 range,也可以用条件。但是用死循环的话要注意:channel 关闭后依然会不断发送消息
  • channel 可以关闭 close(c)
  • channel 可以定义缓冲区,make(chan int, 3)
  • 注意:channel 收发是同步的,也就是说:
  • 当发消息的时候,发送方要等待消息被接受才会继续执行
  • 当收消息的时候,接收方要等待消息被发送才会继续执行
  • 如果在 goroutine 之间只发不收或只收不发,会出现死锁

func worker(id int, c chan int) {
// 当 channel 收到值,且未关闭时
for n := range c {
fmt.Printf("worker %d receive %c\n", id, n)
}
}

// 返回值为只收 channel
func createWorker(id int) chan<- int {
c := make(chan int)
go worker(id, c)
return c
}

func channelDemo() {
var cs = make([]chan<- int, 10)
for i := 0; i < 10; i++ {
cs[i] = createWorker(i)
}
for i := 0; i < 10; i++ {
// 向 channel 发送数据
cs[i] <- 'a' + i
}
for i := 0; i < 10; i++ {
cs[i] <- 'A' + i
}
time.Sleep(time.Millisecond)
}

等待任务结束

方式一:使用 channel

type worker struct {
id int
in chan int
done chan bool
}

func doWork(w worker) {
for n := range w.in {
fmt.Printf("worker %d receive %c\n", w.id, n)
w.done <- true
}
}

func createWorker(id int) worker {
w := worker{
in: make(chan int),
done: make(chan bool),
id: id,
}
go doWork(w)
return w
}

func channelDemo() {
var w = make([]worker, 10)
for i := 0; i < 10; i++ {
w[i] = createWorker(i)
}
for i, worker := range w {
worker.in <- 'a' + i
}
for _, worker := range w {
<-worker.done
}
for i, worker := range w {
worker.in <- 'A' + i
}
for _, worker := range w {
<-worker.done
}
}

func main() {
channelDemo()
}

方式二:使用 WaitGroup

type worker struct {
id int
in chan int
done func()
}

func doWork(w worker) {
for n := range w.in {
fmt.Printf("worker %d receive %c\n", w.id, n)
w.done()
}
}

func createWorker(id int, wg *sync.WaitGroup) worker {
w := worker{
in: make(chan int),
done: wg.Done,
id: id,
}
go doWork(w)
return w
}

func channelDemo() {
var w = make([]worker, 10)
wg := sync.WaitGroup{}

for i := 0; i < 10; i++ {
w[i] = createWorker(i, &wg)
}
//wg.Add(20)
for i, worker := range w {
wg.Add(1)
worker.in <- 'a' + i
//wg.Add(1) 错误!应该先 Add 再发数据
}
//wg.Wait()
for i, worker := range w {
wg.Add(1)
worker.in <- 'A' + i
}
wg.Wait()
}

func main() {
channelDemo()
}

使用 channel 进行树遍历

func (node *Node) TravelFunc(f func(*Node)) {
// 普通的回调式遍历...
}

func (node *Node) TravelWithChannel() chan *Node {
c := make(chan *Node)
go func() {
defer close(c)
node.TravelFunc(func(node *Node) {
c <- node
})
}()
return c
}

func main() {
var root tree.Node
// ...
max := 0
node := root.TravelWithChannel()
for n := range node {
if n.Value > max {
max = n.Value
}
}
fmt.Println("Max: ", max)
}

使用 select 进行调度

  • select 的使用,可以不加锁控制任务的执行
  • 定时器的使用:定时器返回的也是 channel
  • 在 select 中使用 nil channel:nil channel 永远不会被 select
tm := time.After(time.Second * 10)
tick := time.Tick(time.Second)
for {
var activeChannel chan<- int
var activeValue int
if len(values) > 0 {
activeChannel = w.in
activeValue = values[0]
}

select {
case <-tm:
fmt.Println("program exit")
return
case n := <-c1:
values = append(values, n)
//fallthrough
case n := <-c2:
values = append(values, n)
case activeChannel <- activeValue:
values = values[1:]
case <-tick:
fmt.Println("len(values)=", len(values))
case <-time.After(time.Millisecond * 800):
fmt.Println("timeout")
}

}

传统的同步机制

  • WaitGroup
  • Mutex
  • Cond

如果不加锁,使用 -race 执行会发生 data race:

type atomicInt struct {
Value int
Lock sync.Mutex
}

func (i *atomicInt) add(v int) {
i.Lock.Lock()
defer i.Lock.Unlock()
i.Value += v
}

func (i *atomicInt) getValue() int {
i.Lock.Lock()
defer i.Lock.Unlock()
return i.Value
}

func main() {
i := atomicInt{}
i.add(1)
go func() {
i.add(1)
}()
time.Sleep(time.Millisecond)
fmt.Println(i.getValue())
}

并发编程模式

生成器

func msgGen(id string) <-chan string {
c := make(chan string)
i := 0
go func() {
for {
time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
c <- fmt.Sprintf("generator %s sended %d", id, i)
i++
}
}()
return c
}

fanIn

将多个 channel 的消息合并为一个输出以避免阻塞:

func fanIn(channels ...<-chan string) <-chan string {
c := make(chan string)
for _, ch := range channels {
go func(ch <-chan string) {
for {
c <- <-ch
}
}(ch)
}
return c
}

func main() {
m := fanIn(
msgGen("svc1"),
msgGen("svc2"),
msgGen("svc3"),
msgGen("svc4"),
msgGen("svc5"),
)
for {
fmt.Println(<-m)
}
}

fanIn by select

func fanInBySelect(c1 <-chan string, c2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
select {
case n := <-c1:
c <- n
case n := <-c2:
c <- n
}
}
}()
return c
}

任务的控制

非阻塞等待

func noneBlockingWait(c <-chan string) (string, bool) {
select {
case n := <-c:
return n, true
default:
return "", false
}
}

超时机制

func timeoutWait(c <-chan string, timeout time.Duration) (string, bool) {
select {
case n := <-c:
return n, true
case <-time.After(timeout):
return "", false
}
}

优雅退出

当消息的内容无所谓时,channel 可以用空的 struct,体积比 boolean 更小。

func msgGen(id string, done chan struct{}) <-chan string {
c := make(chan string)
i := 0
go func() {
for {
select {
case <-done:
fmt.Println("cleaning...")
time.Sleep(time.Second * 2)
fmt.Println("clean done.")
done <- struct{}{}
return
case <-time.After(time.Duration(rand.Intn(2000)) * time.Millisecond):
c <- fmt.Sprintf("generator %s sended %d", id, i)
i++
}
}
}()
return c
}

func main() {
done := make(chan struct{})
m1 := msgGen("svc1", done)
for i := 0; i < 5; i++ {
if msg, ok := timeoutWait(m1, 1*time.Second); ok {
fmt.Println("msg from svc1: ", msg)
} else {
fmt.Println("timeout")
}
}
done <- struct{}{}
<-done
}
//msg from svc1: generator svc1 sended 0
//timeout
//msg from svc1: generator svc1 sended 1
//timeout
//msg from svc1: generator svc1 sended 2
//cleaning...
//clean done.

http

标准库

简单访问

resp, err := http.Get("https://www.imooc.com")
response, err := httputil.DumpResponse(resp, true)

自定义 Header

request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
request.Header.Add("User-Agent", "xxx")
client := http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
fmt.Println("Redirect:", req)
return nil
}}
resp, err2 := client.Do(request)
response, err3 := httputil.DumpResponse(resp, true)

性能监测

导入 pprof 后,可以在 web 端查看调试界面(端口为 web 服务端口):

http://localhost:8888/debug/pprof/

import (
// ...
_ "net/http/pprof"
)

也可以在控制台查看 cpu 与 内存信息:

$ go tool pprof http://localhost:6060/debug/pprof/heap
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30