Golang(二)高效編程

Golang做爲一門新的編程語言,它借鑑了現有語言的思想但擁有着不一樣尋常的特性,使得有效的Go程序在性質上不一樣於其親屬編寫的程序。若是直接將C++或Java程序翻譯成Go程序也不太可能產生使人滿意的結果,由於從Go的角度來思考這個問題,能夠產生一個成功但又徹底不一樣的程序。要寫得好,理解它的性質和習語及Golang中既定約定(如命名、格式化、程序構造等)對於提供編寫清晰、慣用go代碼的技巧頗有幫助,這樣編寫出的程序也便於其餘Golang程序員理解。Golang素以簡潔高效著稱,我想這是對學習並掌握好語言的基礎知識並加以靈活運用的效果。本文將從Golang編程語言的數據類型、語法規則、語言特性方面進行介紹html

基本語法

Formatting格式化問題是最有爭議但最不一致的。人們能夠適應不一樣的格式樣式,若是每一個人都堅持相同的樣式,那麼能夠花費更少的時間在格式化主題上。在go語言中採起了一種不尋常的方法,讓機器處理大多數格式化問題。gofmt程序(也可用做go fmt,在包級別而不是源文件級別運行)讀取go程序,並以縮進和垂直對齊的標準樣式發佈源代碼,保留並在必要時從新格式化註釋git

示例

package main程序員

import "fmt"複製代碼
func main() {
                    a:= 12//initial a
    fmt.Printf("hello, world, a = %v\n", a)//print
    }複製代碼
## gofmt格式化: gofmt -s -w hello.go
    package main複製代碼
import "fmt"複製代碼
func main() {
                    a := 12                                 //initial a
                    fmt.Printf("hello, world, a = %v\n", a) //print
    }複製代碼

標準包中的全部go代碼都已用gofmt格式化。一些格式細節仍然保留,簡要說明web

  • Indentation 使用製表符縮進,默認狀況下gofmt會發布製表符,只有在必要時才使用空格
  • Line length go沒有行長度限制,若是一行太長,用一個額外的標籤把它包起來縮進
  • Parentheses go須要的括號比C和Java少: 控制結構(if, for, switch)的語法中沒有括號;一樣,運算符的優先級層次越短越清晰,所以,
x<<8 + y<<16複製代碼

註解 Go提供C風格的塊註解/**/及C++風格的行註解//。行註釋是規範;塊註釋主要做爲包註釋出現,但在表達式中頗有用,或用於禁用大片代碼。godoc程序經過源文件來提取有關包內容的文檔,在頂級聲明以前出現的註釋(沒有中間的換行符)將與聲明一塊兒提取,做爲解釋文本。這些註釋的性質和樣式決定了godoc生成的文檔的質量。每一個包都應該有一個包註釋,即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複製代碼

若是包很簡單,包註釋能夠很簡短express

// Package path implements utility routines for
    // manipulating slash-separated filename paths.複製代碼

評論不須要額外的格式,生成的輸出甚至可能不會以固定寬度的字體顯示,因此不要依賴於對齊的間距,gofmt會處理這個問題。註釋是純文本,所以html和其餘註釋將逐字複製,不該使用。godoc作的一個調整是以固定寬度的字體顯示縮進的文本,適合程序片斷編程

在包中,頂級聲明前面的任何註釋都將做爲該聲明的文檔註釋。程序中的每一個導出(首字母大寫)名稱都應該有一個doc註釋。doc註釋做爲完整的句子最有效,它容許各類各樣的自動化演示。第一句話應該是一句話的總結,以所聲明的名稱開頭json

// 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) {複製代碼

命名 命名在Go語言和其餘語言中同樣重要,它們甚至具備語義效果: 包外名稱的可見性取決於其第一個字符是否大寫。所以,在go程序中花點時間討論命名約定是值得的api

*包名* 當包被導入時,包名稱將成爲內容的訪問器數組

import "bytes"複製代碼

導入包後能夠調用bytes.Buffer; 若是每一個使用包的人均可以使用相同的名稱來引用它的內容,這將頗有幫助,這意味着包的名稱應該具備:簡短、簡潔、使人印象深入的特色。按照慣例,包的名稱是小寫的、單字的;不須要下劃線或混合大寫;包名稱是其源目錄的基名稱;src/encoding/base64中的包被導入爲「encoding/base64」,但名稱爲base64,而不是base64中的編碼,也不是encodingbase64

Getters go不爲getter和setter提供自動支持。本身提供getter和setter並無錯,這樣作一般是合適的,但不是慣用的,也不是必須使用getter的名稱。若是有一個名爲owner的字段(小寫,不可導出),則getter方法應稱爲Owner(大寫,可導出),而不是GetOwner。在導出中使用大寫名稱提供一個鉤子來區分字段和方法。若是須要,setter函數可能會被稱爲SetOwner

owner := obj.Owner()
    if owner != user {
            obj.SetOwner(user)
    }複製代碼

接口名稱 按照慣例,接口由方法名加上er後綴或相似的修改來命名,例如: reader、writer、formatter、closenotifier等

MixedCaps go中的約定是使用MixedCaps或mixedCaps格式來編寫多個單詞的名稱,而不是使用下劃線鏈接

分號 與C同樣,go的形式語法使用分號來終止語句,但與c不一樣的是,這些分號不會出如今源代碼中,lexer使用一個簡單的規則在掃描時自動插入分號,所以輸入的文本基本上沒有分號。規則:若是換行符以前的最後一個標記是標識符(包括int和float64等詞)、基本文本(如數字或字符串常量)或其中一個標記,則lexer始終在該標記以後插入分號。這能夠歸納爲:「若是換行符位於能夠結束語句的標記以後,請插入分號」

break continue fallthrough return ++ -- ) }複製代碼

在右大括號以前也能夠省略分號; go程序習慣用法只有在for循環子句等位置纔有分號,以分隔初始值設定項、條件和循環元素。它們也是在一行中分隔多個語句所必需的,您應該這樣編寫代碼

