effective_go

  • effective_go 笔记

  • 资料来源:

    <>

  • 更新

    1
    2025.02.11 初始

导语

effective_go GO ON!

effective_go

  • https://go.dev/doc/effective_go
  • https://github.com/bingohuang/effective-go-zh-en

Formatting

代码格式化, gofmt 处理一切

Commentary 注释

类似 c 语言

1
2
3
4
/*
xxx
*/
// xxx

godoc 生成 go 程序 doc 的工具, 同时也提供 http 服务

package 需要包含一段包注释, 包含多个文件只需要出现在一个位置即可.

注释被作为纯文本解析, html 标记等无法生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp

文档注释句子尽量完整, 尽量以 可导出的名称为开头, 这样非常方便检索

1
2
3
// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {

对于组声明的, 注释只需要一个 godoc 会自动映射到整个组

1
2
3
4
5
6
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
)

命名

package 命名: 包名是导入 package 唯一的名称

  • 小写字母, 单个单词, 不需要下划线和驼峰标记.
  • package 名应为其源码目录的名称, src/pkg/encoding/base64 下的包应该作为 "encoding/base64" 导入, 而不是 encoding_base64
  • 长命名并不会更具可读性, 一份说明文档更有价值

getter/setter: go 默认不提供支持, 但是有个约定

  • getter 更适合作为不用 getter 而是 Owner, setter 更适合 SetOwner
1
2
3
4
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}

Interface 命令

  • 只包含一个方法的 interface 命名 = 方法名 + er 后缀
  • 尽量避免使用 Read Write Close Flush String 等为 interface 的方法命名, 除非你知道你在做什么
  • 反之, 如果 方法作用和 String 一致, 那就应该使用 String 而不是 ToString

Go 默认使用 MixedCaps 或 mixedCaps

  • 可导出, 首字母大写 驼峰
  • 不可导出, 首字母小写 驼峰

Semicolons

go 其实也使用 ; 作为一行的结束, 但是鸡贼的使用语法分析器 自动添加 ; 而不是源码中写 ;.

原文概况: " 如果新行前的最后一个标记可以结束该段语句,则插入分号 "

  • 标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面
  • 分号也可在闭合的大括号之前直接省略

忘记上面的规则吧, 语法分析器会直接报错,排查错误就好了.

1
2
3
4
5
6
7
8
9
10
// case1
if i < f() {
g()
}
// case2
// go 的语法分析器直接提示报错
if i < f() // wrong!
{ // wrong!
g()
}

Control Structures

控制语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if x > 0 {
return y
}
// 参加的连续模式
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)

上述的 f, err := os.Open(name)d, err := f.Stat(), 对于 err 而言,其在第二个语句是赋值而不是新建, 这仅仅是个实用主义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }

// 遍历切片/数组/字符串/映射/chanel -> range
for key, value := range oldMap {
newMap[key] = value
}
// 遍历时候 省略第二个变量
for key := range m {
for _, value := range array {
1
2
3
4
5
6
7
// ++ -- 依然存在, 但是其是语句而不是表达式
// ++ -- 不能用于 = ,赋值语句
i++
i--
// 下面错误
x = i--
y = i++
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
26
27
28
29
30
31
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
// 逗号隔开, 条件罗列
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return 0
}

// 另一种场景, 选择 type
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

控制流语句: break continue goto 与 c 区别较大

  • goto 限定只能同一个 函数内使用, 不能跳过变量声明.
  • break continue 除了 c 的相同写法,还存在 +tag 的写法.用于一次跳出多层 or 终止多层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
OuterLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
continue OuterLoop // 继续外层循环的下一次迭代
}
fmt.Println(i, j)
}
}

OuterLoop:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j == 6 {
break OuterLoop // 跳出外层循环
}
fmt.Println(i, j)
}
}

Functions

返回多个值… c/c++ 中实现相同定义, 需要传入一堆的指针…

1
2
3
4
5
6
// func (file *File) Write(b []byte) (n int, err error)
// 常见的错误排查
n, status := Write(x)
if status != OK {
xxx
}

无参数 return, 这是特别的一点

1
2
3
4
5
6
7
8
9
10
11
// 返回值会关联到 n 和 err
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
// 没有参数
return
}

