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重新拋出運行時錯誤,錯誤棧中會包含新舊錯誤信息。