go func() { for { dst <- <-src } }()複製代碼

分號插入規則的一個結果是,不能將控件結構的左大括號(if、for、switch或select)放在下一行。若是這樣作,將在大括號以前插入分號,這可能會形成沒必要要的影響

// like this
    if i < f() {
            g()
    }複製代碼
// 而不是
    if i < f()  // wrong!
    {           // wrong!
            g()
    }複製代碼

控制結構 go的控制結構與C的控制結構有關,但存在諸多不一樣。沒有do或while循環,只有一個輕微泛化的for;switch更靈活;if和switch接受一個相似for的可選初始化語句;break和continue語句使用一個可選標籤來標識要中斷或繼續的語句塊;新的控制結構包括類型開關和多路通訊複用器--select。語法也略有不一樣: 沒有括號,正文必須始終用大括號分隔

If go中一個簡單的if語句相似

if x > 0 {
        return y
    }複製代碼

強制大括號鼓勵在多行上編寫簡單的if語句。不管如何,這樣作是一種好的風格,尤爲是當主體包含這樣一個返回或中斷的控制語句時。if和switch接受初始化語句,因此一般會看到用於設置局部變量的語句

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

在go庫中,您會發現當一個語句沒有流入下一個語句(即,主體以break、continue、goto或return結尾)時,沒必要要的else語句將被省略

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

這是一個常見狀況的例子,代碼必須防止一系列錯誤條件。若是成功的控制流在頁面上運行,代碼讀起來很好,從而消除了出現的錯誤狀況。因爲錯誤狀況每每以返回語句結尾,所以生成的代碼不須要其餘語句

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

從新聲明而且從新賦值 := 符號聲明並賦值,參考以上一個示例

## 調用os.Open(),聲明兩個變量f和err
    f, err := os.Open(name)複製代碼
## 接下來調用f.Stat(),聲明變量d和err
    d, err := f.Stat()複製代碼

err變量出如今兩個語句中,這種重複是合法的: err在第一個語句中聲明,在第二個語句中從新分配。這意味着對f.Stat()的調用使用了上面聲明的現有err變量,並給它分配一個新的值

在 := 聲明中,即便變量v已經聲明,也可能再出現,前提是

  • 此聲明與v的現有聲明在同一做用域以內;若是v已經在外部做用域聲明,則此聲明將建立一個新變量§
  • 初始化中的相應值可分配給v,而且
  • 聲明中至少有一個變量是新的(第一次聲明)

For go的for循環相似於但又不一樣於C,它統一了for和while,沒有do-while。如下三種表達形式,只有一種帶有分號

// Like a C for
    for init; condition; post { }複製代碼
// Like a C while
    for condition { }複製代碼
// Like a C for(;;)
    for { }複製代碼

簡短的語句使在循環中聲明索引變量變得很容易

sum := 0
    for i := 0; i < 10; i++ {
            sum += i
    }複製代碼

若是在數組、切片、字符串或map上循環,或從channel讀取,則使用range子句能夠管理循環

for key, value := range oldMap {
            newMap[key] = value
    }複製代碼

若是隻須要range中的第一項(key或索引號),能夠丟棄第二項

for key := range m {
            if key.expired() {
                    delete(m, key)
            }
    }複製代碼

若是隻須要range中的第二項(值),使用空白標識符(下劃線)放棄第一項

sum := 0
    for _, value := range array {
            sum += value
    }複製代碼

對於字符串,range經過解析UTF-8來分解各個Unicode編碼,錯誤的編碼消耗一個字節併產生替換的rune(寬字符) U+FFFD。rune,Go術語,表示單個Unicode編碼(相關的內置類型)

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沒有逗號運算符,++和--are語句不是表達式。所以,若是要在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 go的switch比C更通用,表達式沒必要是常量甚至沒必要是整數,在找到匹配項以前將至上而下評估case。若是switch沒有表達式,switches on 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
    }複製代碼

不存在自動篩選,能夠用逗號分隔case的列表

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

break語句能夠提早終止switch和循環語句,儘管它在Go中不似C語言中那麼常見,不過有時有必要從一個環繞的循環中脫離出來,能夠經過在循環上貼上標籤並從標籤上「斷開」來跳出循環。這個例子展現了這兩種用途

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)
            }
        }複製代碼

continuous語句也接受一個可選的標籤,但它只適用於循環

如下是使用兩個switch語句的字節切片比較例程

// 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還能夠用來發現接口變量的動態類型,這樣的type switch使用類型斷言的語法,括號內有關鍵字type。若是switch在表達式中聲明瞭一個變量,則該變量在每一個子句中將具備相應的類型;在這種狀況下重用名稱也是慣用的,其實是用相同的名稱聲明一個新變量,但在每種狀況下都使用不一樣的類型

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
    }複製代碼

函數和方法

函數 多返回值 Go的一個不尋常的特性是函數和方法能夠返回多個值,帶內錯誤返回,例如-1表示EOF,修改按地址傳遞的參數。在Go語言中,Write函數能夠返回一個計數(寫入的字節數)和一個錯誤,os包中Write函數的簽名以下

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

相似的方法消除了將指針傳遞到返回值以模擬引用參數的須要。這裏有一個簡單的函數從字節切片中的一個位置獲取一個數字,返回數字和下一個位置

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
    }複製代碼

也能夠用它像這樣掃描輸入切片b中的數字

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

命名結果參數 go函數的返回或結果「參數」能夠被命名並用做常規變量,就像傳入的參數同樣。當函數啓動時,被命名的參數將被初始化爲它們類型的零值;若是函數執行不帶參數的返回語句,則結果參數的當前值將用做返回值。這些名稱不是強制性的,但它們可使代碼更短、更清晰;以下定義一個函數netxInt的兩個int類型返回參數的名稱爲value和nextPos