又见 defer : 常用于解锁互斥和关闭文件

  • 执行参数在 defer 执行时候已确认
  • 多个 defer 先进后出… 奇怪的技巧…
  • defer 基于函数而非 block, 怪哉怪哉
1
2
3
4
5
6
7
8
9
10
// 追踪函数执行堆栈...
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
trace("a")
defer untrace("a")
// do something….
}

Data

new make and slice

New

new 相当于 C 的 calloc()

  • 申请创建一块内存
  • 将内存区域置为 0
  • 返回指向这块内存的指针
1
2
3
// 已经初始化完毕, pv 可以直接用了
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
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
26
27
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
// 繁琐
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
// 简化
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
// 再简化
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
return &File{fd, name, nil, 0}
}

复合字面量 ^235660

1
2
3
4
5
6
7
8
// 类似 C 的 指定初始化器, 直接为每个字段赋值
// go 还真是实用主义....
f := File{fd, name, nil, 0}
// 还能用于 数组 切片 映射
// 至于字段是 索引还是标签 无所谓, 实用主义..
a := […]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

Make

make 只用于创建切片 映射 信道 返回 T (注意不是 *T)

  • 这 3 种都是引用类型, 使用之前必须初始化;
  • 内存大概率为非 0 值
1
make([]int, 10, 100)

与 new 的区别… 切片/映射/信道 -> make; 需要指针 -> new;

Arrays

与 C 完全不同!!!

  • 数组是一个值
    • array_a = array_b 会复制整个 数组 b
    • func(arrary_b) 参数 b 会是 数组 b 的整个 copy
  • 大小是类型的一部分
    • [10]int[20]int 是不同类型

基本与数组相关的都交给了 slices

Slices

除了矩阵变换, Go 中的大部分数组编程都是通过切片完成

1
2
3
4
// read 参数是切片
func (file *File) Read(buf []byte) (n int, err error)
// 返回读取的字节数
n, err := f.Read(buf[0:32])

切片不超出底层数组长度, 各种操作都是原地; 超出容量会重新分配新数组, Go 有 gc!!

  • make 预先留出位置是最好的
  • append / copy

Two-dimensional Slices

默认切片是一维的, 二维切片就需要一个 切片的数组 再转成 切片

  • 有趣的是 切片本身没有长度限制, 不同长度切片仍然是一个类型, 因此二维切片 的第二个维度下不同切片的长度可以不一样
1
2
3
4
5
6
7
8
type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
// 二维
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}

必须分配二维切片

  • 独立分配每一个切片
  • 只有一个数组, 其他切片指向这个数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

Maps

映射

  • 不保证顺序
  • key 是任何支持比较的类型, 因此 切片不能
  • 也是引用类型, 修改内容,全部引用都变

支持 复合字面量 初始化

1
2
3
4
5
6
7
8
// 
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
1
2
3
4
5
6
7
8
9
10
11
12
offset := timeZone["EST"]
// 不存在的值查询 到底返回什么和类型有关
// 这里就会返回 false , 如果是 int 类型可能返回 0
attended := map[string]bool{
"Ann": true,
"Joe": true,

}
// 确实需要区分到底是 这个 vlaue 存在就是 0, 还是 key 不存在返回默认值
var ok bool
seconds, ok = timeZone[tz]
_, present := timeZone[tz]

Printing

打印

1
2
3
4
5
// 以下输出都是一样的
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

和 c 相似的到此为止

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
26
27
28
// %d %x 等并不控制输出
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
//18446744073709551615 ffffffffffffffff; -1 -1

// 万能打印 %v 按照原样打印, 甚至是 数组 结构体 映射
// 打印映射 %+v 带上字段名 %#v go 的语法打印
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)

type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
// &{7 -2.35 abc def}
fmt.Printf("%+v\n", t)
// &main.T{a:7, b:-2.35, c:"abc\tdef"}
fmt.Printf("%#v\n", t)
// &main.T{a:7, b:-2.35, c:"abc\tdef"}
fmt.Printf("%#v\n", timeZone)
// map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}


// %T 打印 type
fmt.Printf("%T\n", timeZone) prints
// map[string] int

自定义类型的默认格式 -> 为这个类型自定义 String() string

