中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Go語(yǔ)言的 10 個(gè)實(shí)用技術(shù)

2018-07-20    來(lái)源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線(xiàn)!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

 十條有用的 Go 技術(shù)

  這里是我過(guò)去幾年中編寫(xiě)的大量 Go 代碼的經(jīng)驗(yàn)總結(jié)而來(lái)的自己的最佳實(shí)踐。我相信它們具有彈性的。這里的彈性是指:

  某個(gè)應(yīng)用需要適配一個(gè)靈活的環(huán)境。你不希望每過(guò) 3 到 4 個(gè)月就不得不將它們?nèi)恐貥?gòu)一遍。添加新的特性應(yīng)當(dāng)很容易。許多人參與開(kāi)發(fā)該應(yīng)用,它應(yīng)當(dāng)可以被理解,且維護(hù)簡(jiǎn)單。許多人使用該應(yīng)用,bug 應(yīng)該容易被發(fā)現(xiàn)并且可以快速的修復(fù)。我用了很長(zhǎng)的時(shí)間學(xué)到了這些事情。其中的一些很微小,但對(duì)于許多事情都會(huì)有影響。所有這些都僅僅是建議,具體情況具體對(duì)待,并且如果有幫助的話(huà)務(wù)必告訴我。

  1. 使用單一的 GOPATH

  多個(gè) GOPATH 的情況并不具有彈性。GOPATH 本身就是高度自我完備的(通過(guò)導(dǎo)入路徑)。有多個(gè) GOPATH 會(huì)導(dǎo)致某些副作用,例如可能使用了給定的庫(kù)的不同的版本。你可能在某個(gè)地方升級(jí)了它,但是其他地方卻沒(méi)有升級(jí)。而且,我還沒(méi)遇到過(guò)任何一個(gè)需要使用多個(gè) GOPATH 的情況。所以只使用單一的 GOPATH,這會(huì)提升你 Go 的開(kāi)發(fā)進(jìn)度。

  許多人不同意這一觀點(diǎn),接下來(lái)我會(huì)做一些澄清。像 etcd 或 camlistore 這樣的大項(xiàng)目使用了像 godep 這樣的工具,將所有依賴(lài)保存到某個(gè)目錄中。也就是說(shuō),這些項(xiàng)目自身有一個(gè)單一的 GOPATH。它們只能在這個(gè)目錄里找到對(duì)應(yīng)的版本。除非你的項(xiàng)目很大并且極為重要,否則不要為每個(gè)項(xiàng)目使用不同的 GOPATH。如果你認(rèn)為項(xiàng)目需要一個(gè)自己的 GOPATH 目錄,那么就創(chuàng)建它,否則不要嘗試使用多個(gè) GOPATH。它只會(huì)拖慢你的進(jìn)度。

  2. 將 for-select 封裝到函數(shù)中

  如果在某個(gè)條件下,你需要從 for-select 中退出,就需要使用標(biāo)簽。例如:

func main() {

L:
    for {
        select {
        case <-time.After(time.Second):
            fmt.Println("hello")
        default:
            break L
        }
    }

    fmt.Println("ending")
}

  如你所見(jiàn),需要聯(lián)合break使用標(biāo)簽。這有其用途,不過(guò)我不喜歡。這個(gè)例子中的 for 循環(huán)看起來(lái)很小,但是通常它們會(huì)更大,而判斷break的條件也更為冗長(zhǎng)。

  如果需要退出循環(huán),我會(huì)將 for-select 封裝到函數(shù)中:

func main() {
    foo()
    fmt.Println("ending")
}

func foo() {
    for {
        select {
        case <-time.After(time.Second):
            fmt.Println("hello")
        default:
            return
        }
    }
}

  你還可以返回一個(gè)錯(cuò)誤(或任何其他值),也是同樣漂亮的,只需要:

// 阻塞
if err := foo(); err != nil {
    // 處理 err
}

  3. 在初始化結(jié)構(gòu)體時(shí)使用帶有標(biāo)簽的語(yǔ)法

  這是一個(gè)無(wú)標(biāo)簽語(yǔ)法的例子:

