golang effective 翻譯

參考

Effective Go 官方文檔
其餘參考譯文
https://studygolang.com/articles/3228
http://docscn.studygolang.com/doc/effective_go.htmlhtml

Intruction

Go是一門新語言。儘管它也借鑑了現存語言的一些思想,使用GO完成一個高效(優秀)的程序,相比使用其餘相似語言差別巨大。
直接將C++或Java翻譯成Go是寫不出好程序的(unlikely to produce a satisfactory result) ----Java程序就要用Java寫,不能用Go。
Think about the problem from Go perspective could produce a succcessful but quite different program.
以Go的視角思考程序,會產生與從不一樣的優秀設計。
換種說法,要想寫好Go程序,理解Go的特性和習慣十分重要。
It's also important to know the established conventions for programming in Go,
另外,瞭解Go程序的構建習慣也很重要,像命名,格式化,組織結構等等,規例這些標準的程序,其餘Go開發者才更容易理解。c++

本文檔描述如何編寫清晰,符合慣例的Go代碼技巧。
It augments the language specification, the Tour of Go, and How to Write Go Code, all of which you should read first.
此讀此文檔以前,最好先看看 語言規劃,Go手冊,如何編寫Go代碼幾個文檔。git

Examples

Go package sources 不只是核心庫代碼,也是演示如何Go語言的代碼樣例。此外,其中許多package是自包含且可執行的示例,
你能直接在 golang.org 網站中運行。
若是你有相似 「如何解決某問題」 或者 「如何實現某功能」 的疑問,也許能在這此文檔、代碼和示例找到答案或一絲線索。程序員

Formatting

Formatting issues are the most contentious but the least consequential.
代碼格式化(排版)也許是爭議最多,但最不重要的問題了。
People can adapt to different formatting styles but it's better if they don't have to, and less time is devoted to the topic if every one adheres to the same style.
人們能適應不一樣的格式化風格,但若是全部人都堅持一種網絡,就能在此類爭議中節省更多時間
The problem is how to approach this Utopia without a long prescriptive style guide.
問題是,如何能在脫離冗長的代碼風格指南的狀況下,達到這種理想烏托邦呢。golang

在Go中,咱們用了一種不一樣尋常的辦法,那就是讓機器解決大部分格式化問題。 gofmt 程序(即 go fmt命令,which operate at the package level rather than source file level)用於讀取Go代碼,並將源碼縮進、對齊、註釋等規範成標準風格。
若是不清楚如何處理某種代碼格式,那就運行gofmt;,若是結果不太對,從新整理代碼後重試一下(也能夠給 gofmt提一個bug)正則表達式

看看下面的示例,咱們不用浪費時間將結構體中的字段名對齊了,直接用 gofmt就能解決。看看下面的聲名:shell

type T struct {
    name string // name of the object
    value int // its value
}

gofmt 會將字段排列整齊express

type T struct {
    name   string // name of the object
    value  int// its value
}

全部標準庫中的Go代碼都通過gofmt格式化過。
還剩一些格式化要求,簡介以下:編程

  • Indentation 縮進
    咱們使用 tabs 縮進,gofmt默認也這樣。非特殊狀況,不要使用空格縮進
  • Line length 行長度
    Go不限制每行代碼的長度。Don't worry overflowing a punched card. 不要擔憂穿孔卡片寬度不夠(最先編程用穿孔卡片)。
    若是以爲一行太長了,就換行,而後用幾個 tabs 縮進一下就行。
  • Parentheses 圓括號
    Go相比C和Java不多使用圓括號,控制結構(如 if, for, switch)的語法中都不要求圓括號。
    運算符的優先級別也更簡潔清晰,好比:
x<<8 + y<<16

表示含義同空格分隔的同樣,不像其餘語言那麼麻煩(TODO驗證其餘語言有什麼問題?)c#

Commentary 註釋

Go提供C風格的塊註釋/* */,還有C++風格的行註釋//。行註釋使用更廣泛一些,塊註釋較多用於package註釋中,
另外也用於行內註釋,或者註釋某一大段代碼塊。(行內註釋即: if a>b /&& a>c/ ,其中 a>c的條件就失效了 )
godoc命令用於提取Go源代碼中的註釋。
Comments that appear before top-level declarations. with no intervening newlines, are extracted along with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.

每一個緊挨 package 聲名(clause) 的塊註釋中,都該有package說明註釋(comments)。對於含有多個文件的package,說明應該集中在一個文件中(任意一個文件均可以)。
package的說明應該包含本身簡介,及全部相關信息。
這些註釋會出如今 godoc 生成的文檔中,因此應該像下面這樣註釋。