func nextInt(b []byte, pos int) (value, nextPos int) {複製代碼

命名結果是初始化的,而且綁定到未修飾的返回,因此它們既能夠簡化,也能夠聲明。以io.ReadFull的一個版本爲例

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語句將函數調用(deferred函數)安排在執行defer返回的函數以前當即運行。這是一種不尋常但有效的方法來處理這樣的狀況,不管函數返回的路徑如何,都必須釋放資源。典型的例子是解鎖互斥鎖或關閉文件

// Contents 以字符串形式返回文件的內容.
    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等函數的調用有兩個優勢:第一,它保證您永遠不會忘記關閉文件,若是之後編輯函數以添加新的返回路徑,則很容易犯這個錯誤;第二,這意味着Close位於Open附近,這比將其放在函數末尾要清楚得多

deferred函數的參數(若是函數是一種方法,則包括接收器)在defer執行時計算,而不是在調用時計算。除了避免擔憂變量在函數執行時更改值以外,這意味着一個延遲調用站點能夠延遲多個函數的執行

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

defer函數以LIFO順序執行,所以,上述代碼返回時將會打印4 3 2 1 0 。一個更合理的例子,是經過簡單的程序跟蹤函數執行,咱們能夠編寫一些簡單的跟蹤例程,以下所示

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....
    }複製代碼

咱們能夠更好地利用這樣一個事實,即deferred函數的參數是在延遲執行時計算的。跟蹤例程能夠將參數設置爲untracing例程。示例

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
    複製代碼

方法

*指針 vs. 值* 正如咱們在ByteSize中看到的,能夠爲任何命名類型(指針或接口除外)定義方法;接收器沒必要是結構。在上面對切片的討論中,咱們編寫了一個Append函數,能夠將其定義爲切片上的方法。爲此,首先聲明一個能夠綁定該方法的命名類型,而後將該方法的接收器設置爲該類型的值

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

這仍然須要方法返回更新後的切片,能夠經過從新定義方法來消除這種冗餘操做,將指向字節切片的指針做爲其接收器,這樣方法就能夠覆蓋調用方的切片

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

事實上,咱們能夠作得更好,若是修改咱們的函數使其看起來像一個標準的write方法,就像這樣

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

而後,類型*ByteSize知足標準io.Writer接口,例如,咱們能夠打印成一個

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

此處,傳遞ByteSize類型的地址是由於只有*ByteSize知足io.Writer。關於指針和接收器值的規則是,值方法能夠在指針和值上調用,但指針方法只能在指針上調用。這一規則的產生是由於指針方法能夠修改接收器;對值調用它們將致使方法接收值的副本,所以任何修改都將被丟棄。所以,該語言不容許出現此錯誤。不過,有一個例外:當值是可尋址的時,語言經過自動插入address運算符來處理在值上調用指針方法的常見狀況。在咱們的例子中,變量b是可尋址的,因此咱們能夠用b.Write調用它的Write方法。編譯器將會爲咱們重寫爲(&b).Write。順便提一下,在字節切片上使用Write的思想是bytes.Buffer實現的核心

數據及初始化

使用new分配 go有兩個分配原語,分別是內置函數new和make,它們應用於不一樣的類型作不一樣的事情。對於new,它是一個分配內存的內置函數,而且只將其歸零而不初始化內存。也就是說,new(T)爲T類型的新對象分配零存儲並返回它的地址,即類型*T的值。在go的術語中,它返回一個指向新分配的T類型零值指針

因爲new返回的內存爲零,所以在設計數據結構時,最好安排在不進行進一步初始化的狀況下使用每種類型的零值。這意味着數據結構的用戶可使用new建立一個數據結構,並得到工做權限。例如,bytes.buffer的文檔聲明「buffer的零值是一個空緩衝區,可使用。」一樣,sync.mutex沒有顯式的構造函數或init方法。相反,sync.mutex的零值被定義爲未加鎖的mutex

zero-value-is-useful屬性能夠透明工做,開旅下面的類型聲明

type SyncedBuffer struct {
            lock    sync.Mutex
            buffer  bytes.Buffer
    }複製代碼

SyncedBuffer類型的值也能夠在分配或僅聲明時當即使用。在如下代碼片斷中,p和v都將正常工做,無需進一步安排

p := new(SyncedBuffer)  // type *SyncedBuffer
    var v SyncedBuffer      // type  SyncedBuffer複製代碼

構造器和文字複合 有時零值不夠好,須要初始化構造函數,如本例中從包os派生的File

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
    }複製代碼

可使用複合文本(composite literal)來簡化它,複合文本是一個表達式,它在每次求值時都建立一個新實例

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}複製代碼

做爲一個限制條件,若是一個文本組合根本不包含字段,它將爲類型建立一個零值。表達式new(File)&File{}是等價的。還能夠爲數組、切片和映射建立複合文本,字段標籤是索引或映射鍵(視狀況而定),在這些示例中,無論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"}複製代碼

使用make分配 內置函數make(T, args)用途不一樣於new(T),它只用於建立slices、maps和channels而且返回一個已經初始化的(非零的)類型爲T(不是*T)的值;其中,差異的緣由是這三種類型存在對底層數據結構的引用,這些數據結構必須在使用以前初始化。例如,切片數據結構包括:指向數據(數組內部)、長度和容量的指針,在初始化這些項以前,切片是nil。對於切片、maps和channels,make初始化其內部數據結構,並準備要使用的值。例如

make([]int, 10, 100)複製代碼

分配一個100個整數的數組,而後建立一個長度爲十、容量爲100的切片結構,指向數組的前面第10個元素。相反,new([]int)返回一個指向新分配的零切片結構的指針,即指向nil切片值的指針。這些例子說明了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)複製代碼

*Tips make只適用於maps、slices和channels,不返回指針;使用new獲取顯式指針分配或顯示獲取變量地址

數組 數組在規劃內存的詳細布局時頗有用,有時能夠幫助避免分配,但它們主要是用於切片的構建塊。數組在Go中工做方式與C有很大的不一樣,主要表如今

  • 數組是值,將一個數組分配給其餘數組將複製全部元素
  • 若是將數組傳遞給函數,它將收到數組的副本,而不是指向它的指針
  • 數組的大小是其類型的一部分,類型[10]int和[20]int是不一樣的
