1. 介绍
2. 格式化
3. 注释
4. 命名
5. 分号
6. 控制结构
7. 函数
8. 数据
9. 初始化
10. 方法
11. 接口和其它类型
12. 空白标识符
13. 内嵌
14. 并发
15. 错误处理
1. 介绍[1]
本文档提供了编写清晰、惯用的GO代码的技巧。
2. 格式化[2]
gofmt自动生成统一风格代码格式,程序员无需关心代码格式问题。
3. 注释[3]
Go提供C风格的块注释/* */
和C++风格的行注释//
。一般使用行注释,包注释使用块注释。
4. 命名[4]
首字母大写的命名包外可见。
包名
包名应该简洁明了,便于记忆。建议是一个小写单词,不包含下划线和混合大小写。
import "src/encoding/base64"
后,导入包用base64
代替。这使得包中导出名字避免冗余,bufio.Reader
和bufio.BufReader
相比,更简洁明了。
Getters
小写命名做为包内成员变量,大写命名做为公开成员读方法,公开成员写方法。例如,owner
为包内成员变量,Owner()
为公开方法返回owner
,SetOwner()
为公开方法修改owner
变量。
接口命名
单方法接口采用方法名加er
后缀方式命名,例如Reader
,Wrtier
。除非用途和签名完全一致,不要采用Read
,Write
,String
等系统保留方法名。转换字符串的方法名为String
而非ToString
。
多词命名
采用MixedCaps或mixedCaps风格,而非下划线。
5. 分号[5]
大多数情况下不需要输入分号,go语法分析器会自动插入。for语句或多条语句在一行时,需要输入分号分割语句。自动插入的一个副作用是左花括号不能在一行开头。
6. 控制结构[6]
If
if可以接受一个初始语句
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
重声明和重赋值
f, err := os.Open(name)
这条语句声明并赋值两个变量f
和err
。符合下面条件时:=
可对已存在变量v
重赋值。
- 同一作用域中
v
已存在。(否则在不同作用域中声明新变量v
) - 右边值可以正确赋给
v
- 至少产生一个新声明变量
例如,d, err := f.Stat()
go语言中,函数参数和返回值同函数体具有相同的作用域。
For
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
针对集合对象,使用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
}
go不支持++
,--
运算,但支持多赋值
// 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
go的switch表达式可以不为常量甚至不为数字,依次比较每个case直到匹配。表达式为空意味匹配true
。
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
}
匹配成功后就会返回,case子句支持逗号分隔。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
break
在swtich中并不常见,但也可以和label配合,直接跳到外层。
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)
}
}
Type Swtich
switch也可以用来判断一个变量的类型,switch表达式中声明一个变量,它在每个case子句中具有相应的类型。
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", 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
}
7. 函数[7]
多返回值
go函数和方法支持多返回值。
命名结果参数
返回值可以命名,并像输入参数一样使用。它们的初始值为0。
Defer
defer
调用一个函数,使其在被调用函数返回前运行。defer的典型用法是释放资源。defer函数的参数是在defer调用时赋值并被保持住。defer调用的执行顺序是后进先出。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
上面代码的运行结果是4 3 2 1 0
8. 数据[8]
new分配
new(T)
返回类型T
的新分配的0值对象指针*T
。
构造器和对象构造方法(复合文字)
构造器就是对象工厂方法分配对象并进行初始化工作。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
return File{fd, name, nil, 0}
}
go可以返回临时对象指针(go采用垃圾回收机制)。默认对象构造方法必须依次列举所有的成员。采用field:name对方式,可以只列举需要初始化的成员,return &File{fd: fd, name: name}
。new(File)
和&File{}
相同
make分配
make
只能用来创建slice,map和channel,并返回初始化号的对象(不是对象指针)。例如
make([]int, 10, 100)
生成一个slice对象,长度是10,容量是100,并指向一个长度为100的int数组。相反,new([]int)
返回的0值slice指针并不能使用。
数组
go数组定义需要制定大小,并且大小是类型的一部分。[10]int
和[20]int
是2个不同的类型。go数组是值类型。赋值或传入函数参数时会发生值拷贝。如果需要指针类型数组,一般使用切片。
切片
切片是基于数组的方便使用对象,它具有底层数组引用和当前数组长度以及数组最大长度。切片本身是值对象,但是赋值后,两个切片会指向同一段底层数组,故此能够传递修改。数组支持范围访问n, err := f.Read(buf[0:32])
。切片增加元素建议使用内置方法append
,它支持自动扩容。
二维数组和切片
type Transform [3][3]float64
type LinesOfText [][]byte
二维切片中的每个切片长度可以不同。分配二维切片有两种方式,如果切片们的长度不同,应该为每个切片单独分配。如果它们的长度一样,可以只分配一次。
- 单独分配例子,注意每个slice都有独立的make分配
// 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:]
}
映射
键值对映射是一种很有用的类型。键可以是任何定义了equality操作的类型,注意切片不支持equality。映射也是持有底层数据结构引用,能够传递修改。映射支持复合文字构造,
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,
}
获取不存在的键值,会返回值类型的0值。如果要明确知道是否存在,使用多返回
var seconds int
var ok bool
seconds, ok = timeZone[tz]
如果只想判断键值是否存在,采用空标记符_
。_, present := timeZone[tz]
删除键值采用内置函数delete
,它确保键不存在也能工作。
格式化打印
fmt
包提供一系列格式化打印的方法,例如fmt.Printf
, fmt.Fprintf
, fmt.Sprintf
,以及默认格式版本,fmt.Print
, fmt.Fprint
, fmt.Sprint
,默认格式会在每个参数前后插入一个空格。
- %d 显示数字
- %v 显示所有类型的值
- %+v 对于结构体显示字段名
- %#v 显示对象全部信息
- %q 显示字符串或字节数组,也可用于数字或rune,显示单引号标记的rune字符
- %#q 尽可能使用反引号
- %x 用于字符串、字节数组或数字,显示十六进制值
- % x 在显示字节前后增加空格
- %T 显示类型
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
结果
18446744073709551615 ffffffffffffffff; -1 -1
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
结果
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
结果
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
fmt.Printf("%T\n", timeZone)
结果
map[string] int
自定义显示方法只需重写类型的String() string
方法
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"
重写方法时避免无限重入
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.
}
不定参数...
- 作为参数传入
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string)
}
- 作为slice使用
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
append
内置函数签名func append(slice []T, elements ...T) []T
。T
表示任何类型。切片通过...
方式转换为可变参数。
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
9. 初始化[9]
go的初始化比c和c++更强大,可以构造复杂结构体,不同包之间的初始化顺序也会被正确处理。
常量
go常量在编译时生成,只能是数字、字符、字符串或布尔类型。定义支持常量表达式,例如1<<3
,math.Sin(math.Pi/4)
不支持,因为math.Sin
是运行时函数。
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
)
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)
}
ByteSize(1e13)
显示结果为9.09TB
变量
变量在运行时完成初始化
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
init函数
每个原文件可以定义init
函数(支持多个init函数)。init函数会在所有变量初始化完成、所有导入包初始化完成后运行。init函数除了进行初始化外,也常做一些状态验证和修复。
10. 方法[10]
指针 vs 值
任何命名类型都可以定义方法(除了指针和接口)。方法定义需要接收者,它可以是指针也可以是值。值将传递拷贝,方法内做的修改无法影响传入值。指针可以将修改带出方法外。
值接收者的方法可以在值和指针上执行,指针接收者的方法只能在指针上执行。一个特例是,如果值能够转为指针,值上调用指针接收者方法会自动转换为指针再调用。例如,b是值变量且可以取地址,b.Write
会被自动重写为(&b).Write
。
11. 接口和其它类型[11]
接口
和其它语言一样,go的接口是定义对象行为。只要具有接口方法,就可以当接口使用。go的接口一般只有1、2个方法,名字也来源于方法。
一个类型可以实现多个接口,只要它含有指定接口的方法。
转型
T(value)
将一个类型值转换成另一个指定类型T
,如果两个类型完全一样,此过程并不会产生新值。(int
转为float
,会产生新值)。
接口转型和类型断言
Type Switch已涉及接口转型,每个case都会转换成对应类型。如果已知接口类型,就要使用类型断言。value.(typeName)
。typeName是具体的类型名字,例如str := value.(string)
。但如果转型失败,会发生运行时错误。可用下面方式避免错误。
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
如果转型失败,str
仍然存在,只是0值。
概述
如果一个类型只是实现了一个接口,并不需要暴露这个类型而应暴露它实现的接口,以隐藏具体的实现细节。这要求构造器返回接口而不是具体实现类型。例如crc32.NewIEEE
和adler32.New
都返回接口hash.Hash32
。替换crc32算法为adler32算法,只需要修改构造器调用,而其它代码都保持不变。
接口和方法
只要实现了接口方法的类型就实现了接口,由于几乎所有的类型都可定义方法,故此几乎所有的类型都可以实现接口。
例如Handler接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
下面的结构实现了此接口
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)
}
也可以用整数类型实现这个接口
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
也可以是其它类型,比如管道
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
函数类型可以实现接口,例如
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
http.Handle("/args", http.HandlerFunc(ArgServer))
上面的代码实现了访问/args
时,返回系统参数。
12. 空白标识符[12]
空白标识符类似unix中的 /dev/null
文件,是一个占位符但不关心它的值。
在多赋值中使用
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
未使用导入和变量
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 _ "net/http/pprof"
导入这个包只是为了运行它的init函数
接口检查
对于运行时接口检查,如果不关心转换值,采用空白标识符忽略转换结果
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
有种罕见情况,需要在编译时确认一个代码中未使用到的接口检查,采用空白标识符忽略转换值。
var _ json.Marshaler = (*RawMessage)(nil)
13. 内嵌[13]
go不支持标准面向对象的继承。go推荐使用组合,并提供内嵌达到类似继承的效果。接口和结构都可使用内嵌。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
接口只能内嵌接口。
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
type Job struct {
Command string
*log.Logger
}
内嵌是在外部类型中定义了一个类型同名字段,并可从外部类型中直接访问内嵌数据。内嵌不支持重载和多态。go通过接口实现多态。需要多态的方法都定义为小接口(go推荐小接口),用组合代替继承。
14. 并发[14]
不同的并发方式
go没有采用资源竞争方式实现并发,而通过共享资源确保每个线程访问各自的资源。总结为一句话
勿以共享内存实现通讯,而以通讯实现共享内存
go的并发方式源于CSP模型(Communicating Sequential Processes)。
go协程
在函数或方法调用前加上go就会启动一个go协程,不同的协程能够并发的运行在同一个代码地址上。协程非常轻量,比分配栈大不了多少。协程可以多路复用操作系统线程,它屏蔽了线程创建、管理等的复杂细节。
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Note the parentheses - must call the function.
}
go函数支持闭包。
通道
通道由make
分配,类似队列类型。make通道是可指定缓存大小,默认为0。通道接收方一直被拥塞直到有数据收到。对于发送方,如果通道中的数据小于缓存,只拥塞到数据复制进通道,如果通道已满(或缓存为0),会一直拥塞到直到有数据被接收。
缓存通道可用作信号量,缓存数量就是控制的并发数量。见下例
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}
上面的代码有个问题,可能创建无限个协程,但只有MaxOutstanding个运行。改进一下,把创建协程也放入信号量控制中。
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
注意上面代码采用函数闭包将公共变量req的值固定在每个函数调用中。
也可用声明新的局部变量方式完成
func Serve(queue chan *Request) {
for req := range queue {
req := req // Create new instance of req for the goroutine.
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
函数式编程中,更推荐闭包方式。
还有另一个方法,开启指定数量个处理协程,同时处理。这种方案更自然。
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.
}
通道类型的通道
通道可以传递任何类型,也包括通道本身。下面例子实现一个简单的RPC。
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
客户端代码
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
服务端代码
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
并行
另个应用是将一个复杂计算分散到多个CPU同时运行。见下例
type Vector []float64
// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // signal that this piece is done
}
const numCPU = runtime.GOMAXPROCS(0) // number of CPU cores runtime.NumCPU()
func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // Buffering optional but sensible.
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// Drain the channel.
for i := 0; i < numCPU; i++ {
<-c // wait for one task to complete
}
// All done.
}
务必理解并发和并行的区别。并发是指程序能够独立执行各个模块。并行是指在多个CPU上同时执行运算以提高效率。go是一个并发语言,并不是并行语言,有些并行问题go并不适合。
简单垃圾回收例子
go的并发设计也简化一些非并发问题的解决。如下面这个源于RPC框架的简单垃圾回收例子。
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
var b *Buffer
// Grab a buffer if available; allocate if not.
select {
case b = <-freeList:
// Got one; nothing more to do.
default:
// None free, so allocate a new one.
b = new(Buffer)
}
load(b) // Read next message from the net.
serverChan <- b // Send to server.
}
}
func server() {
for {
b := <-serverChan // Wait for work.
process(b)
// Reuse buffer if there's room.
select {
case freeList <- b:
// Buffer on free list; nothing more to do.
default:
// Free list full, just carry on.
}
}
}
select
语句如果找不到满足子句,会执行default
子句,这意味着它是非拥塞的。
15. 错误处理[15]
包开发者应该提供丰富的错误信息包含全部的错误信息,比如包名、操作名等。也可以使用类型断言转换为某种指定错误,进一步处理。
运行时错误
常见的错误处理方式是返回它。由调用者判断如何处理。但如果发现一个严重错误无法处理或绕过,调用内置函数panic
产生一个运行时错误,panic接收一个任何类型的参数,一般是表示错误信息的字符串。
错误恢复
当运行时错误发生时,无论是显示产生的,还是隐式产生的例如数组下标越界,都会立刻停止当前执行并开始层层退出调用栈,退出前会执行对应的defer
函数。可以使用recover
方法重新截获运行时错误。recover只能在defer函数中使用,因为退栈时只有defer函数能够执行。例子如下
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只有在defer函数中才可能返回非nil,defer函数中的调用不受panic和recover的影响。错误恢复也用来处理内部错误,下面的例子是regexp包处理解析错误。
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
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 // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
doParse代码
if pos == 0 {
re.error("'*' illegal at start of expression")
}
一个需要遵守的原则是,内部运行时错误被转成error返回,不要暴露到包外。
使用re-panic重新抛出运行时错误,错误栈中会包含新旧错误信息。
网友评论