一个坑: string 不能调用 Sprintf, 会无限递归

  • Sprintf 会默认调用 string
  • 破局: m 转为 string, string 本身没有 Printf
1
2
3
4
5
6
7
8
9
10
type MyString string
// 会无限递归
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

// 这样可以, 将 m 转为 string, string 本身没有 Printf
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}

接下来有点迷惑 可变参数忘记了…

1
2
3
4
5
// interface{} 相当于 void *, 任意类型
// ... 代表任意数量参数
func Printf(format string, v interface{}) (n int, err error) {}
// 其实还是能指定具体类型的
func Min(a …int) int {}

Append

这个小节其实是对内建 append 函数说明

1
func append(slice []T, elements …T) []T

这里的语法普通函数是不能这样使用的, 需要编译器支持;

因此 append 可以实现 相同内层的 切片叠加

1
2
3
4
5
6
7
x := []int{1,2,3}
y := []int{4,5,6}
// 叠加 int
x = append(x, 4, 5, 6)
// 叠加另一个切片, 还是特殊一点需要 ...
x = append(x, y…)
fmt.Println(x)

Initialization

const 常量只能是数字、字符(符文)、字符串或布尔值;

String 之类的方法附加在用户定义的类型上, 因此它就为打印时自动格式化任意值提供了可能性

  • 有点意思
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
26
27
28
29
30
31
32
33
34
35
36
type ByteSize float64

const (
// 通过赋予空白标识符来忽略第一个值
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
// 表达式 YB 会打印出 1.00YB,而 ByteSize(1e13) 则会打印出 9.09TB。
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%fKB", b/KB)
}
return fmt.Sprintf("%fB", b)
}

init 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通过命令行中的 --gopath 标记覆盖掉。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

Methods

go 可以为任何已经命名的类型定义方法

对应两种类型: 参数是 值 参数是指针;

一般情况下 类型支持寻址 (支持使用指针), 只定义一种方法就行, go 会自动解引用 (*p)/自动取址 (&v), 指针或者变量本身 区别不大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 为ByteSlice类型定义方法
type ByteSlice []byte

// 值接收者的方法
func (slice ByteSlice) Append(data []byte) []byte {
// …需要返回新切片
}

// 指针接收者的方法
func (p *ByteSlice) Append(data []byte) {
// …可以直接修改原切片
}

var b ByteSlice
// b 是一个值,
b.Write(data) // Go自动转换为(&b).Write(data)
(&b).Write(data) // 直接使用指针调用

Interfaces and other Types

接口定义行为, 类似鸭子类型;

传入接口的就存在类型判断问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Stringer interface {
String() string
}

var value interface{} // 调用者提供的值
// switch 选择
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
// 直接转换然后判断的
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}

接口的通用性:

  • 如果现有类型仅实现一个接口 而且其他方法均无需导出时, 该类型就不需要导出, 只导出接口.
  • 构造函数这样情况下应该只返回接口而非实例
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
26
27
28
29
30
31
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

// 标准实现
// Simple counter server.
type Counter struct {
n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

// 简化一点
// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}

// 假设每次访问都需要触发一个提醒, 绑定到 channel
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <req
fmt.Fprint(w, "notification sent")
}

so 还是这个接口, 实现接口的限制是: 不能是 接口 和 指针, 因此可以为 函数类型 实现 接口…

  • 函数类型也是一个类型啊
  • 感觉当初的 rust 也能走这套路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 假设现在的需要是 每个 http 请求 都要输出一遍系统 os.args
// 等同于每个 http 请求都需要执行一次 ArgServer
func ArgServer() {
fmt.Println(os.Args)
}

// 已有 http 的接口
// 函数类型,
type HandlerFunc func(ResponseWriter, *Request)

// 为这个函数类型实现接口
// ServeHTTP calls f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
// 在实现内部, 执行函数, 与上面例子本质上没有什么不同
f(w, req)
}

// 因此改动下 ArgServer 签名, 默认实现了这个接口
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
// boom
http.Handle("/args", http.HandlerFunc(ArgServer))

The Blank Identifier

就是 _ 忽略掉不使用的变量/声明等等