value屬性可能頗有用,但也很昂貴;若是要相似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)  // 注意運算符的顯示地址,即便是這種方式也不是Go慣用的方法,slice纔是複製代碼

Slices 切片包裝數組,爲數據序列提供更通用、更強大、更方便的接口。除了具備顯式維度的項(如轉換矩陣)外,go中的大多數數組編程都是用切片而不是簡單數組完成的。切片保存對底層數組的引用,若是將切片分配給另外一個切片,則兩個切片都引用同一個數組。若是一個函數接受一個slice參數,它對slice元素所作的更改將對調用者可見,相似於將指針傳遞給底層數組。所以,Read函數能夠接受slice參數,而不是指針和計數;slice中的長度設置了要讀取多少數據的上限。下面是包os中文件類型的Read方法的函數簽名

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

該方法返回讀取的字節數和錯誤值(若是有),要讀入較大緩衝區的前32個字節,請分割緩衝區

n, err := f.Read(buf[0:32])複製代碼

這樣的切片是常見而有效的。事實上,暫時不考慮效率,下面的代碼片斷還將讀取緩衝區的前32個字節

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

一個切片的長度能夠改變,只要它仍然在底層數組的限制範圍內;只需將它分配給自身的一個切片。切片的容量由內置函數cap訪問,報告切片可能假定的最大長度。以下示例是一個Append函數,用於將數據追加到切片: 若是數據超出容量,則從新分配切片,返回結果切片

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)]
            copy(slice[l:], data)
            // 此處必須返回append以後ded切片,由於雖然append能夠修改切片的元素,但切片自己(包含指針、長度和容量的運行時數據結構)是按值傳遞的
            return slice
    }複製代碼

二維切片 go的數組和切片是一維的,要建立與二維數組或切片等效的數組或切片,須要定義array-of-arrays或slice-of-slices,以下所示

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
    type LinesOfText [][]byte     // A slice of byte slices.複製代碼

切片是可變長度的,因此可讓每一個內部切片具備不一樣的長度。這多是一種常見的狀況,如咱們的LinesOfText示例:每一行都有獨立的長度

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

有時候須要申請一個二維切片,例如,scan在處理像素時可能出現這種狀況。有兩種方式實現:其一,獨立地申請每個切片;另外一種是申請一個數組並將各個切片指向其中;具體使用哪一種方式取決於應用程序。若是切片可能會增加或收縮,則應單獨分配它們,以免覆蓋下一行;若是沒有,則能夠更有效地使用單個分配構造對象。如下是這兩種方法的示意圖,以供參考。首先,一次一行

// 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)
    }複製代碼

方法二:申請一個大的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 map是一種方便而強大的內置數據結構,它將一種類型(鍵)的值與另外一種類型(元素或值)的值相關聯。鍵能夠是定義了相等運算符的任何類型,例如整數、浮點和複數、字符串、指針、接口(只要動態類型支持相等)、結構和數組;切片不能用做map的鍵,由於它們沒有定義相等性。與切片同樣,map保存對底層數據結構的引用: 若是將map傳遞給更改map內容的函數,則這些更改將在調用方中可見

可使用一般的複合文本語法和冒號分隔的鍵值對來構造,所以在初始化期間很容易構建它們

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值在語法上與對數組和切片執行相同,只是索引不須要是整數

offset := timeZone["EST"]複製代碼

使用map中不存在的鍵獲取map值時,將返回map中item類型的零值。例如,若是map包含整數,查找不存在的鍵將返回0; 集合能夠實現爲值類型爲bool的映射,將map item設置爲true以將值放入集合中,而後經過簡單的索引對其進行測試

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")
    }複製代碼

有時須要區分缺乏的條目和零值。是否存在「utc」的條目?或者是0由於它根本不在map中,你能夠用多重分配的形式來區分

var seconds int
    var ok bool
    seconds, ok = timeZone[tz]複製代碼

這被稱爲"comma-ok"習語,在這個例子中,若是TZ存在seconds將被適當設置,ok將爲true;不然,seconds被設置爲零值,ok將爲false。下面是一個函數,它將與一個error report放在一塊兒

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

在不關心實際值的狀況下測試map中是否存在,可使用空白標識符代替值的經常使用變量

_, present := timeZone[tz]
複製代碼

要刪除map條目,請使用delete內置函數,該函數的參數是要刪除的map和key。即便key已經不在map上,這樣作也是安全的

delete(timeZone, "PDT")  // Now on Standard Time複製代碼

Printing go中的格式化打印使用了相似於C的printf系列的樣式,但更加豐富和通用。這些函數位於fmt包中,並有大寫的名稱:fmt.Printf、fmt.Fpprintf、fmt.Sprintf等等。字符串函數(sprintf等)返回字符串,而不是填充提供的緩衝區。不須要提供格式字符串,對於Printf、Fprintf和Sprintf,都有另外一對函數,例如Print和Println。這些函數不接受格式字符串,而是爲每一個參數生成默認格式。Println版本也在參數之間插入一個空格,並在輸出中追加一個換行符,而Print版本只在兩邊的操做數都不是字符串時才添加空格。在本例中,每一行產生相同的輸出

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和friends將實現io.Writer接口的任何對象做爲第一個參數;變量os.Stdout和os.Stderr是常見的實例。更多細節此處不一一贅述

Append 內置Append函數的簽名與上面自定義的append函數不一樣,其定義以下

func append(slice []T, elements ...T) []T複製代碼

此處,T是任何給定類型的佔位符。在go中,T類型由調用方決定,這就是append內置的緣由:它須要編譯器的支持。append的做用是將元素附加到切片的末尾並返回結果。須要返回結果,由於與手工編寫的append同樣,底層數組可能會更改。這個簡單的例子

x := []int{1,2,3}
    x = append(x, 4, 5, 6)
    fmt.Println(x)
    ## 打印結果[1 2 3 4 5 6],因此append的工做方式有點像printf,收集任意數量的參數
