Effective Go 笔记
格式化
go的编码风格是统一的,它提供了一个格式化工具gofmt
,将其集成在go的命令行中。代码的缩进,对齐等gofmt
都能够解决。所有代码包括标准库中的都遵循同样的编码风格。
注意:
- 缩进(Indentation):
gofmt
缩进默认采用的是tab
,自己也可以使用空格。(可在编辑器中设置自动转换tab为space)。 - 行宽(Line length):go没有限制行宽,如果自己觉得太宽可以换行用tab缩进后继续。
- 圆括号(Parentheses):控制结构(if/for/switch)的语法中并没有圆括号。由于操作符的优先级比较简洁,所有在一些运算中也没有圆括号,只是用空格来表示。例如
x<<8 + y<<16
。
注释
- 行注释:
//
,比较常用。 - 块注释:
/*...*/
,主要用于包注释。
go提供了文档生成程序godoc
,它能提取包中出现在顶级声明之前且与该声明之间没有空行的注释,将该声明一起提取出来作为说明文档。
每个包都应该有一段放在package
语句之前的块注释作为该包整体介绍的包注释。对于多个文件的包,包注释只需要在其实一个文件中即可。它将出现在godoc文档的最上面。
推荐的格式如下:
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
...
*/
若包比较简单,则可以用行注释来写包注释:
// Package path implements utility routines for
// manipulating slash-separated filename paths.
godoc会将注释解释为纯文本,任何顶级声明前的注释都是该声明的文档注释。每个可导出的名称都应该有文档注释,文档注释最好是完整句子,第一句应该是被声明的名称开头,并且是单句摘要。
例如:
// 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) {
...
go支持组声明,可以用行注释来介绍一组常量或变量。
// 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 name
- 包名应符合短,简洁,易记的标准。
- 包名应以小写的单个单词命名,不应该使用下划线或者驼峰命名法。
- 包名是引入时用到的唯一默认名称,不过并不需要在所有的源码中保持唯一。
- 在少数名称有冲突的情况下可以为引入的包选择一个别名来局部使用。+ 无论如何,通过文件名来判断使用的包是不会产生混淆的。
- 包名应该是其源码目录的基本名称。
例如:src/encoding/base64
引入时为encoding/base64
,其名字应该是base64
,而不是encoding_base64
或encodingBase64
。 - 避免使用
import .
,除非遇到必须在测试包外运行的测试,否则尽量避免使用。 - 为了简洁起见,最好根据包结构取构造函数名字。
例如:用于创建ring.Ring
的新实例的函数(这就是go中的构造函数)一般会称之为NewRing
,但由于Ring
是该包所导出的唯一类型,且该包也叫ring
,因此它可以只叫做New
,它跟在包的后面,就像ring.New
。另一个简短的例子是once.Do
,once.Do(setup)
表述足够清晰, 使用once.DoOrWaitUntilDone(setup)
完全就是画蛇添足。 长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。
Getters
go不对getter和setter提供自动支持,需要自己编写,但若要将Get放到获取器的名字中,既不符合习惯,也没有必要。若你有个名为owner(小写,未导出)的字段,其获取器应当名为Owner(大写,可导出)而非GetOwner。大写字母即为可导出的这种规定为区分方法和字段提供了便利。若要提供设置器方法,SetOwner是个不错的选择。两个命名看起来都很合理:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Interface names
- 只包含一个方法的接口应该用该方法名加上
er
后缀来命名。如Reader
,Writer
,Formatter
,CloseNotifier
等。 - 为避免冲突,不要使用Read、Write、Close、Flush、String等具有典型含义的词作为方法名,除非明确知道他们的签名和含义相同。
- 若类型的实现方法与一个众所周知的方法名一样,那就用相同的名字。例如:字符串转换方法命名为
String
而不是ToString
。
MixedCaps
go中多个单词使用MixedCaps或者mixedCaps而不是下划线的连接的方式命名。
分号 Semicolons
go正式语法使用分号来结束语句,但是分号并不是在源码中出现,而是由词法分析器使用一条简单的规则自动插入。
规则是:
若新行前的标记为语句的末尾,则插入分号。
语句末尾的标记有:标识符(包括int和float64这样的)、数值、或者字符串常量以及break continue fallthrough return ++ -- ) }
之一。
分号可以在闭合的大括号前直接省略。如go func() { for { dst <- <-src } }()
无需分号。
go只有在诸如for的循环子句这样的地方使用分号来将初始化器,条件以及增量元素分开。若在一行写多个语句,也是需要用分号分开的。
无论如何都不应该将控制结构(if for switch select)的做大括号放在下一行,因为这样将会在大括号前面插入一个分号。
if i < f() {
g()
}
// 而不是
if i < f() // wrong!
{ // wrong!
g()
}
控制结构 Control Structures
if
if conditions {
statements
}
if conditions {
statements
} else {
statements
}
if conditions {
statements
} else if {
statements
} else {
statements
}
go中,若if不会执行到下一条语句时,即以break,continue,goto或return结束的时候,不必要的else会被省略。
重新声明与再次赋值 Redeclaration and reassignment
go使用:=
来快速声明变量。
满足下面条件时,已被声明的变量可以出现在后面的:=
中:
- 本次声明的变量和已经声明的变量处于同一作用域。
- 两次变量类型一致。
- 此次声明中至少另有一个新的变量。
go函数的形式参数和返回值在词法上处于大括号之外,但是作用域与函数体依旧相同。
For
// like a C for, need semicolons
for init; condition; post {}
// like a C while
for condition {}
// like a C for(;;)
for {}
若想遍历array、slice、string、map,或从channel中读取数据, 可以用range来实现循环。
for key, value := range oldMap {
newMap[key] = value
}
若只需要遍历结果中的第一项(键或下标),去掉第二个就行了:
for key := range m {
if key.expired() {
delete(m, key)
}
}
若只需要遍历中的第二项,可以用空白标识符_
代替第一项:
sum := 0
for _, value := range array {
sum += value
}
对字符串,range有很多用法。比如可以解析UTF-8字符串,将每个独立的Unicode编码分离出来。
go没有,
操作符,++
,--
为语句而非表达式。若要在for中使用多个变量,应该用平行赋值的方式。
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
Switch
switch的表达式无需为常量或者整数,case会自上而下逐一求值知道匹配。若switch后面没有表达式,将匹配为true
,因此,if-else-if-else可以写成switch的方式。
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
}
return 0
}
switch不会自动下溯,但是case可以通过,
分隔来列举相同的条件。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
break可以使switch提前终止,break也可以跳出层层循环至循环外某一标签。
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
continue也可以接受一个可选的标签,不过只能在循环中使用。
Type switch
switch可以用来判断接口变量的动态类型。通过type使用类型断言来匹配。若switch在表达式中声明了一个变量,那么该变量的每个字符都有该变量对应的类型。实际上是每个case里声明了一个不同类型但名字相同的新变量。
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
}
函数 Fuctions
Multiple return values
go的函数和方法可以返回多个值。(C中一般通过返回错误值和修改通过地址传入的实参来实现。)
Named result parameters
- go函数的返回值或结果”形参“可以被命名,并作为常规变量使用。
- 命名后,一旦该函数开始执行,它们就会被初始化为其类型相应的零值。
例如:
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的函数。
被推迟的函数按照后进先出(LIFO)的顺序执行。例如:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
会打印:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
数据 Data
Allowcation with new
go提供new
和make
这两个内建函数作为分配原语。
new
:用来分配内存,但它并不初始化内存,只会将内存置零。即new(T)
会为类型T
的新项分配已置零的内存空间,并返回它的地址,也就是一个类型为*T
的值。用go的术语来讲,它返回一个指针,该指针指向新分配的,类型为T的零值。
new
返回的内存已经置零,当设计数据结构的时候,每种类型的零值不必进一步初始化。
置零的操作是可以传递的。例如:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
SyncedBuffer类型的值在声明时就已经分配好内存了,p,v无需进一步处理即可正常工作。
Constructors and composite literals
有时零值不够好,需要一个初始化构造函数。例如:
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
}
与C不同,这里返回一个局部变量的地址没有问题,该局部变量对应的数据返回后依然有效。实际上每当获取一个复合变量的地址时,都会创建一个新的实例,所以上面的代码也可以合并为:
return &File{fd, name, nil, 0}
复合变量的字段必须按顺序全部列出,但是如果以field:value
方式列出的话,任何顺序都是可以的,没有给出的field将赋予零值。因此,可以用以下形式:
return &File{fd: fd, name: name}
少数情况下,若复合变量没有任何field,它将创建该变量类型的零值。new(File)
和&File{}
是等价的。
复合变量同样可以用于创建array,slice,map。feild的标签是array或slice的索引还是map的key看情况而定。例如下面:在初始化的过程中,无论Enone,Eio,Einval的值是什么,只要它们的标签不同就行。
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"}
Allocation with make
make(T, args)
和new(T)
不同,它只用于创建slice,map,channel,并返回类型为T
(而不是*T
)的一个已经初始化(而非置零)的值。原因在于,这三种类型本质上是引用数据类型,它们在使用前必须初始化。例如:slice是一个具有三项内容的描述符,包含一个指向(数组内部)数据的指针、长度以及容量, 在这三项被初始化之前,该slice为 nil;make([]int, 10, 100)
分配一个具有10个int类型的数组空间,接着创建一个长度为10,容量为100并指向该数组中前10个元素的slice结构。与此相反,new([]int)会返回一个指向新分配的,已经置零的slice结构,即一个指向值为nil的slice指针。
new和make的区别:
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
make只适用于map,slice,channel,并且不返回指针,若要返回指针,要用new分配内存。
Arrays
- array是值。将一个数组赋值给另一个数组会复制其所有元素。
- 特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而不是指针,即值传递。
- 数组的大小是其类型的一部分。即[10]int和[20]int是不同类型的。
数组值传递的属性很有用,但是代价比较高,若要像C一样引用传递,可以传递一个指向该数组的指针。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
但这并不是go的习惯用法,slice才是。
Slices
slice是对array的封装,为数据序列提供了更通用,强大而方便的接口。除了矩阵变换这类需要明确维度的情况外,go中大部分array编程都是通过slice来完成的。
- slice保存了对底层array的引用,若将slice赋值于另一个slice,它们会引用同一个array。
- 若某个函数将slice作为参数传入,则它对该slice的修改对调用者而言同样可见,即传递了底层array的指针。
- slice的长度决定了可读取数据的上限。只要slice不超出底层array的限制,它的长度就是可变的。
- slice的容量可以通过内建函数
cap
获得,它返回slice可取得的最大长度。
以下是将数据append到slice的函数。若数据超出其容量,则会重新分配该slice。返回值即为所得的slice。该函数中所使用的len
和cap
在应用于 nil slice时是合法的,它会返回 0。
func Append(slice, data[]byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
最终必须返回slice,尽管Append可以修改slice的元素,但是slice(运行时数据结构包含指针、长度和容量)是通过值传递的。
Two-dimensional slices
go的array和slice是一维的,要创建等价的二维array或slice,就必须定义array的array或者slice的slice,例如:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
由于slice的长度是可变的,因此其内部可能拥有多个不同长度的slice。在下面的例子中,每行都有其自己的长度。
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
有两种方式分配二维数组:
-
独立分配每一个slice。通常在slice会增长或者收缩的情况下使用,从而避免覆盖下一行。
// 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) }
-
只分配一个array,将各个slice指向它。
// 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可以时任何相等行操作符支持的类型,如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。
- slice不能用作map的key。
- map也是引用类型。若map传入函数,更改map的内容后,此更改对调用者可见。
- map可以用一般的复合变量语法构建,其key-value对使用
,
分隔。 - 赋值和获取map的value的方法类似于slice,不同的时map的索引不必为整数。
- 若用不存在的key获取map的value时,会返回与map中value类型对应的零值。
- 有时需要区分某项时不存在还是其值本身就为零值,可以使用多重赋值的方式来分辨:
例如:func offset(tz string) int { if seconds, ok := timeZone[tz]; ok { return seconds } log.Println("unknown time zone:", tz) return 0 }
若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; 若不存在,seconds 则会被置为零,而 ok 会被置为 false。
- 若仅需要判断map中是否存在某项而不关心其value,可以用空白标识符
_
来代替。
例如:_, present := timeZone[tz]
- 删除map中的某项可以用内建函数
delete
,它以map要删除的key为实参,即使key不存在也是安全的。
例如:delete(timeZone, "PDT") // Now on Standard Time
Printing
go的格式化打印风格和C的printf系列操作非常相似,这些函数位于fmt包中,函数名首字母均为大写。
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
%v
会打印任意值,包括slices,structs,maps- maps中的key可能按照任意顺序输出。
- 打印structs的时候改进的格式
%+v
会为structs的每个字段添加字段名,另一种格式%#v
将完全按照go的语法打印。 - 当遇到string或[]byte值时, 可使用
%q
产生带引号的字符串;而格式%#q
会尽可能使用反引号。 %x
还可用于字符串、字节数组以及整数,并生成一个很长的十六进制字符串, 而带空格的格式(% x
)还会在字节之间插入空格。%T
会打印某个值的类型。
若你想控制自定义类型的默认格式,只需为该类型定义一个具有 String() string 签名的方法。对于我们简单的类型 T,可进行如下操作。
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
会打印出:
7/-2.35/"abc\tdef"
如果你需要像指向 T 的指针那样打印类型 T 的值, String 的接收者就必须是值类型的;上面的例子中接收者是一个指针, 因为这对结构来说更高效而通用。
这里的String方法也可以调用Sprintf。不过有个重要的细节:**不要通过调用 Sprintf 来构造 String 方法,因为它会无限递归你的的 String 方法。**当 Sprintf 试图将一个接收者以字符串形式打印输出,而在此过程中反过来又调用了 Sprintf 时,这种情况就会出现。这是一个很常见的错误,如下例所示。
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}
要解决这个问题也很简单:将该实参转换为基本的字符串类型,它没有这个方法。
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
形参可指定具体的类型,例如从整数列表中选出最小值的函数 min,其形参可为 …int 类型。
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
func append(slice []T, elements ...T) []T
T 为任意给定类型的占位符。实际上,你无法在go中编写一个类型T由调用者决定的函数。这也就是为何append为内建函数的原因:它需要编译器的支持。
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
初始化 Initialization
go在初始化过程中,不仅可以构建复杂的结构,还能正确处理不同包对象间的初始化顺序。
Constants
- 常量在编译时创建,即便它们可能是函数中定义的局部变量。
- 常量只能是数字、字符(runes)、字符串或布尔值。
- 由于编译时的限制,定义它们的表达式必须也是可被编译器求值的常量表达式。例如
1<<3
就是,而math.Sin(math.Pi/4)
则不是,因为对math.Sin
的函数调用在运行时才会发生。 - 枚举常量使用枚举器 iota 创建。iota是表达式的一部分,可以隐式重复。
例如:
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
)
由于可将 String 之类的方法附加在用户定义的类型上, 因此它就为打印时自动格式化任意值提供了可能性,即便是作为一个通用类型的一部分。 尽管你常常会看到这种技术应用于结构体,但它对于像 ByteSize 之类的浮点数标量等类型也是有用的。
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
在这里用 Sprintf 实现 ByteSize 的 String 方法很安全(不会无限递归),这倒不是因为类型转换,而是它以 %f 调用了 Sprintf,它并不是一种字符串格式:Sprintf 只会在它需要字符串时才调用 String 方法,而 %f 需要一个浮点数值。
Variables
变量的初始化与常量类似,但其初始值也可以是在运行时才被计算的一般表达式。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
The init function
每个源文件都可以通过定义自己的无参数init函数来设置一些必要的状态。(其实每个文件都可以拥有多个 init 函数。)而它的结束就意味着初始化结束:只有该包中的所有变量声明都通过它们的初始化器求值后init 才会被调用,而那些 init 只有在所有已导入的包都被初始化后才会被求值。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
方法 Methods
Pointers vs. Values
-
可以为任何已经命名的类型(除指针或接口)定义方法。
-
接收者(reciver)可以不是struct。
例如:type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Body exactly the same as above }
-
一般会使用指针作为接收者,以避免调用方法时需要返回更改后的数据。
例如:func (p *ByteSlice) Append(data []byte) { slice := *p // Body as above, without the return. *p = slice }
方法的返回值可以用于返回更有效的信息,例如:
func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // Again as above. *p = slice return len(data), nil }
-
值方法可以通过指针和值调用,而指针方法只能通过指针来调用。因为指针方法可以修改接收者,而值调用时方法接收的时该值的副本,任何修改都会被丢弃。
-
若该值是可寻址的,那么语言会自动插入取址操作符来对付一般的通过值调用的指针方法。例如,例子中,
b
是可寻址的,因此只需要通过b.Write
来调用它的Write
方法,编译器会将其重写为(&b).Write
。
接口和其他类型 Interfaces and other types
Interfaces
- 接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里。
- 每种类型可以实现多个接口。
Conversions
Interface conversions and type assertions
-
type switch
:type
关键字,value.(type)
type Stringer interface { String() string } var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str case Stringer: return str.String() }
-
type assertion
:value.(typeName)
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
若类型断言失败,
str
将继续存在且为字符串类型,但它将拥有零值,即空字符串。
Generality
- 若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。
- 在这种情况下,构造函数应当返回一个接口值而非实现的类型。例如在 hash 库中,crc32.NewIEEE 和 adler32.New 都返回接口类型 hash.Hash32。要在 Go 程序中用 Adler-32 算法替代 CRC-32, 只需修改构造函数调用即可,其余代码则不受算法改变的影响。
Interfaces and methods
几乎任何类型都可以添加方法,因此几乎任何类型都能满足一个接口。例如http包中定义的Handler接口。任何实现了Handler的对象都能够处理HTTP请求。
空白标识符 The blank identifier
空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。
The blank identifier in multiple assignment
若某次赋值需要匹配多个左值,但其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
Unused imports and variables
若导入某个包或声明某个变量而不使用它就会产生错误,此时可以用空白标识符代替。
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
}
Import for side effect
导入一个只使用其副作用的包, 只需将该包重命名为空白标识符。
import _ "net/http/pprof"
Interface checks
一个类型无需显式地声明它实现了某个接口。取而代之,该类型只要实现了某个接口的方法, 其实就实现了该接口,但有些接口检查会在运行时进行。使用类型断言来检查类型的属性:
m, ok := val.(json.Marshaler)
如果只是判断类型是否实现接口,而不需要实际使用接口,可以使用空白标识符:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
内嵌 Embedding
go并不提供典型的,类型驱动的子类化概念,但通过将类型内嵌到结构体或接口中, 它就能 “借鉴” 部分实现。
例如:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
ReadWriter 能够做任何 Reader 和 Writer 可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集)。只有接口能被嵌入到接口中。
同样的基本想法可以应用在结构体中,但其意义更加深远。例如:bufio 包中有 bufio.Reader 和 bufio.Writer 这两个结构体类型, 它们每一个都实现了与 io 包中相同意义的接口。此外,bufio 还通过结合 reader/writer 并将其内嵌到结构体中,实现了带缓冲的 reader/writer:它在结构体中列出了这些类型,但并未给予它们字段名。
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
内嵌的元素为指向结构体的指针,当然它们在使用前必须被初始化为指向有效结构体的指针。ReadWriter 结构体可通过如下方式定义:
type ReadWriter struct {
reader *Reader
writer *Writer
}
但为了提升该字段的方法并满足 io 接口,我们同样需要提供转发的方法, 就像这样:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
而通过直接内嵌结构体,我们就能避免如此繁琐。 内嵌类型的方法可以直接引用,这意味着 bufio.ReadWriter 不仅包括 bufio.Reader 和 bufio.Writer 的方法,它还同时满足下列三个接口: io.Reader、io.Writer 以及 io.ReadWriter。
还有种区分内嵌与子类的重要手段。当内嵌一个类型时,该类型的方法会成为外部类型的方法, 但当它们被调用时,该方法的接收者是内部类型,而非外部的。在我们的例子中,当 bufio.ReadWriter 的 Read 方法被调用时, 它与之前写的转发方法具有同样的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。
内嵌字段与常规命名的字段结合也提供很大的便利:
type Job struct {
Command string
*log.Logger
}
Job 类型现在有了 Log、Logf 和 *log.Logger 的其它方法。我们当然可以为 Logger 提供一个字段名,但完全不必这么做。现在,一旦初始化后,我们就能记录 Job 了:
job.Log("starting now...")
Logger 是 Job 结构体的常规字段, 因此我们可在 Job 的构造函数中,通过一般的方式来初始化它,就像这样:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
或者:
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
若需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名。若我们需要访问 Job 类型的变量 job 的 *log.Logger, 可以直接写作 job.Logger。若我们想精炼 Logger 的方法时, 这会非常有用。
func (job *Job) Logf(format string, args ...interface{}) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
内嵌类型会引入命名冲突的问题,解决规则很简单:
- 首先,字段或者方法X会隐藏该类型中更深层嵌套的其他项X。若 log.Logger 包含一个名为 Command 的字段或方法,Job 的 Command 字段会覆盖它。
- 其次,若相同的嵌套层级出现同名冲突,通常会产生一个错误。若 Job 结构体中包含名为 Logger 的字段或方法,再将 log.Logger 内嵌到其中的话就会产生错误。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错。 这种限定能够在外部嵌套类型发生修改时提供某种保护。 因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。
并发 Concurrency
Share by communicating
不要通过共享内存来通信,而应通过通信来共享内存。
Goroutines
Goroutine 具有简单的模型:它是与其它 goroutine 并发运行在同一地址空间的函数。它是轻量级的, 所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。
Goroutine 在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待 I/O, 那么其它的线程就会运行。Goroutine 的设计隐藏了线程创建和管理的诸多复杂性。
在函数或方法前添加 go 关键字能够在新的 goroutine 中调用它。当调用完成后, 该 goroutine 也会安静地退出。(效果有点像 Unix Shell 中的 & 符号,它能让命令在后台运行。)
go list.Sort() // run list.Sort concurrently; don't wait for it.
函数字面在 goroutine 调用中非常有用。
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Note the parentheses - must call the function.
}
在go中,函数字面都是闭包:其实现在保证了函数内引用变量的生命周期与函数的活动时间相同。
这些函数没什么实用性,因为它们没有实现完成时的信号处理。因此,我们需要信道。
Channels
channels与maps一样,需要通过make来分配内存。其结果值充当了对底层数据结构的引用。若提供了一个可选的整数形参,它就会为该channel设置缓冲区大小。默认值是零,表示不带缓冲的或同步的channel。
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
无缓冲channel在通信时会同步交换数据,它能确保(两个 goroutine)计算处于确定状态。
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
- 接收者在收到数据前会一直阻塞。
- 若channel是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞。
- 若channel是带缓冲的,则在值被复制到缓冲区开始之前,发送者是阻塞的。
- 若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。
Channels of channels
go最重要的特性就是信道是一等值,它可以被分配并像其它值到处传递。 这种特性通常被用来实现安全、并行的多路分解。
Parallelization
目前 Go 运行时的实现默认并不会并行执行代码,它只为用户层代码提供单一的处理核心。 任意数量的 goroutine 都可能在系统调用中被阻塞,而在任意时刻默认只有一个会执行用户层代码。 它应当变得更智能,而且它将来肯定会变得更智能。但现在,若你希望 CPU 并行执行, 就必须告诉运行时你希望同时有多少 goroutine 能执行代码。有两种途径可达到这一目的,要么 在运行你的工作时将 GOMAXPROCS 环境变量设为你要使用的核心数, 要么导入 runtime 包并调用 runtime.GOMAXPROCS(NCPU)。
注意不要混淆并发和并行的概念:并发是用可独立执行的组件构造程序的方法, 而并行则是为了效率在多 CPU 上平行地进行计算。尽管 Go 的并发特性能够让某些问题更易构造成并行计算, 但 Go 仍然是种并发而非并行的语言,且 Go 的模型并不适合所有的并行问题。
A leaky buffer
错误 Errors
-
错误的类型通常为 error,这是一个内建的简单接口。
type error interface { Error() string }
例如:
// PathError records an error and the operation and // file path that caused it. type PathError struct { Op string // "open", "unlink", etc. Path string // The associated file. Err error // Returned by the system call. } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
-
错误字符串应尽可能地指明它们的来源,例如产生该错误的包名前缀。
-
若调用者关心错误的完整细节,可使用类型选择或者类型断言来查看特定错误,并抽取其细节。
例如:for try := 0; try < 2; try++ { file, err = os.Create(filename) if err == nil { return } if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { deleteTempFiles() // Recover some space. continue } return }
Panic
- 内建的 panic 函数会产生一个运行时错误并终止程序。 该函数接受一个任意类型的实参(一般为字符串),并在程序终止时打印。它还能表明发生了意料之外的事情,比如从无限循环中退出了。
- 实际的库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行而不是终止整个程序。一个可能的反例就是初始化: 若某个库真的不能让自己工作,且有足够理由产生 Panic,那就由它去吧。
Recover
当 panic 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败), 程序将立刻终止当前函数的执行,并开始回溯 goroutine 的栈,运行任何被推迟的函数。 若回溯到达 goroutine 栈的顶端,程序就会终止。不过我们可以用内建的 recover 函数来重新取回 goroutine 的控制权限并使其恢复正常执行。
调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。
recover 的一个应用就是在服务器中终止失败的 goroutine 而无需杀死其它正在执行的 goroutine。
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)
}