type T struct {
    Foo string
    Bar int
}

func main() {
    t := T{"example", 123} // 無(wú)標(biāo)簽語(yǔ)法
    fmt.Printf("t %+v\n", t)
}

 那么如果你添加一個(gè)新的字段到T結(jié)構(gòu)體,代碼會(huì)編譯失。

type T struct {
    Foo string
    Bar int
    Qux string
}

func main() {
    t := T{"example", 123} // 無(wú)法編譯
    fmt.Printf("t %+v\n", t)
}

  如果使用了標(biāo)簽語(yǔ)法,Go 的兼容性規(guī)則(http://golang.org/doc/go1compat)會(huì)處理代碼。例如在向net包的類(lèi)型添加叫做Zone的字段,參見(jiàn):http://golang.org/doc/go1.1#library;氐轿覀兊睦,使用標(biāo)簽語(yǔ)法:

type T struct {
    Foo string
    Bar int
    Qux string
}

func main() {
    t := T{Foo: "example", Qux: 123}
    fmt.Printf("t %+v\n", t)
}

  這個(gè)編譯起來(lái)沒(méi)問(wèn)題,而且彈性也好。不論你如何添加其他字段到T結(jié)構(gòu)體。你的代碼總是能編譯,并且在以后的 Go 的版本也可以保證這一點(diǎn)。只要在代碼集中執(zhí)行g(shù)o vet,就可以發(fā)現(xiàn)所有的無(wú)標(biāo)簽的語(yǔ)法。

  4. 將結(jié)構(gòu)體的初始化拆分到多行

  如果有兩個(gè)以上的字段,那么就用多行。它會(huì)讓你的代碼更加容易閱讀,也就是說(shuō)不要:

T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}

  而是:

T{
    Foo: "example",
    Bar: someLongVariable,
    Qux: anotherLongVariable,
    B: forgetToAddThisToo,
}

  這有許多好處,首先它容易閱讀,其次它使得允許或屏蔽字段初始化變得容易(只要注釋或刪除它們),最后添加其他字段也更容易(只要添加一行)。

  5. 為整數(shù)常量添加 String() 方法

  如果你利用 iota 來(lái)使用自定義的整數(shù)枚舉類(lèi)型,務(wù)必要為其添加 String() 方法。例如,像這樣:

type State int