複製代碼

Initialization 雖然看起來與C或C++中的初始化沒有太大的區別,但Go中的初始化功能更強大。在初始化過程當中能夠構建複雜的結構,而且正確地處理初始化對象之間的排序問題,甚至不一樣包之間的排序問題

Constants Go中的常量就是常數,在編譯時建立,即便在函數中定義爲局部變量,也只能是數字、字符(rune)、字符串或布爾。因爲編譯時的限制,定義它們的表達式必須是常量表達式,由編譯器計算,例如,1<<3是一個常量表達式,而math.sin(math.pi 4)不是,由於函數調用math.sin發生在運行時<="" p="">

在go中,使用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)
    }複製代碼

Variables 變量能夠像常量同樣初始化,但初始化器能夠是在運行時計算的通用表達式

var (
            home   = os.Getenv("HOME")
            user   = os.Getenv("USER")
            gopath = os.Getenv("GOPATH")
    )複製代碼

init函數 每一個源文件均可以定義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")
    }
複製代碼

接口及其餘類型

接口 go中的接口提供了一種指定對象行爲的方法。在go代碼中,只有一個或兩個方法的接口是常見的,一般會被賦予一個從該方法派生的名稱,例如io.Writer用於實現Write的對象一個類型能夠實現多個接口。例如,一個集合實現了sort.Interface(包含Len()、Less(i, j int) bool和Swap(i, j int)),那麼它能夠按包sort中的例程進行排序,而且它還能夠有一個自定義格式化程序。在這個虛構的例子中,Sequence同時知足條件

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]
    }複製代碼
// Copy returns a copy of the Sequence.
    func (s Sequence) Copy() Sequence {
            copy := make(Sequence, 0, len(s))
            return append(copy, s...)
    }複製代碼
// Method for printing - sorts the elements before printing.
    func (s Sequence) String() string {
            s = s.Copy() // Make a copy; don't overwrite argument.
            sort.Sort(s)
            str := "["
            for i, elem := range s { // Loop is O(N²); will fix that in next example.
                    if i > 0 {
                            str += " "
                    }
                    str += fmt.Sprint(elem)
            }
            return str + "]"
    }複製代碼

Conversions Sequence的String方法正在從新建立Sprint已經爲切片所作的工做(它具備O(N*N)時間複雜度,性能不好)。若是在調用Sprint以前將序列轉換爲[]int,咱們就能夠共享工做(也能夠加快工做速度)

func (s Sequence) String() string {
            s = s.Copy()
            sort.Sort(s)
            return fmt.Sprint([]int(s))
    }複製代碼

此方法是轉換技術的另外一個示例,用於從String方法安全地調用Sprintf。由於兩個類型(Sequence和[]int)是相同的,若是咱們忽略類型名,那麼在它們之間進行轉換是合法的。轉換不會建立新值,它只是臨時地充當現有值具備新類型的角色。(還有其餘合法的轉換,例如從整數到浮點的轉換,這裏會建立新值)go程序中的一個習慣用法是將表達式的類型轉換爲訪問一組不一樣的方法。例如,咱們可使用現有的sort.Intslice類型將整個示例縮減爲

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

咱們再也不讓Sequence實現多個接口(排序和打印),而是使用將數據項轉換爲多個類型(Sequence、sort.Intslice和[]int)的能力,每一個類型都有效地完成部分工做

Interface conversions and type assertions Type switches是轉換的一種形式:它們接受一個接口,對於switch中的每一種狀況,在某種意義上都將其轉換爲該狀況下的類型。下面是fmt.Printf下的代碼:使用Type Switches將值轉換爲字符串的簡化版本。若是它已是一個字符串,接口返回實際的字符串值;而若是它有一個字符串方法,調用該方法的結果

type Stringer interface {
        String() string
    }複製代碼
var value interface{} // 調用方提供的值
    switch str := value.(type) {
    case string:
        return str
    case Stringer:
        return str.String()
    }複製代碼

第一種狀況找到一個具體的值;第二種狀況將接口轉換爲另外一個接口

若是咱們只關心一種類型呢?若是咱們知道這個值包含一個字符串,咱們只想提取它?one-case的type switch能夠實現,類型斷言也能夠。類型斷言接受接口值,並從中提取指定顯式類型的值

value.(typeName)複製代碼

結果是一個typeName類型的新值,該類型必須是具體的接口類型,或者值能夠轉換爲第二個接口類型。要提取值中的字符串,咱們能夠編寫

str := value.(string)複製代碼

若是發現結果不包含字符串,會形成程序崩潰並出現運行時錯誤。這裏使用"comma, ok"來安全地測試值是不是字符串

str, ok := value.(string)
    if ok {
            fmt.Printf("string value is: %q\n", str)
    } else {
            fmt.Printf("value is not a string\n")
    }複製代碼

若是類型斷言失敗,str將仍然存在併爲string類型,只是它將具備零值,即空字符串

Generality 若是某類型的存在只爲實現一個接口,而且不會在該接口以外存在導出方法,那麼該類型自身也不須要被導出。僅導出接口能夠清楚地代表,除了接口中描述的內容以外,它沒有任何有趣的行爲。在這種狀況下,構造函數應該返回一個接口值,而不是實現類型。例如,在hash庫中,crc32.NewIEEE和adler32.New都返回接口類型hash.Hash32。用CRC-32算法替換GO程序中的Adler-32只須要更改構造函數調用;其他代碼不受算法更改的影響

相似的方法容許不一樣密碼包中的流密碼算法與它們連接在一塊兒的塊密碼分離。crypto/cipher包中的塊接口指定塊密碼的行爲,它提供對單個數據塊的加密。而後,經過與bufio包的類比,實現這個接口的密碼包能夠用來構造流式密碼,用流接口表示,而不須要知道塊加密的細節。crypto/cipher形如

type Block interface {
            BlockSize() int
            Encrypt(dst, src []byte)
            Decrypt(dst, src []byte)
    }複製代碼