有一种情况: 开发中总是会有 暂时未使用的导入和变量, 未来可能有用, 但是当下不得不将其删除已通过编译 (编译器喋喋不休提示未使用变量 xxx) -> _ 占个位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"log"
"os"
)

var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader // For debugging; delete when done. // 用于调试,结束时删除。

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}

接口检查: 大部分情况下, 接口实现的类型检查是静态, 但是总会有需要运行时 检查时候, 并需要实际使用这个接口变量;

1
2
3
4
5
6
7
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

// 又或者 添加一个仅提醒编译器的 规则;
// 要求 `*RawMessage` 实现 Marshaler, 这样编译器 就会在编译时 检查 RawMessage 是否真的实现了 Marshaler
var _ json.Marshaler = (*RawMessage)(nil)

Embedding

内嵌? 嵌套? 这一节的翻译感觉有些误解; while 又是实用主义的一个特性

一个场景, 为 ReadWriter 实现 Reader 和 Writer 接口

  • 如果接口更多, 那转发的代码更多….
1
2
3
4
5
6
7
8
9
10
11
type ReadWriter struct {
reader *Reader
writer *Writer
}

// 还需要这样的转发到内存的 reader
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}

rw.Read()

但是通过 Embedding 可以直接节约掉上面的样板代码 -> 内嵌

  • 内嵌的还可以是 struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内嵌指针
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
rw.Read()

// 直接使用 Log 方法
type Job struct {
Command string
*log.Logger
}
job.Log("starting now…")

// 对应初始化
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

命名冲突:

  • 不在同一个级别, 引用内层则是 x.y.name
  • 如果同一个级别, 编译器会报错…
1
2
3
4
5
6
7
8
type Base struct {
Name string
}

type Container struct {
Base
Name string // 这个 Name 会隐藏 Base.Name
}

这么实用主义的特性自然是小心的大量使用了

Concurrency

这一节是 go 的核心, 小心小心;

Do not communicate by sharing memory; instead, share memory by communicating.

  • 通过信道 交换信息 而不是共享内存…

个人愚见: 基于 channel 的并发模型, 拆解了 C 中大量的锁结构, 简化 & 更符合直觉

go func(){xxx}

chanel

1
2
3
ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0) // unbuffered channel of integers
cs := make(chan *os.File, 100) // buffered channel of pointers to Files

等待 chanel 时候更多情况下接收方处于阻塞挂起状态, 一定程度上 带缓冲的信道可被用作信号量

  • 例如限制吞吐量, 一旦有 MaxOutstanding 个处理器进入运行状态,其他的所有处理器都会在试图发送值到信道缓冲区的时候阻塞,直到某个处理器完成处理并从缓冲区取回一个值为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
sem <1 // 等待活动队列清空。
process(r) // 可能需要很长时间。
<-sem // 完成;使下一个请求可以运行。
}

func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // 无需等待 handle 结束。
}
}

尽管只有 MaxOutstanding 个 goroutine 能同时运行, 但 Serve 还是为每个进入的请求都创建了新的 goroutine(多余的就阻塞), 若请求来得很快 该程序就会无限地消耗资源. 那么限制下启动 go 协程的数量呢?

1
2
3
4
5
6
7
8
9
func Serve(queue chan *Request) {
for req := range queue {
sem <1
go func() {
process(req) // 这儿有 Bug
<-sem
}()
}
}

req 变量在所有 go 协程之间共享, 这带来了同步问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一种解决办法是 作为参数 值传递进入函数
func Serve(queue chan *Request) {
for req := range queue {
sem <1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
// 还有就是可以创建 同名变量(还是值拷贝)
func Serve(queue chan *Request) {
for req := range queue {
req := req // 为该 Go 程创建 req 的新实例 | 看起来有些奇怪, 但这是 go 的常见写法
sem <1
go func() {
process(req)
<-sem
}()
}
}

另一种函数是 server 启动协程时候就限制下数量

1
2
3
4
5
6
7
8
9
10
11
12
13
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}

func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}

chanels of chanel ?? what ??, 信道能够传递信道, 玩出花…

还是 handle, 如果返回中包含一个可用于回复的信道呢?

  • 下面只是一个最简单示例
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
26
// 请求之中就带着一个 channel
type Request struct {
args []int
f func([]int) int
resultChan chan int
}