const (
    Running State = iota 
    Stopped
    Rebooting
    Terminated
)

  如果你創(chuàng)建了這個(gè)類(lèi)型的一個(gè)變量,然后輸出,會(huì)得到一個(gè)整數(shù)(http://play.golang.org/p/V5VVFB05HB):

func main() {
    state := Running

    // print: "state 0"
    fmt.Println("state ", state)
}

  除非你回顧常量定義,否則這里的0看起來(lái)毫無(wú)意義。只需要為State類(lèi)型添加String()方法就可以修復(fù)這個(gè)問(wèn)題(http://play.golang.org/p/ewMKl6K302):

func (s State) String() string {
    switch s {
    case Running:
        return "Running"
    case Stopped:
        return "Stopped"
    case Rebooting:
        return "Rebooting"
    case Terminated:
        return "Terminated"
    default:
        return "Unknown"
    }
}

  新的輸出是:state: Running。顯然現(xiàn)在看起來(lái)可讀性好了很多。在你調(diào)試程序的時(shí)候,這會(huì)帶來(lái)更多的便利。同時(shí)還可以在實(shí)現(xiàn) MarshalJSON()、UnmarshalJSON() 這類(lèi)方法的時(shí)候使用同樣的手段。

  6. 讓 iota 從 a +1 開(kāi)始增量

  在前面的例子中同時(shí)也產(chǎn)生了一個(gè)我已經(jīng)遇到過(guò)許多次的 bug。假設(shè)你有一個(gè)新的結(jié)構(gòu)體,有一個(gè)State字段:

type T struct {
    Name  string
    Port  int
    State State
}

  現(xiàn)在如果基于 T 創(chuàng)建一個(gè)新的變量,然后輸出,你會(huì)得到奇怪的結(jié)果(http://play.golang.org/p/LPG2RF3y39):

func main() {
    t := T{Name: "example", Port: 6666}

    // prints: "t {Name:example Port:6666 State:Running}"
    fmt.Printf("t %+v\n", t)
}

  看到 bug 了嗎?State字段沒(méi)有初始化,Go 默認(rèn)使用對(duì)應(yīng)類(lèi)型的零值進(jìn)行填充。由于State是一個(gè)整數(shù),零值也就是0,但在我們的例子中它表示Running。

  那么如何知道 State 被初始化了?還是它真得是在Running模式?沒(méi)有辦法區(qū)分它們,那么這就會(huì)產(chǎn)生未知的、不可預(yù)測(cè)的 bug。不過(guò),修復(fù)這個(gè)很容易,只要讓 iota 從 +1 開(kāi)始(http://play.golang.org/p/VyAq-3OItv):

const (
    Running State = iota + 1
    Stopped
    Rebooting
    Terminated
)

  現(xiàn)在t變量將默認(rèn)輸出Unknown,不是嗎?

func main() {
    t := T{Name: "example", Port: 6666}

    // 輸出: "t {Name:example Port:6666 State:Unknown}"
    fmt.Printf("t %+v\n", t)
}

  不過(guò)讓 iota 從零值開(kāi)始也是一種解決辦法。例如,你可以引入一個(gè)新的狀態(tài)叫做Unknown,將其修改為:

const (
    Unknown State = iota 
    Running
    Stopped
    Rebooting
    Terminated
)

  7. 返回函數(shù)調(diào)用

  我已經(jīng)看過(guò)很多代碼例如(http://play.golang.org/p/8Rz1EJwFTZ):

func bar() (string, error) {
    v, err := foo()
    if err != nil {
        return "", err
    }

    return v, nil
}

  然而,你只需要:

func bar() (string, error) {
    return foo()
}

  更簡(jiǎn)單也更容易閱讀(當(dāng)然,除非你要對(duì)某些內(nèi)部的值做一些記錄)。

  8. 把 slice、map 等定義為自定義類(lèi)型

  將 slice 或 map 定義成自定義類(lèi)型可以讓代碼維護(hù)起來(lái)更加容易。假設(shè)有一個(gè)Server類(lèi)型和一個(gè)返回服務(wù)器列表的函數(shù):

type Server struct {
    Name string
}

func ListServers() []Server {
    return []Server{
        {Name: "Server1"},
        {Name: "Server2"},
        {Name: "Foo1"},
        {Name: "Foo2"},
    }
}

  現(xiàn)在假設(shè)需要獲取某些特定名字的服務(wù)器。需要對(duì) ListServers() 做一些改動(dòng),增加篩選條件:

// ListServers 返回服務(wù)器列表。只會(huì)返回包含 name 的服務(wù)器。空的 name 將會(huì)返回所有服務(wù)器。
func ListServers(name string) []Server {
    servers := []Server{
        {Name: "Server1"},
        {Name: "Server2"},
        {Name: "Foo1"},
        {Name: "Foo2"},
    }

    // 返回所有服務(wù)器
    if name == "" {
        return servers
    }

    // 返回過(guò)濾后的結(jié)果
    filtered := make([]Server, 0)

    for _, server := range servers {
        if strings.Contains(server.Name, name) {
            filtered = append(filtered, server)
        }
    }

    return filtered
}

  現(xiàn)在可以用這個(gè)來(lái)篩選有字符串Foo的服務(wù)器:

func main() {
    servers := ListServers("Foo")

    // 輸出:“servers [{Name:Foo1} {Name:Foo2}]”
    fmt.Printf("servers %+v\n", servers)
}

  顯然這個(gè)函數(shù)能夠正常工作。不過(guò)它的彈性并不好。如果你想對(duì)服務(wù)器集合引入其他邏輯的話(huà)會(huì)如何呢?例如檢查所有服務(wù)器的狀態(tài),為每個(gè)服務(wù)器創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)記錄,用其他字段進(jìn)行篩選等等……

  現(xiàn)在引入一個(gè)叫做Servers的新類(lèi)型,并且修改原始版本的 ListServers() 返回這個(gè)新類(lèi)型:

type Servers []Server

// ListServers 返回服務(wù)器列表
func ListServers() Servers {
    return []Server{
        {Name: "Server1"},
        {Name: "Server2"},
        {Name: "Foo1"},
        {Name: "Foo2"},
    }
}

  現(xiàn)在需要做的是只要為Servers類(lèi)型添加一個(gè)新的Filter()方法:

// Filter 返回包含 name 的服務(wù)器?盏 name 將會(huì)返回所有服務(wù)器。
func (s Servers) Filter(name string) Servers {
    filtered := make(Servers, 0)

    for _, server := range s {
        if strings.Contains(server.Name, name) {
            filtered = append(filtered, server)
        }

    }

    return filtered
}

  現(xiàn)在可以針對(duì)字符串Foo篩選服務(wù)器:

func main() {
    servers := ListServers()
    servers = servers.Filter("Foo")
    fmt.Printf("servers %+v\n", servers)
}

  哈!看到你的代碼是多么的簡(jiǎn)單了嗎?還想對(duì)服務(wù)器的狀態(tài)進(jìn)行檢查?或者為每個(gè)服務(wù)器添加一條數(shù)據(jù)庫(kù)記錄?沒(méi)問(wèn)題,添加以下新方法即可:

func (s Servers) Check() 
func (s Servers) AddRecord() 
func (s Servers) Len()
...

  9. withContext 封裝函數(shù)

  有時(shí)對(duì)于函數(shù)會(huì)有一些重復(fù)勞動(dòng),例如鎖/解鎖,初始化一個(gè)新的局部上下文,準(zhǔn)備初始化變量等等……這里有一個(gè)例子:

func foo() {
    mu.Lock()
    defer mu.Unlock()

    // foo 相關(guān)的工作
}

func bar() {
    mu.Lock()
    defer mu.Unlock()

    // bar 相關(guān)的工作
}

func qux() {
    mu.Lock()
    defer mu.Unlock()

    // qux 相關(guān)的工作
}

  如果你想要修改某個(gè)內(nèi)容,你需要對(duì)所有的都進(jìn)行修改。如果它是一個(gè)常見(jiàn)的任務(wù),那么最好創(chuàng)建一個(gè)叫做withContext的函數(shù)。這個(gè)函數(shù)的輸入?yún)?shù)是另一個(gè)函數(shù),并用調(diào)用者提供的上下文來(lái)調(diào)用它:

func withLockContext(fn func()) {
    mu.Lock
    defer mu.Unlock()

    fn()
}

  只需要將之前的函數(shù)用這個(gè)進(jìn)行封裝:

func foo() {
    withLockContext(func() {
        // foo 相關(guān)工作
    })
}

func bar() {
    withLockContext(func() {
        // bar 相關(guān)工作
    })
}

func qux() {
    withLockContext(func() {
        // qux 相關(guān)工作
    })
}

  不要光想著加鎖的情形。對(duì)此來(lái)說(shuō)最好的用例是數(shù)據(jù)庫(kù)鏈接,F(xiàn)在對(duì) withContext 函數(shù)作一些小小的改動(dòng):

func withDBContext(fn func(db DB) error) error {
    // 從連接池獲取一個(gè)數(shù)據(jù)庫(kù)連接
    dbConn := NewDB()

    return fn(dbConn)
}

  如你所見(jiàn),它獲取一個(gè)連接,然后傳遞給提供的參數(shù),并且在調(diào)用函數(shù)的時(shí)候返回錯(cuò)誤。你需要做的只是:

func foo() {
    withDBContext(func(db *DB) error {
        // foo 相關(guān)工作
    })
}

func bar() {
    withDBContext(func(db *DB) error {
        // bar 相關(guān)工作
    })
}

func qux() {
    withDBContext(func(db *DB) error {
        // qux 相關(guān)工作
    })
}

  你在考慮一個(gè)不同的場(chǎng)景,例如作一些預(yù)初始化?沒(méi)問(wèn)題,只需要將它們加到withDBContext就可以了。這對(duì)于測(cè)試也同樣有效。

  這個(gè)方法有個(gè)缺陷,它增加了縮進(jìn)并且更難閱讀。再次提示,永遠(yuǎn)尋找最簡(jiǎn)單的解決方案。

  10. 為訪(fǎng)問(wèn) map 增加 setter,getters

  如果你重度使用 map 讀寫(xiě)數(shù)據(jù),那么就為其添加 getter 和 setter 吧。通過(guò) getter 和 setter 你可以將邏輯封分別裝到函數(shù)里。這里最常見(jiàn)的錯(cuò)誤就是并發(fā)訪(fǎng)問(wèn)。如果你在某個(gè) goroutein 里有這樣的代碼:

m["foo"] = bar

  還有這個(gè):

delete(m, "foo")

  會(huì)發(fā)生什么?你們中的大多數(shù)應(yīng)當(dāng)已經(jīng)非常熟悉這樣的競(jìng)態(tài)了。簡(jiǎn)單來(lái)說(shuō)這個(gè)競(jìng)態(tài)是由于 map 默認(rèn)并非線(xiàn)程安全。不過(guò)你可以用互斥量來(lái)保護(hù)它們:

mu.Lock()
m["foo"] = "bar"
mu.Unlock()

  以及:

mu.Lock()
delete(m, "foo")
mu.Unlock()

  假設(shè)你在其他地方也使用這個(gè) map。你必須把互斥量放得到處都是!然而通過(guò) getter 和 setter 函數(shù)就可以很容易的避免這個(gè)問(wèn)題:

func Put(key, value string) {
    mu.Lock()
    m[key] = value
    mu.Unlock()
}
func Delete(key string) {
    mu.Lock()
    delete(m, key)
    mu.Unlock()
}

  使用接口可以對(duì)這一過(guò)程做進(jìn)一步的改進(jìn)。你可以將實(shí)現(xiàn)完全隱藏起來(lái)。只使用一個(gè)簡(jiǎn)單的、設(shè)計(jì)良好的接口,然后讓包的用戶(hù)使用它們:

type Storage interface {
    Delete(key string)
    Get(key string) string
    Put(key, value string)
}

  這只是個(gè)例子,不過(guò)你應(yīng)該能體會(huì)到。對(duì)于底層的實(shí)現(xiàn)使用什么都沒(méi)關(guān)系。不光是使用接口本身很簡(jiǎn)單,而且還解決了暴露內(nèi)部數(shù)據(jù)結(jié)構(gòu)帶來(lái)的大量的問(wèn)題。

  但是得承認(rèn),有時(shí)只是為了同時(shí)對(duì)若干個(gè)變量加鎖就使用接口會(huì)有些過(guò)分。理解你的程序,并且在你需要的時(shí)候使用這些改進(jìn)。

  總結(jié)

  抽象永遠(yuǎn)都不是容易的事情。有時(shí),最簡(jiǎn)單的就是你已經(jīng)實(shí)現(xiàn)的方法。要知道,不要讓你的代碼看起來(lái)很聰明。Go 天生就是個(gè)簡(jiǎn)單的語(yǔ)言,在大多數(shù)情況下只會(huì)有一種方法來(lái)作某事。簡(jiǎn)單是力量的源泉,也是為什么在人的層面它表現(xiàn)的如此有彈性。

  如果必要的話(huà),使用這些基數(shù)。例如將[]Server轉(zhuǎn)化為Servers是另一種抽象,僅在你有一個(gè)合理的理由的情況下這么做。不過(guò)有一些技術(shù),如 iota 從 1 開(kāi)始計(jì)數(shù)總是有用的。再次提醒,永遠(yuǎn)保持簡(jiǎn)單。

  特別感謝 Cihangir Savas、Andrew Gerrand、Ben Johnson 和 Damian Gryski 提供的極具價(jià)值的反饋和建議。

  原文: Fatih Arslan.

標(biāo)簽: 安全 代碼 服務(wù)器 數(shù)據(jù)庫(kù)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:大神說(shuō)Scala是個(gè)有趣的語(yǔ)言 你值得擁有

下一篇:小眾編程語(yǔ)言同樣值得你關(guān)注