type Stream interface {
            XORKeyStream(dst, src []byte)
    }複製代碼
// 這裏是counter mode(CTR)stream 的定義,它將分組密碼轉換爲流密碼;請注意,分組密碼的詳細實現被抽象出來了
    // NewCTR不只適用於一個特定的加密算法和數據源,還適用於塊接口和任何流的任何實現。由於它們返回接口值,因此用其餘加密模式替換CTR加密是一種本地化的更改複製代碼
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
    // counter mode. The length of iv must be the same as the Block's block size.
    func NewCTR(block Block, iv []byte) Stream複製代碼

Interfaces and methods 幾乎任意類型均可以附加方法,幾乎任何類型均可以實現接口。以http包爲例,它定義了handler接口,任何實現handler的對象均可覺得http請求提供服務

type Handler interface {
            ServeHTTP(ResponseWriter, *Request)
    }複製代碼

ResponseWriter自己就是一個接口,它提供將響應返回給客戶端所需方法的訪問。這些方法包括標準的Write方法,所以能夠在可使用io.Writer的任何地方使用http.ResponseWriter。Request是一個結構,包含來自客戶端的請求的解析表示

爲了簡潔起見,讓咱們忽略POST並假設http請求老是GET方式;這種簡化不會影響handler的設置方式。下面是一個簡單但完整的處理程序實現,用於計算訪問頁面的次數

// Simple counter server.
    type Counter struct {
            n int
    }複製代碼
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
            ctr.n++
            fmt.Fprintf(w, "counter = %d\n", ctr.n)
    }複製代碼
// 做爲參考,將Counter服務附加到url樹上
    import "net/http"
    ...
    ctr := new(Counter)
    http.Handle("/counter", ctr)
    複製代碼

爲何要把計數器變成一個結構呢?只須要一個整數(接收器須要是指針,以便調用者能夠看到增量)

// Simpler counter server.
    type Counter int複製代碼
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
            *ctr++
            fmt.Fprintf(w, "counter = %d\n", *ctr)
    }複製代碼

若是您的程序有一些內部狀態,須要通知您訪問了某個頁面,該如何處理?經過將channel綁定到web頁面來實現

// A channel that sends a notification on each visit.
    // (Probably want the channel to be buffered.)
    type Chan chan *http.Request複製代碼
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
            ch <- req
            fmt.Fprint(w, "notification sent")
    }複製代碼

假設咱們想在args上顯示調用服務二進制文件時使用的參數。能夠很容易地編寫一個函數來打印參數

func ArgServer() {
            fmt.Println(os.Args)
    }複製代碼
// 如何將其轉換爲http服務器?咱們可讓ArgServer成爲某種類型的方法,忽略它的值,但有一種更簡潔的方法
    // 能夠爲除了指針和接口以外的任何類型定義方法,因此咱們能夠爲函數編寫方法。http包中包含此代碼複製代碼
// The HandlerFunc type is an adapter to allow the use of
    // ordinary functions as HTTP handlers.  If f is a function
    // with the appropriate signature, HandlerFunc(f) is a
    // Handler object that calls f.
    type HandlerFunc func(ResponseWriter, *Request)複製代碼
// ServeHTTP calls f(w, req).
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
            f(w, req)
    }複製代碼
// HandlerFunc是一個帶有ServeHTTP方法的類型,所以該類型的值能夠服務於http請求
    // 看看方法的實現:接收器是一個函數f,方法調用f。這可能看起來很奇怪,但與接收器是一個通道和在通道上發送的方法沒有太大區別複製代碼
// 要使ArgServer成爲一個http服務,咱們首先修改其函數簽名
    // Argument server.
    func ArgServer(w http.ResponseWriter, req *http.Request) {
            fmt.Fprintln(w, os.Args)
    }複製代碼
// AargServer如今與HandlerFunc具備相同的函數簽名,所以能夠將其轉換爲該類型以訪問其方法
    http.Handle("/args", http.HandlerFunc(ArgServer))複製代碼
// 當有人訪問/args頁面時,安裝在該頁面上的handler具備值ArgServer和類型HandlerFunc
    // http服務將調用該類型的方法ServeHTTP,以ArgServer做爲接收器,後者將依次調用ArgServer(經過HandlerFunc.ServeHTTP中的調用f(w,req))。而後顯示參數
複製代碼

在本節中,從一個結構、一個整數、一個channel和一個函數建立了一個http服務,這是由於接口只是一組方法,(幾乎)能夠爲任何類型定義

嵌入 go不提供典型的、類型驅動的子類概念,但它確實可以經過在結構或接口中嵌入類型來「借用」已實現的片斷。接口嵌入很是簡單。咱們以前提到過io.Reader和io.Writer接口,下面是它們的定義

type Reader interface {
            Read(p []byte) (n int, err error)
    }複製代碼
type Writer interface {
            Write(p []byte) (n int, err error)
    }複製代碼

io包還導出其餘幾個接口,這些接口指定能夠實現多個此類方法的對象。例如,有io.ReadWriter,一個同時包含讀和寫的接口。咱們能夠經過顯式地列出這兩個方法來指定io.ReadWriter,可是嵌入這兩個接口以造成新的接口更容易引發注意,以下所示

// ReadWriter is the interface that combines the Reader and Writer interfaces.
    type ReadWriter interface {
            Reader
            Writer
    }複製代碼

ReadWriter能夠作Reader和Writer作的事情;它是嵌入式接口(必須是不相交的方法集)的並集;接口中只能嵌入接口。一樣的基本思想也適用於struct,bufio包有兩種結構類型:bufio.Reader和bufio.Writer,固然每種類型都實現io包中相似的接口。bufio還實現了一個緩衝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類型如今具備Print、Printf、Println和*log.Logger的其餘方法。固然也能夠給Logger一個字段名,但沒必要這樣作。如今一旦初始化,咱們就能夠爲job記錄日誌