// client
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 发送请求
clientRequests <request
// 等待 chanel 回应
fmt.Printf("answer: %d\n", <-request.resultChan)

// server
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <req.f(req.args) // server 的回复直接写入到 channel
}
}

协程可不是仅仅为了方便拆分锁, 更多为了 并行

  • 并行的协程数量为 GOMAXPROCS
    • 在 go 1.5 以后 GOMAXPROCS = cpu 核心数量, 1.5 以前是只有 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const NCPU = 4  // CPU 核心数

func (v Vector) DoAll(u Vector) {
c := make(chan int, NCPU) // 缓冲区是可选的,但明显用上更好
for i := 0; i < NCPU; i++ {
// 启动耗时任务
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
// 排空信道。
for i := 0; i < NCPU; i++ {
<-c // 等待任务完成
}
// 一切完成。
}

A leaky buffer 漏桶模型 这是个新东西 也不新, 限流的常用算法

  • 一个漏斗, 无论来水多凶猛, 滴水的速度是恒等的, 如果水太多溢出了, 直接丢弃请求
  • 同样的算法还有: 令牌桶/固定时间窗口/滑动时间窗口等
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
26
27
28
29
30
31
32
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
for {
var b *Buffer
// 若缓冲区可用就用它,不可用就分配个新的。
select {
case b = <-freeList:
// 获取一个,不做别的。
default:
// 非空闲,因此分配一个新的。
b = new(Buffer)
}
load(b) // 从网络中读取下一条消息。
serverChan <b // 发送至服务器。
}
}

func server() {
for {
b := <-serverChan // 等待工作。
process(b)
// 若缓冲区有空间就重用它。
select {
case freeList <b:
// 将缓冲区放大空闲列表中,不做别的。
default:
// 空闲列表已满,保持就好。
}
}
}

上面的实现并不严格按照 漏桶模型, 最多算个资源池

Errors

错误处理, 到了据说 go 比较繁琐的地方;

err 接口

1
2
3
type error interface {
Error() string
}
1
2
3
4
5
6
7
8
9
10
// PathError 记录一个错误以及产生该错误的路径和操作。
type PathError struct {
Op string // "open"、"unlink" 等等。
Path string // 相关联的文件。
Err error // 由系统调用返回。
}

func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Panic 立刻终止 go 程序执行 panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))

  • 除非万不得已, 没必要彻底终止程序执行

但是 panic 并不是完全不可救药, recover 可以恢复一部分

  • 只能在 defer 中才能调用 recover

panic 执行过程: 立刻终止当前函数执行, 回溯 goroutine 的栈, 运行任何 defer 函数, 直到回溯到 goroutine 栈顶, 终止整个程序.

recover 始终只在 defer 中执行, 取回 goruntine 的控制权, 让我们可以只终止失败的 go 协程 无需杀死其他正常运行的协程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}

func safelyDo(work *Work) {
defer func() {
// 恢复
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}

恰当地使用 recover , 通过调用 panic 来避免更坏的结果; 来看一下一些奇妙用法,简化错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Error 是解析错误的类型,它满足 error 接口。
type Error string
func (e Error) Error() string {
return string(e)
}

// error 是 *Regexp 的方法,它通过用一个 Error 触发 Panic 来报告解析错误。
func (regexp *Regexp) error(err string) {
panic(Error(err))
}

// Compile 返回该正则表达式解析后的表示。
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // 清理返回值。
err = e.(Error) // 若它不是解析错误,将重新触发 Panic。
}
}()
return regexp.doParse(str), nil
}
  • 正则解析错误不影响其他 协程运行,但是如果是其他严重错误, 还是继续终止.
1
2
3
4
5
// 更好的 err 类型, 错误在哪里了
// 但是应该仅仅局限在包内部使用, 不应该暴露给用户
if pos == 0 {
re.error("'*' illegal at start of expression")
}

A Web Server

简单几行, 实现了一个 web server; go 强大, 但是 go 标准库更加强大!

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"flag"
"html/template"
"log"
"net/http"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}

func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET"><input maxLength=1024 size=70
name=s value="" title="Text to QR Encode"><input type=submit
value="Show QR" name=qr>
</form>
</body>
</html>
`