/*
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

若是 package 很簡單,package 註釋也能夠簡略一點。

// Package path implements utility routines for
// manipulating slash-separated filename paths.

說明(comments)文字自己不須要格式化(如banners of starts)。godoc輸出的文檔是非等寬字體,
因此不能像gofmt那樣依賴空格對齊。
The comments are uninterpreted plain text, so HTML and other annotations such as this will reproduce verbatim and should not be used. One adjustment godoc does do is to display indented text in a fixed-width font, suitable for program snippets. The package comment for the fmt package uses this to good effect.
說明文字是不通過處理的文本,因此相似 HTML 或者 this 一類的符號會直接顯示,儘可能不要使用。
但 godoc 會使用等寬字體顯示縮進過的文本,用來放置代碼片斷。標準庫中 fmt 就使用了相似效果。

Depending on the context, godoc might not even reformat comments, so make sure they look good straight up: use correct spelling, punctuation, and sentence structure, fold long lines, and so on.
根據實際狀況, godoc 也許不會改動說明的格式,必定確保拼寫、標點、句子結構以及換行都沒有問題。

package內部,全部緊挨聲明之上的註釋文字,都被當作文檔。全部導出變量(大寫字母開頭)都會生成文檔。

Doc comments work best as complete sentences, which allow a wide variety of automated presentations. The first sentence should be a one-sentence summary that starts with the name being declared.
文檔說明最好是一個完整句子,這樣方便任意顯示格式。註釋的第一句話,應該以所聲名的變量名稱開頭,作簡要介紹。

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

若是每一個文檔說明都以它描述的變量名開頭,godoc 的輸出與 grep 配合使用會很方便。
假設你想尋找正則表達式函數,但不記得函數名是"Compile"了,你可使用下面的命令搜索文檔。

$ godoc regexp | grep -i parse

若是文檔說明沒有以它描述的函數名開關(即"Compile"),grep 就無法顯示出準確的函數名。
但咱們要求每一個package中的文檔註釋都以它描述的變量名開頭,你就能看到相似下面的輸出結果:

$ godoc regexp | grep parse
Compile parses a regular expression and returns, if successful, a Regexp
parsed. It simplifies safe initialization of global variables holding
cannot be parsed. It simplifies safe initialization of global variables
$
// TODO 在 windows 7 go 1.9.1 中測試,godoc 輸出的函數文檔雖然邏輯上是一句話
// 但實際輸出仍然是多行的,因此 grep 過濾時,不會顯示 Compile 這行字符
// 這也就達不到上文說的目的了,不知道是否是我測試環境有問題?

Go's declaration syntax allows grouping of declarations. A single doc comment can introduce a group of related constants or variables. Since the whole declaration is presented, such a comment can often be perfunctory.
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 ')'")
...
)

批量聲名一般指明幾個相關數據項,好比下面這種,多個變量同時由一個mutex保護。

var (
countLock   sync.Mutex
inputCount  uint32
outputCount uint32
errorCount  uint32
)

Names

Go語言中命名的重要性同其餘語言同樣。命名甚至能影響語法:package中變量名稱首字母大小寫決定其是否對外部可見。
所以值咱們花點時間瞭解有關Go的命名習慣。

Package Names

當 package 導入(import)時,其 package name 就是一個訪生問器。出現下面代碼後,

import "bytes"

咱們就能使用 bytes.Buffer 這樣的類型了。每一個使用 package 的人都能用相同的 name 引用package,
就說明這是一個具有這些特色的好的名稱:短、簡潔、形象 (vocative) 。
packages 通常使用小字的單個單詞命名,不加下劃線或者大小寫字母。
Err on the side of brevity, since everyone using your package will be typing that name.
不用擔憂衝突(collisions a priori)。 package name 只是import時的默認名稱;不必在全部源代碼中都是惟一的。
偶爾遇到衝突時,使用局部重命名就能解決。並且import的名稱只決定被使用的package。(In any case, confusion is rare because the file name in the import determines just which package is being used.)

另一個慣例是,package 名稱是源代碼全部目錄的名稱;
好比 src/encoding/base64 導入時使用 import "encoding/base64" ,但真正調用時,使用"base64"做爲名稱。
既不是encoding_base64,也不是encodingBase64

使用者經過 package name 引用 package 中的內容,so exported names in the package can use that fact to avoid stutter.
(Don't use the import . notation, which can simplify tests that must run outside the package they are testing, but should otherwise be avoided.)
好比在bufio中的 buffered reader 的 package name 是Reader,而不是BufReader,由於使用者經過 bufio.Reader 調用。
由於調用者總會加上 package name 爲前綴使用,因此 bufio.Reader 永遠不會和 io.Reader 衝突。
一樣,通常用於建立一個ring.Ring的新實例的函數,咱們起名爲NewRing,但由於Ring中 package ring 中的導出類型,
因此咱們將函數命名爲New就能夠了。這樣用戶就能使用ring.New這種簡潔的名稱。
利用 package 的目錄結構幫你起個好名字。(Use the package structure to help you choose good names.)

還有個例子,once.Do; once.Do(setup)明顯就比once.DoOrWaitUntilDone(setup)好多了。
過長的名字反而可能影響可讀性。好的 doc comment 可能比冗長的名稱要有用得多。
(譯:結論我贊成,但這個例子中,我以爲 DoOrWaitUntilDone() 更好,還不到20個字符的名字,不能算長 :) )

Getter

Go不提供默認的 Getter 和 Setter 。這種東西由程序員本身實現就行。但不必在 Getter 函數名前加 Get 前綴。
若是你有一個名爲 owner (小寫,表示私有變量)的字段,那麼其 Getter 函數名可起爲 Owner (大小,表示公有函數),
不必起這 GetOwner 這樣的名稱。由於咱們僅憑大小寫就能區分出字段和函數。
Setter 能夠起這樣的名稱,示例以下:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

Interface Names

一般,僅有一個函數的 interface ,通常用它的函數名加 ex 後綴修飾成名詞,好比:Reader, Writer, Formatter, CloseNotifier
There are a number of such names and it's productive to honor them and the function names they capture. Read, Write, Close, Flush, String and so on have canonical signatures and meanings.
爲避免混淆,不要給函數起這樣的名字,除非它確實表達相似含義。
Conversely, if your type implements a method with the same meaning as a method on a well-known type, give it the same name and signature; call your string-converter method String not ToString.

MixedCaps

一般,Go中傾向使用MixedCapsmixedCaps這中駝峯命名法,不多使用下劃線(_)分隔多個單詞。

Semicolons 分號

像C同樣,Go也使用分號(;)斷句,不一樣於C的是,源代碼中能夠不出現分號。
詞法分析器(lexer)會自動插入分號,所以,大部分狀況下,編寫代碼時沒必要手動輸入分號。

規則是這樣的。若是一行尾的標記(token)是標識符號(identifier, (which includes words like int and float64)),
或者數字、字符串字面量(literal),或者是如下標記之一

break continue fallthrough return ++ -- ) }

詞法分析器就自動在標記(token)後插入分號。
這個規則能夠簡單理解爲,「在能夠斷句的地方,插入分號」。
(「if the newline comes after a token that could end a statement, insert a semicolon」)
(譯:初看這個說法,有點搞笑,但細想還真是這麼回事。
經歷一些項目後,不難發現,有些複雜邏輯背後的目標其實很簡單,幾個字就歸納出來。
但實現成代碼就會異常複雜。若是讀者能瞭解複雜行爲背後的目標,那就很容易理解了。
因此這個有點「搞笑」的話,應該也是golang開發者的一個目標吧。

兩個閉合的括號以後也能省略分號。好比下面這種狀況就不須要分號:

go func() { for { dst <- <-src } }()

一般,Go中常在for循環中分隔語句(initializer,condition,continuation)。
有時,也會用分號分隔一行代碼存在多條語句的狀況。

因爲自動插入分號的規則的影響,咱們無法在控制結構(if,for,switch,or select)中換行寫大括號了。
若是大括號換行了,那麼大括號以前就會被插入一個分號,這可能就出錯了。
這樣寫是對的:

if i < f() {
    g()
}

這樣寫是錯的:

if i < f()  // wrong!
{// wrong!
    g()
}

Control structures

Go中的控制結構和C有說不清楚的關係,但差別很大。
沒有do或者while循環了。但有增強版本的for;有更靈活的switch
ifswitch都能使用相似for中的 initialization 語句;
breakcontinue標籤仍然保留了下來;
新增長了用於多路複用的select
語法上有很大的變化,用於條件判斷的小括號不須要了,但用於定界的大括號是必須存在的。

If

Go中if語句通常是下面這樣:

if x > 0 {
    return y
}

強制要求不能省略大括號,使得簡單的if判斷也要寫成多行代碼。
這麼作是有好處的,作成是代碼中包含return或者break這樣的控制語句時。
(譯:這種硬性要求在golang中有不少,但確實是有好處的。
好比這個要求,就能從根本是解決維護舊代碼中,調整單行if語句時,因爲忽略{}而經常出現的bug)
ifswitch支持 initialization 語句,這很是便於使用局部變量

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

In the Go libraries, you'll find that when an if statement doesn't flow into the next statement—that is, the body ends in break, continue, goto, or return—the unnecessary else is omitted.

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

代碼中檢查了每一個可能出錯的環節,只要代碼執行到函數最後,說明全部異常問題都排除。
在每一個if條件處理中都用 return 返回 error ,因此代碼中都不須要出現 else 語句。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

Redeclaration and reassignment

上面的示例代碼也展現了:=符號的用法。
os.Open這行代碼中,聲名了兩個變量ferr

f, err := os.Open(name)

f.Stat代碼中,看似又聲名了兩個變量derr

d, err := f.Stat()

注意,err出如今兩個聲名的代碼中,但這是合法的。
每一次出現err是聲名此變量,第二次出現err中對上一次聲名的變量從新覆值。
也就是說errf.Stat()調用以前就已經聲名,f.State()只是賦予一個新值給err

In a := declaration a variable v may appear even if it has already been declared, provided:

this declaration is in the same scope as the existing declaration of v (if v is already declared in an outer scope, the declaration will create a new variable §),
the corresponding value in the initialization is assignable to v, and
there is at least one other variable in the declaration that is being declared anew.

這種不常見的特性徹底是爲了實用而已。咱們能在很長的if-else代碼中僅僅使用一個err變量。
你應該能常常看到這種用法。

It's worth noting here that in Go the scope of function parameters and return values is the same as the function body, even though they appear lexically outside the braces that enclose the body.

For

Go的for循環結合了C中forwhile的功能,不過不支持do-while的功能。
一共有兩種形式,只有一種必須要用分號。

// Like a C for
for init; condition; post { }
 
// Like a C while
for condition { }
 
// Like a C for(;;)
for { }

for語法中定義一個索引變量,用起來很方便吧。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

使用clause遍歷 array,slice,string,map 或者讀取 channel:

for key, value := range oldMap {
    newMap[key] = value
}

若是隻需用到第一個數據項(key/index),直接省略第二個就好了。

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

若是須要用到第二個數據項(value),用blank標識符()佔位,忽略掉便可:
(譯:golang全部聲名的變量必須使用,不然編譯失敗,因此不使用的變量,須要使用
符號佔位)

sum := 0
for _, value := range array {
    sum += value
}

bland標識符還有不少種用法,詳細描述參考這裏

遍歷字符串,解析UTF-8編碼時,range能跳過單個的Unicode碼。
錯誤的編碼只消費一個Byte,並使用rune類型的U+FFFD代替 value。
rune是內置類型,表示 Unicode code point ,詳細解釋參考 Rune_literals
如下循環代碼:

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

輸出:

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '?' starts at byte position 6
character U+8A9E '語' starts at byte position 7

最後,Go中沒有逗號(comma)運算符,而且++--是語句,不是表達式。
因此若是想在for中使用多個變量,只能使用批量賦值(parallel assignment)語句,避免使用++--
(譯:這就有點不爽了。。。)

// 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比C用途更廣。表達式不要求是常量或整型,從上往下找到第一個匹配的 case 便可,
若是switch沒有表達式,那麼找到第一個case表達式爲true的。
固然switch的實現功能,也能用if-else-if-else實現。

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
}

There is no automatic fall through, 但能夠用逗號分隔多個 case 條件:

func shouldEscape(c byte) bool {
    switch c {
        case ' ', '?', '&', '=', '#', '+', '%':
            return true
    }
    return false
}

switch中也能用break提早結束switch ,但Go這並不常常這樣用。
(譯:由於Go中不會連續執行兩個 case ,因此不須要用 break 分隔 case 。
但若是有須要連續執行多個 case 的狀況,能夠用逗號分隔 case ,達到相似的目的。)
有些特殊狀況,不只要結束switch,還要跳出外部循環。
在Go中能夠經過設置label實現。請看如下示例:
(TODO這怎麼跟 goto 語法很像?)

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語句也可使用label,但continue僅能在循環中使用。

用一個比較 byte slice 的 routine 示例結束本節吧:

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
            case a[i] > b[i]:
                return 1
            case a[i] < b[i]:
                return -1
        }
    }
    switch {
        case len(a) > len(b):
            return 1
        case len(a) < len(b):
            return -1
    }
    return 0
}

Type switch

switch 也能夠用來識別 interface 的動態類型。
通常在小括號包裹的type關鍵字進行類型斷言。若是在 switch 表達式內聲名一個變量,變量類型就和 case 中一致。
固然,也能直接在 case 中使用這個變量名稱,效果等同於在每一個 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
}

Functions

Multiple return values

另外一個Go的亮點是,函數(functions and methods)支持多返回值。
這個特色可用來解決C中遺存已久的麻煩:經過返回值肯定操做成功或失敗,參考傳遞參數地址返回額外的變量。( in-band error returns such as -1 for EOF and modifying an argument passed by address.)

(In C, a write error is signaled by a negative count with the error code secreted away in a volatile location.)
在C中,write()返回count>=0表示成功的字節後,count<0表示失敗緣由,錯誤代碼隱藏在返回參數中。
在Go中,write能同時返回兩個參數count和error,這能表達出C中沒法區分的一種狀況:「雖然成功的寫了count字節,但設備仍是出了一些異常」。
write方法定義以下 :

func (file *File) Write(b []byte) (n int, err error)

像文檔描述的同樣,它返回成功寫入的字節數 n ,若是 n!=len(b) ,返回非nil的error
看看後面有關錯誤處理的示例,你就會發現,這是一種頗有用(common)的風格。

A similar approach obviates the need to pass a pointer to a return value to simulate a reference parameter. Here's a simple-minded function to grab a number from a position in a byte slice, returning the number and the next position.

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

You could use it to scan the numbers in an input slice b like this:

for i := 0; i < len(b); {
    x, i = nextInt(b, i)
    fmt.Println(x)
}

Named result parameters 命名返回參數

Go中函數返回參數能夠像普通變量同樣命名並使用,就跟輸入參數同樣。
當函數開始時,命名返回參數會被初始化爲0(相關類型的zero值,不必定是數值0);
若是函數執行到一個 return 語句,而且沒有參數,那麼命名參數的當前值就做爲函數返回值。

命名不是強制的,善加利用能使代碼更簡潔:起到文檔的效果。
若是咱們給nextInt函數返回值命名,那就很容易知道每一個返回參數是幹什麼用的了。

func nextInt(b []byte, pos int) (value, nextPos int) {

由於命名參數會自動初始化並返回,它能使代碼十分乾淨。
看看下面這個版本的io.ReadFull函數棒不棒:
```golang
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

Go的defer語句能讓指定語句在延遲到函數結束前調用。
這個不太常見,但用來回收資源時,十分有用,尤爲是函數有不少返回路徑時。
最典型使用場景就是解鎖 mutex 或者關閉文件。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

延遲Close調用有兩個好處,首先,它保證不論後期怎麼維護調整代碼,你都不會忘掉關閉文件的事情,
其次,關閉和打開文件的代碼能夠緊挨着,這比在函數開關打開,函數末尾關閉清晰的多。

傳遞給defer函數的參數,是defer語句調用時的值,而不是defer函數真正運行時的值。
因此沒必要擔憂函數調用時,相關值會改變。
this means that a single deferred call site can defer multiple function executions. Here's a silly example.

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

Defer函數是按後進先出(LIFO)的順序執行的。所以上面的代碼會在函數返回時輸出4 3 2 1 0
一個更合理的示例是,用defer追蹤函數的執行。好比能夠這樣寫一對簡單的追蹤程序。

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....
}

We can do better by exploiting the fact that arguments to deferred functions are evaluated when the defer executes. The tracing routine can set up the argument to the untracing routine. This example:
咱們能夠改造這個程序,讓它用起來更方便:

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

對於習慣了塊級資源管理的程序員來講,defer看起來有些古怪。
更有趣且強大的地方在於,它是函數級的。
its most interesting and powerful applications come precisely from the fact that it's not block-based but function-based
panicrecover章節中咱們還能看到其餘用法。
(譯:相比c++中 class 的 destructer ,我仍是以爲 defer 比較難用,上向說的那些功能,用 destructer 能夠一行代碼實現。
但考慮到 golang 一直把 c 當作超越目標,我就原諒它吧。
TODO不過golang中有相似 destructer 的機制嗎?若是沒有,那是爲何不支持這樣的機制呢)
可能緣由是,destruct 的時機並不是肯定,也許某些優化使用,緣由已經能夠銷燬的變量,並未當即銷燬。

Data

Allocation with new 使用 new 分配內存

Go中有兩種分配原語(allocation 申請內容空間的方法),內置函數是newmake。這倆函數很容易混淆,但用於徹底不一樣的類型,區別很大。區分的規則也很簡單。先說new,這是內置的分配內存的函數,它不會初始化內存,只會將其清零(zeros)。即new(T)會分配類型爲T的內存空間,並清零後,返回類型爲*T的內存地址。 TODO zero 標準譯法

由於new返回的內存數據都通過zero(清零的),咱們的自定義結構體均可以不初始化了。也就是說,咱們用new建立一個指定類型的變量後,就能直接使用了。好比關於bytes.Buffer的文檔就這樣描述「zero的Buffer就是隨時可用的空 buffer」。一樣,sync.Mutex也沒有顯示初始批的Init方法。 zero 的 sync.Mutex 就是解鎖狀態的mutex。

zero值很是有用(transitively)。 看看下面的類型聲名。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer類型的變量一經聲名(allocation or just declaration)就能直接使用。
下面的代碼片段中,pv都能直接使用,不須要其餘初始化代碼了。

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // typeSyncedBuffer

Constructors and composite literals

有時 zero 仍是不夠用,咱們須要更進一步的初始化,即構造函數(constructor)。
下面示例是來自package os

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
}

下面還有更多樣例(boiler plate)。我能夠簡化成只用一句複合字面量(composite literal)就建立一個實例並賦值。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

注意,這跟C不同,咱們能返回局部變量的地址:函數返回時,變量存量空間仍然保留。實際上,composite literal 執行的時候,就已經分配了地址空間了。咱們能把最後兩行合併。

return &File{fd, name, nil, 0}

composite literal 中必須按序寫出相關結構的全部字段。若是顯示指定字段名,咱們就能按任意順序,初始化任意的字段,沒有列出的字段,初始化爲 zero 。像下面這樣:

return &File{fd: fd, name: name}

若是composite literal 若是未包含任何字段,就賦值爲zero。 這就跟表達式new(File)&File{} 是等效的。

composite literal 也能建立 arrays, slices, maps ,字段名會自動適配爲array 的 索引或 map 的 鍵。下面的示例中,只要 Enone,Eio,Einval 的值不一樣,就能正確初始化。

const (
    Enone = 0
    Eio = 1
    Einval = 3 // 取值能夠不連續
    // Einval  = "4" // 若是是字符串,就不能編譯經過
)
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

回到 allocation (資源分配)的話題。內置函數make(T, args)new(T)使用目的徹底不一樣。它僅用於建立 slices, maps, channels ,並返回初始化過的(不是zero),且類型爲T的變量(不是*T)。出現這種差別的本質(under the cover)緣由是這三種類型是引用類型,必須在使用前初始化。好比,slice由三個descriptor(描述符)組成,分別指向 data (數組的數據),length (長度),capacity(容量),在三個descriptor未初始化前, slice 的值是 nil 。對於 slices, maps, channels 來講,make用於初始化結構體內部數據並賦值。好比, ```golang make([]int, 10, 100) ``` 分配了一個包含100個int的array,並建立了一個length爲10,capacity爲100的slice,指向array的前10個元素。(建立 slice 時, capacity 能夠省略,查看有關 slice 的章節,瞭解更多信息。)與之對照,new([]int)返回一個 zero 的 slice 結構體,也就是一個指向值爲 nil 的 slice 。 下面代碼闡明瞭newmake`的不一樣。

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僅用於 maps, slices, channels ,返回的也不是指針。
只有使用new或者對變量執行取地址操做&File{}才能獲得指針。

Arrays

在詳細規劃內存總局時, array 是頗有用的,有時它還能避免過多的內存分配,但它的主要做用是構造 slice ,就是下一節的主題了,這裏先說幾句作鋪墊。

下面是 C 與 Go 中有關 array 的主要區別。在 Go 中,

  • Arrays 是值類型,兩個 array 之間賦值會複製全部元素。
  • 具體來說,若是函數參數是數據,函數將接收一個 array 的完整副本(深拷貝),而不是指針。
  • array 大小是類型的一部分。 [10]int[20]int是不一樣類型。

值類型有用,但代價高;若是你想要類C的行爲和效率,能夠傳遞array的指針作參數。

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 作了封裝,提供更通用、強大、方便的管理序列化(sequence)數據的接口。降了轉換矩陣這種須要明確維度的操做外,Go中大部分編程操做經過 slice 完成。

slice 保存了對底層 array 的引用,若是你把一個 slice 賦值給另一個 slice ,兩個slice引用同一個 array 。
若是一個函數接收 slice 參數,那麼函數內部對 slice 的修改,都能影響調用方的參數,這和傳遞底層 array 指針的效果相似。
比方說,Rread函數可使用 slice 做爲參數,slice 的長度恰好用來限制能讀取的最大數量量,這種方法很適合代替以 data 指針 與 count 容量 做爲參數的方式。如下是 package osFile類型的Read方法定義:

func (f *File) Read(buf []byte) (n int, err error)

這個方法返回成功讀取的字節數 n,以及標明是否遇到錯誤的 err 。
用下面這種方法,僅讀取文件前32字節,並將其填入緩衝區buf中的前32字節的空間中,其中使用了切割(slice the buffer, slice used as a verb)緩衝的方法。

n, err := f.Read(buf[0:32])

這種切割(slicing)方式常見而高效。若是撇開高效,下面的代碼也能讀取前32字節到緩衝區的目的。

var n int
var err error
for i := 0; i < 32; i++ {
    nbytes, e := f.Read(buf[i:i+1])// Read one byte.
    if nbytes == 0 || e != nil {
        err = e
        break
    }
    n += nbytes
}

在 slice 的底層數組沒有填滿時,也能改變 slice 的長度(length),只要對 slice 作一次切割(slicing)就行。
使用內置函數cap返回 slice 的容量(capacity),這是 slice 當前能使用的最大長度。
下面的函數能向 slice 中追加數據。若是數據超出最大容量,則爲 slice 從新分配空間。返回值就是追加數據後的 slice 。
函數lencap能正確處理值爲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的元素(譯:指array中的內容),但 slice 自己(保存 data指針,length, capacity的數據結構)是做爲值傳遞的。

向 slice 中追加數據的操做用途很大,因此咱們用內置函數append實現了此功能。
咱們還須要更多信息纔好理解這個函數的設計,因此,一會還會談到它。

Two-dimensional slices 二維 slice

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 也能有不一樣的長度。這種用法很常見,好比下面的LinesOfText示例,每行長度都不同。

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

處理像素描述行時,就會須要2D的 slice 。有兩種方法來實現。
一種是,每行獨立分配 slice ;另外一種是,分配一個 array , 將其分割成多塊交由 slice 管理。
根據本身應用的實際狀況選擇使用哪一種方法。
若是 slice 空間會增長或收縮(shrink), 應該選用第一種獨立分配 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)
}

第二種方法,一次分配,再分割成多行:

// 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

maps 是內建的方便而強大的數據類型,用於將一種類型的值(鍵,key)與另外一種類型的值(元素,element, value)進行關聯。
key 能夠是任何能用等號(=)比較的類型,如 integer, floating point 和 complex numbers, strings, pointers, interface (只要動態類型支持等號比較), structs 和 arrays。 slice 不能用作 maps 的 key ,由於沒法用等號比較 slice 的值(equality is not defined on them).
和 slice 相似, map 在底層保存某個數據類型的引用( maps hold references to an underlying data structure)。若是將 map 做爲函數參數,而且在函數內部改變了 map 的值,這種改變對調用者是可見的。

map 能夠由使用分號分隔 key 和 value 對(鍵值對)的 composite literal (複合字面量)聲名。
所以,很容易使用下面的方法初始化。

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,
}

設定和獲取 map 值與 array / slice 的作法同樣,只是索引(index)沒必要是 ingeger 了。

offset := timeZone["EST"]

若是嘗試獲取 map 中不存在的 key ,將返回 value 類型的 zero 值。
比哪,若是 map 的 value 是 integer,那麼查詢不存在的 key 時,返回值是 0 。(譯:zero 跟 0 是不同的,若是value 是string,返回""空字符串)
set 類型能夠用 value 是 bool 的 map 進行模擬。將 value 設置爲 true 表示元素加入 set ,直接索引操做就能確認 key 是否存在。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}
 
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}

有時,須要區分 key 不存在(即zero值)與 value 是0值的狀況。
好比,返回 0 時,是由於 key 爲 "UTC" 仍是由於 key 根本不存在於 map 中?
能夠用多返回值(multiple assignment)來區分這些狀況。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

按照慣例,在 seconds 後面加一個「, ok」 。在下面的示例中,若是tz存在,則seconds就是對應的值,而且ok會被設置爲 true ;不然,seconds會設置爲 zero 值,ok被設置爲 false。

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

若是隻想確認 map 中是否存在指定key,不關心其值是多少,可使用 blank identifier(_)

_, present := timeZone[tz]

使用內置delete函數刪除 map 中的元素,參數是 map 和須要被刪除的 key 。即便 key 不存在,也能安全調用delete函數。

delete(timeZone, "PDT")  // Now on Standard Time

Printing

Go 的格式化輸出與 C 的 printf很像,但功能更豐富。相關函數位於 fmt package 中,以首字母大寫命名,如fmt.Printffmt.Fprintffmt.Sprintf等等。字符串函數,如(Sprintf 等)會返回一個 string ,而不會直譯某個 buffer。

也能夠不提供 format string 。每一個Printf, Fprintf, Sprintf都有一個對應函數,如Print Println。這些函數不須要 format string 參數,由於它會給每一個參數生成一個默認格式。Print會兩個參數之間增長空格(只要任一參數是字符串),Println不只在參數之間增長空格,還會在行尾增長一個換行符號。下面的示例中,每行的輸出結果都同樣。

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

fmt.Fprint這類格式化輸出函數的第一個參數必須是實現了io.Writer接口的對象;好比常見的os.Stdoutos.Stderr

與C不一樣的是。%d這樣的格式化符號不須要表示符號或大小的標記(譯:好比不存在 %ld 表示 long int,而 %d 表示int這種狀況);
輸出函數能直接根據參數類型,決定這些屬性。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

輸出

18446744073709551615 ffffffffffffffff; -1 -1

你還能用 通用格式化符號%v ,這個符號有一套默認輸出格式,如對於整數來講,直接輸出十進制整數;其實PrintPrintln的輸出結果就這樣的。
這個格式化符號甚至能打印 arrays, slices structs 和 maps 。下面的代碼輸出 time zone map 類型。

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

輸出:

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

注意,maps 的 key 是亂序輸出的。輸出 struct 時,使用%+v這樣的格式化輸出符號能把字段名稱一塊兒輸出,而%#v則按完整的Go語法規則輸出值。

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}

注意t是struct 指針,因此輸出結果有與符號&

使用%q格式化string或者[]byte時,也會輸出雙引號""
使用%#q格式化符號,則會輸出反引號`
%q也可用於 integers 和 runes 類型,此時會輸出單引號'
另外,%x也可用於 strings, byte arrays, byte slices, integers,其輸出爲十六進制字符串。若是在格式化符號前增長空格(% x),則輸出的每一個 bytes 之間也會以空格分隔。

譯:
如下示例是譯者增長,參考: https://blog.golang.org/strings

package main
import"fmt"
func main() {
var x uint64 = 18
var str string = "1漢字string"
var byt []byte = []byte("2漢字byte")
var rne []rune = []rune("3漢字rune")
 
fmt.Printf("%d, %x, %v\n", x, x, x)
fmt.Printf("%q, %#q, %x, % x\n", x, x, x, x)
fmt.Printf("%q, %#q, %x, % x\n", str, str, str, str)
fmt.Printf("%q, %#q, %x, % x\n", byt, byt, byt, byt)
fmt.Printf("%q, %#q, %x, % x\n", rne, rne, rne, rne)
}

輸出

18, 12, 18
'\x12', '\x12', 12,  12
"1漢字string", `1漢字string`, 31e6b189e5ad97737472696e67, 31 e6 b1 89 e5 ad 97 73 74 72 69 6e 67
"2漢字byte", `2漢字byte`, 32e6b189e5ad9762797465, 32 e6 b1 89 e5 ad 97 62 79 74 65
['3' '漢' '字' 'r' 'u' 'n' 'e'], ['3' '漢' '字' 'r' 'u' 'n' 'e'], [33 6c49 5b57 72 75 6e 65], [ 33  6c495b57  72  756e  65]

還有一個經常使用格式化符號是%T,用於出變量類型。

fmt.Printf("%T\n", timeZone)

輸出

map[string] int

若是要控制自定義類型的默認輸出格式,只須要給自定義類型增長一個String() string方法簽名(signature)。
假設自定義類型是T,代碼實現就是下面這樣。

package main
import"fmt"
 
type TPointer struct {
    a int
    b float64
    c string
}
func (t *TPointer) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
type TValue struct {
    a int
    b float64
    c string
}
func (t TValue) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
func main() {
    fmt.Printf("%v\n", TPointer{ 7, -2.35, "tPointer abc\tdef" })
    fmt.Printf("%v\n", &TPointer{ 7, -2.35, "tPointer abc\tdef" })
     
    fmt.Printf("%v\n", TValue{ 7, -2.35, "tValue abc\tdef" })
    fmt.Printf("%v\n", &TValue{ 7, -2.35, "tValue abc\tdef" })
}

輸出如下格式

{7 -2.35 tPointer abc   def}
7/-2.35/"tPointer abc\tdef"
7/-2.35/"tValue abc\tdef"
7/-2.35/"tValue abc\tdef"

注意,String() 方法簽名的接收者是指針*T時,fmt.Printf 的參數也必須是指針,不然不會按自定義格式輸出。
String() 的接收者是值類型T時,沒有這種問題。可是用指針*T效率更高。詳細狀況參考pointers vs. value receivers

Sprintf是可重入函數,因此在String()方法簽名中能夠再次調用Sprintf。可是要當心,別在String()方法簽名中引起String()方法簽名的調用,這會無限循環調用String()
Sprintf中直接將接收者看成 string 輸出時,就會引發上面所述問題。這是一種常見的錯誤。
示例以下:

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

這個問題好解決,把參數強轉成 string 類型便可,由於 string 類型沒有使用 MyString 的 String() 簽名方法,也就不會引發無限循環調用的問題了。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

initialization section一節,咱們能用其餘方法解決這個問題。

另一點值得說明的技術是,print 函數(routine)參數傳遞的過程。
Printf使用...interface{}做爲最後一個參數,表示接收任意數量,任意類型的參數。

func Printf(format string, v ...interface{}) (n int, err error) {

Printf函數中,能夠把參數v當作[]interface{}使用。
但若是把v傳遞到其餘函數使用,就要將其轉爲列表參數(regular list of arguments)。下面是log.Println的實現代碼,它將參數直接傳遞到fmt.Sprintln進行實際的格式化操做。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))// Output takes parameters (int, string)
}

咱們在調用Sprintfln時在參數v後面加了幾個...,用來指明編譯器將v做爲列表變量(list of arguments);若是不加...v參數會被當作 slice 類型傳遞。

還有不少有關 print 的知識點沒有說起,詳細內容可能參考godoc中到fmt的說明。

順帶說一句,...參數也能夠用來指明具體類型,好比下面以...int爲參數的 min 函數,從一列 integers 中選取最小值。

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的設計。這個append與咱們以前自定義的Append有些區別,它的定義以下:

func append(slice []T, elements ...T) []T

T是表示任意類型的佔位符。Go中沒法實現一個參數類型T由調用者指定的函數。這正是爲什麼append是內置類型的緣由,由於它須要編譯器支持。

append的做用就是在 slice 中增長一個 element ,而後返回新的 slice 。
必須返回一個結果是由於,slice 底層的 array 可能改變。簡潔示例以下:

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

結果輸出[1 2 3 4 5 6]。appendPrintf都能接收任意個參數。

若是咱們把在 slice 後面追加一個 slice 怎麼作呢?很簡單,把 ... 放到參數後面就行,和上面示例中std.Output用法。下面示例代碼也輸出[1 2 3 4 5 6]。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

沒有...是沒法編譯經過的,由於類型不正確,y的類型是[]int,而不是int

Initialization

表面上看,Go的初始化過程和C/C++區別不大,其實Go的功能很強大的。
初始化過程不只能構造複雜的結構體,還能正確處理不一樣 package 之間的初始化順序。

Constants

Go中的常量就是不會改動的變量(constant)。
常量必須是 numbers, characters(runes), strings, booleans。
即便在函數中定義的局部常量,也是在編譯時期(compile time)建立的。
因爲編譯時的限制,定義常量的表達式必須能由編譯器計算。
好比 1<<3是可用的常量表達式,而math.Sin(math.Pi/4)就不行,由於math.Sin是函數調用,必須在運行時(run time)執行。

Go中能夠用iota建立枚舉(enumerate)變量。
iota是表達式的一部分,能自動疊加( implicitly repeated,譯:每行自動加1),這種特性方便定義複雜的常量集合。

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方法,能在 printing 時自動格式化輸出。
雖然這個特性常常用於 struct 中,但其實也能用在ByteSize這種浮點數(floating-point)上。

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(YB)會輸出1.00TB,而ByteSize(1e13)會輸出9.09TB

這個ByteSizeString方法實現是安全的(不會出現無限循環調用),並不是由於類型轉換(譯:並不是這個緣由,即表達式 b/YB 的結果轉換成 float64 類型後,就沒有了ByteSize 類型的 String 方法),而是由於這裏調用Sprintf時使用的參數%fSprintf只在指望 string 類型時,調用String方法,而使用%f時,指望的是 floating-point 類型。

Variables

變量與常量的初始化方法相似,但變量初值是在 run time 計算的。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

The init function

每一個源文件都能定義init(niladic )函數來設置一些初始狀態。(實際上每一個文件能夠包含多個init函數。)And finally means finally
在 package 中聲名的全部變量及其 import (導入)的 package 都初始化完畢後,纔會執行init函數。

(譯:如下非直譯)
init中可用於處理沒法在 declaration (聲明)中初始化的表達式,因此一般會在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

(譯註:這裏的 method 可理解爲類的成員方法)
可能給全部命名的類型(除 pointer 和 interface 外)定義 method ;receiver 不必定是 struct 。
就像上面ByteSize的例子就說明了這個特性。

好比以前討論到 slice 時提到的 Append函數其實能夠定義成 slice 的 method 。
爲達到這個目的,咱們先要定義一個類型,而後將這個類型做爲 method 的 receiver 。

type ByteSlice []byte
 
func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

這種方式仍然須要返回更新後的 slice。將method 的 receiver 類型改爲ByteSlice指針,就能在 method 中改變 receiver 的值。

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

咱們還能作的更好一點,若是把 Append修改爲下面這種標準Write方法的格式,

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

因而,*ByteSlice類型就符合io.Writer掊口。這是很實用的技巧,好比,能這樣寫入數據到ByteSlice:

var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)

示例中使用ByteSlice的指針做爲 Fprintf 的參數是由於*ByteSlice類型實現了io.Writer接口須要的方法(即Write方法的接收者類型是*ByteSlice)。

pointer methods,使用指針 做爲方法接收者,則必須經過 指針 調用此方法。
value methods,使用值 做爲方法接收者,則既能經過 值 也能經過指針調用此方法。
( 譯:實測並無此處所說問題,參考在線演示 )
產生以上限制的緣由是,pointer methods能夠修改 方法接收者。但使用 值調用方法時,被修改的變量是 接收者 的一個拷貝,因此修改操做被忽略了。
golang 語法不容許出現這樣的錯誤。不過,這有個例外狀況。當 value 是addressable的,golang編譯器會自動將經過 值 調用pointer methods的代碼轉換成經過 指針 調用。
在咱們的示例中,雖然Write方法是pointer methods,但 變量baddressable的,因此直接寫b.Write()這樣的代碼,也能調用Write方法。由於編譯器替咱們將代碼改寫成了(&b).Write()

順便一提,以上經過Write方法操做 slice bytes 的想法,已經在內置類bytes.Buffer中實現。

什麼是 addressable
官方描述
譯文
原文
簡單理解爲,常量沒法尋址,但變量確定會存儲在內存某個地方,能夠被尋址

  • 下面的值不能被尋址(addresses):
    bytes in strings:字符串中的字節
    map elements:map中的元素
    dynamic values of interface values (exposed by type assertions):接口的動態值
    constant values:常量
    literal values:字面值
    package level functions:包級別的函數
    methods (used as function values):方法
    intermediate values:中間值
    function callings
    explicit value conversions
    all sorts of operations, except pointer dereference operations, but including:
    channel receive operations
    sub-string operations
    sub-slice operations
    addition, subtraction, multiplication, and division, etc.
    注意, &T{}至關於tmp := T{}; (&tmp)的語法糖,因此&T{}可合法不意味着T{}可尋址。
  • 下面的值能夠尋址:
    variables
    fields of addressable structs
    elements of addressable arrays
    elements of any slices (whether the slices are addressable or not)
    pointer dereference operations

Interfaces and other types

Interfaces

Golang 提供 interface 接口來實現 'object‘對象相似的功能:if something can do this, then it can be used here
咱們其實已經看到過多個示例了。好比, 經過實現 String() method 來實現自定義輸出格式的功能;還有使用 Fprintf 打印實現Write() method 的類型。只有一兩個 method 的 interface 在Go代碼中很常見。而且 interface 的命名每每源於其實現的 method 方法名稱,好比,實現了Write() method 的 interface 稱作io.Writer

而且一個type類型能夠實現多個 interface。好比,若是一個集合(譯:這裏應該是專指數組集合,如 []string []int等)實現了sort.Interface interface 要求的 Len(), Less(i, j int) bool, and Swap(i, j int) 三個 method ,那它就能用sort.Sort()實現排序功能。
同時,還能再實現fmt.Stringer interface 要求的 String() method ,知足自定義輸出格式功能。

下面這個刻意爲之的例子中,Sequence type 就實現了 sort.Interfacefmt.Stringer 要求的幾個method。(譯:相似面向對象中,多重繼承的概念,但比多重繼承的概念要好理解,也好用得多)

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

Conversions

下面 Sequence 類型的 String() method 重用了 fmt.Sprint([]int{}) 函數。
咱們把 Sequence 轉換成 []int 類型,就能直接調用 fmt.Sprint([]int{}) 函數了。

func (s Sequence) String() string {
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

這就是,在 String() method 中使用類型轉換 conversion technique 技術調用 Sprintf 方法的示例。
由於兩個類型(Sequence and []int)本質是同樣的,只是名稱不一樣,因此可能合法(譯:且安全)的在兩個類型以前轉換。此次轉換不會建立新值,他只是臨時把已經存在的值當成另外一個類型使用。
(還有另外一種合法的轉換方式,好比把 int 轉換成 floating point 類型,此時就會建立一個新值。)
理所固然,Go程序中也能對集合 set 類型 執行類型轉換。下面就是 Sequence 的另外一種實現方法,由於使用了 sort.IntSlice(s),因此比以前的方法少寫了不少代碼。

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

如今,不用給 Sequence 類型實現 Len() Less() Swap() 三個 method ,只是經過幾回類型轉換,咱們就實現了相關的功能。固然,這種技術雖然管用,但實踐中並不經常使用類型轉換來實現排序功能。
That's more unusual in practice but can be effective.

Interface conversions and type assertions

相關文章
相關標籤/搜索