job.Println("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)}
複製代碼

空白標識符

空白標識符可使用任何類型的值進行分配或聲明,而且該值將被無害地丟棄(這有點像對Unix /dev/null文件的寫入)。它表示一個只寫的值,在須要變量但不關心實際值的地方使用一個佔位符,具備很是好的用途

多重賦值中的空白標識符 在for-range循環中使用空白標識符是通常狀況下的一種特殊場景:多重賦值。若是一個賦值要求在左邊有多個值,但其中一個值不被程序使用,賦值左邊的空白標識符能夠避免建立一個僞變量,並清楚地代表該值將被丟棄。例如,當調用返回值和error的函數時,若是隻有error是重要的,則使用空標識符丟棄不關心的值

if _, err := os.Stat(path); os.IsNotExist(err) {
        fmt.Printf("%s does not exist\n", path)
    }複製代碼

有時您會看到爲了忽略錯誤而丟棄錯誤值的代碼;這是一種糟糕的作法

// Bad! This code will crash if path does not exist.
    fi, _ := os.Stat(path)
    if fi.IsDir() {
            fmt.Printf("%s is a directory\n", path)
    }複製代碼

未使用的導入和變量 導入包或聲明變量而不使用會發生編譯錯誤。未使用的導入會使程序膨脹並致使編譯緩慢,而一個已初始化但未使用的變量至少是一個浪費的計算,而且可能存在潛在bug。然而,當程序處於開發階段時,常常會出現未使用的導入和變量,爲了讓編譯繼續運行而刪除它們,但若是後續再次須要它們...就比較繁瑣。空白標識符提供了一種解決方法

這個半成品程序有兩個未使用的導入(fmt和io)和一個未使用的變量(fd),看起來代碼正確無誤,實際上它是不可編譯的

package main複製代碼
import (
            "fmt"
            "io"
            "log"
            "os"
    )複製代碼
func main() {
            fd, err := os.Open("test.go")
            if err != nil {
                    log.Fatal(err)
            }
            // TODO: use fd.
    }複製代碼

若要消除有關未使用導入的報錯,請使用空白標識符引用導入包中的符號。相似地,將未使用的變量fd分配給空白標識符將使未使用的變量錯誤消失。這個版本的程序是可編譯的

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
    }複製代碼

按照慣例,導入錯誤的全局聲明應該在導入後當即發佈並進行註釋,這樣既便於查找,又能夠提醒之後清理

導入的反作用 上一個例子中未使用的導入(如fmt或io)最終應該被使用或刪除:空白賦值將代碼標識爲正在進行的工做。但有時導入一個包僅僅是爲了它的反作用,而沒有任何顯式的使用是有用的。例如,在其init函數期間,爲http handler提供調試信息而註冊net/http/pprof包。它有一個導出的api,可是大多數客戶機只須要註冊handler並經過web頁面訪問數據。若導入包僅爲獲取其反作用,請將包重命名爲空白標識符

import _ "net/http/pprof"  // 這種形式的導入清楚地代表,導入包是由於它的反作用,由於包沒有其餘可能的用途:在這個文件中,它沒有名稱
複製代碼

接口檢查 正如咱們在上面的接口討論中看到的,類型不須要顯式聲明它實現了接口。相反,類型只是經過實現接口的方法來實現接口。實際上,大多數接口轉換都是靜態的,所以在編譯時進行檢查。例如,將os.File傳遞給須要io的函數。除非os.File實現io.Reader接口,不然不會被編譯

一些接口檢查發生在運行時,例如,在encoding/json包中,它定義了一個Marshall接口。當JSON編碼器接收到實現該接口的值時,編碼器調用該值的處理方法將其轉換爲JSON,而不是執行標準轉換。編碼器在運行時使用以下斷言類型檢查此屬性

m, ok := val.(json.Marshaler)複製代碼

若是隻須要詢問類型是否實現了接口,而不實際使用接口自己(多是錯誤檢查的一部分),則使用空標識符忽略類型斷言的值

if _, ok := val.(json.Marshaler); ok {
            fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
    }複製代碼

併發

共享通訊 併發編程是一個很大的主題,這裏只涉及特定於Golang的亮點。許多環境中的併發編程因爲實現對共享變量的正確訪問所需的微妙之處而變得困難。go鼓勵採用一種不一樣的方法,在channel上傳遞共享值,事實上,歷來沒有被單獨的執行線程主動共享過。在任何給定時間,只有一個goroutine能夠訪問該值。按照設計,不會發生數據競爭。爲了鼓勵這種思惟方式,咱們把它簡化爲一個口號

*不要經過共享內存進行通訊;而是經過通訊共享內存*

思考這個模型的一種方法是考慮在一個CPU上運行一個典型的單線程程序,它不須要同步原語。如今運行另外一個這樣的實例;它也不須要同步。如今讓這兩個通訊;若是通訊是同步地,則仍然不須要其餘同步。例如,unix管道很是適合這個模型。儘管go的併發性方法起源於Hoare's的通訊順序進程(Communication Sequential Processes,CSP),但它也能夠看做是Unix管道的一種類型安全的泛化

Gorutines 它們之因此被稱爲Goroutines,是由於現有的術語(線程、協程、進程等)傳遞了不許確的含義。Goroutine有一個簡單的模型:它是一個與同一地址空間中的其餘Goroutine同時執行的函數。它是輕量級的,只需分配堆棧空間。並且堆棧開始很小,因此很便利,而且經過根據須要分配(和釋放)堆存儲來增加

goroutine被多路複用到多個OS線程上,所以若是其中一個線程被阻塞,例如在等待i/o時,其餘線程將繼續運行,它們的設計隱藏了線程建立和管理的許多複雜性。在函數或方法調用前面加上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.
    }複製代碼

Channels 與映射相似,channel也使用make進行分配,結果值做爲對底層數據結構的引用。若是提供了可選的整數參數,表示設置通道的緩衝區大小;對於未設置緩存區大小或同步通道,默認值爲零

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)處於Ready狀態

使用channel有不少很好的習慣用法。在上一節中,咱們在後臺啓動了排序,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.複製代碼

Receiver老是處於阻塞狀態直到有數據要接收。若是不是緩衝channel,則發送方阻塞,直到接收器接收到該值。若是是帶緩衝的channel,則發送方僅在值複製到緩衝區以前阻塞;若是緩衝區已滿,則意味着要等待某個接收器檢索到值

帶緩衝的channels能夠像信號量同樣使用,例如限制吞吐量。在本例中,傳入的請求被傳遞到handle,handle將值發送到channel,處理請求,而後從channel接收值,以便爲下一個使用者準備信號量。channel緩衝區的容量限制了要處理的同時調用的數量

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.
            }
    }複製代碼

一旦handlers正在執行進程達到MaxOutstanding,將阻止嘗試發送到channel的緩衝區,直到現有處理程序完成退出而後從緩衝區接收下一個信號。不過,這種設計有一個問題:Server爲每一個傳入的請求建立一個新的goroutine,即便MaxUnstanding能夠在任什麼時候候運行。若是請求太快,程序能夠消耗無限的資源。咱們能夠經過將Server更改成goroutines的建立gate來解決這個缺陷。這是一個顯而易見的解決方案,但請注意它有一個咱們稍後將修復的錯誤

func Serve(queue chan *Request) {
            for req := range queue {
                    sem <- 1
                    go func() {
                            process(req) // Buggy; see explanation below.
                            <-sem
                    }()
            }
    }
複製代碼

bug在於,在go的for循環中,循環變量在每次迭代中都被重用,所以req變量在全部goroutine中共享...這不是咱們想要的。咱們須要確保每一個goroutine的req是惟一的。有一種方法能夠作到這一點,將req的值做爲參數傳遞給goroutine中的閉包

func Serve(queue chan *Request) {
            for req := range queue {
                    sem <- 1
                    go func(req *Request) {
                            process(req)
                            <-sem
                    }(req)
            }
    }複製代碼

與前一版本進行比較,以查看如何聲明和運行閉包;另外一個解決方案是建立一個同名的新變量,如本例所示

func Serve(queue chan *Request) {
            for req := range queue {
                    req := req // Create new instance of req for the goroutine.
                    sem <- 1
                    go func() {
                            process(req)
                            <-sem
                    }()
            }
    }複製代碼
// req := req 看起來很奇怪,這是合法的也是處理這種問題的慣用方法:得到同名變量的新版本,故意在本地隱藏循環變量,但對每一個goroutine都是惟一的複製代碼

回到編寫服務器的通常問題,管理好資源的另外一種方法是啓動固定數量的handle goroutines,從請求channels讀取全部內容。goroutines的數量限制了同時調用進程的次數。這個服務函數還接受一個channel,在這個channel上,它將被告知退出;在啓動goroutines以後,它將阻止從該channel接收

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.
    }複製代碼

Channels of channels go最重要的特性之一是channel是first-class值,能夠像其餘任何channel同樣分配和傳遞。此屬性的一個常見用途是實現安全的並行解複用。在上一節的示例中,handle是請求的理想處理程序,但咱們沒有定義它正在處理的類型。若是該類型包含Response channel,則每一個客戶端均可覺得Response提供本身的路徑。下面是請求類型的示意定義

type Request struct {
            args        []int
            f           func([]int) int
            resultChan  chan int
    }複製代碼

客戶端提供一個函數及其參數,以及請求對象內部的channel,用於接受Response

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)複製代碼

在服務器端,僅需改變handler函數便可。顯然還要作不少事情才能實現它,但這段代碼是一個用於速率受限、並行、非阻塞的RPC系統的框架,並且看不到互斥鎖

func handle(queue chan *Request) {
            for req := range queue {
                    req.resultChan <- req.f(req.args)
            }
    }複製代碼

Parallelization 這些思想的另外一個應用是跨多個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
    }複製代碼

在循環中獨立地啓動這些部件,每一個核一個。它們能夠按任何順序完成,但這可有可無;咱們只需在啓動全部goroutine以後經過draining通道來計算完成信號

const numCPU = 4 // number of CPU cores複製代碼
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.
    }
複製代碼

運行時核數的設置,能夠沒必要硬編碼,經過runtime.NumCPU來設置機器中硬件實際的CPU數量

var numCPU = runtime.NumCPU()複製代碼

還有一個函數runtime.GOMAXPROCS,它報告(或設置)go程序能夠同時運行用戶指定核數。它默認爲runtime.NumCPU值,但能夠經過設置名稱相似的Shell環境變量或經過使用正數調用函數來覆蓋默認值,用零調用它只是查詢值。所以,若是咱們想知足用戶的資源請求,咱們應該寫

var numCPU = runtime.GOMAXPROCS(0)複製代碼

緩衝 併發編程的工具也可使非併發性的想法更容易表達。下面是一個從RPC包中抽象出來的示例。客戶端goroutine循環從某個源(多是網絡)接收數據。爲了不分配和釋放緩衝區,它保留一個空閒list,並使用一個緩衝通道來表示它。若是通道爲空,則分配新的緩衝區。一旦消息緩衝區準備好,它就經過serverChan發送到服務器上

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.
                    }
            }
    }複製代碼

Errors

庫例程必須常常向調用方返回某種錯誤指示。如前所述,go的多值返回使得在返回正常值的同時返回詳細的錯誤描述變得很容易。使用此功能提供詳細的錯誤信息是一種很好的方式。例如,正如咱們將看到的,os.Open不只在失敗時返回一個nil指針,它還返回一個錯誤值來描述發送什麼錯誤

錯誤有一個類型error,一個簡單的內置接口

type error interface {
            Error() string
    }複製代碼

庫編寫者能夠自由地使用更豐富的模型來實現這個接口,這樣不只能夠看到錯誤,還能夠提供一些上下文。如前所述,除了一般的*os.File返回值外,os.Open還返回一個錯誤值。若是文件成功打開,則錯誤爲nil,但出現問題時,它將返回一個os.PathError

// 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()
    }
複製代碼
相關文章
相關標籤/搜索