在线亚洲AV日韩AV综合AV,国产订精品电影,久久国产精品蜜

      <thead id="u9ncv"><del id="u9ncv"><rp id="u9ncv"></rp></del></thead>

        <thead id="u9ncv"></thead>

        <thead id="u9ncv"><s id="u9ncv"></s></thead>

          <strike id="u9ncv"></strike>
          <table id="u9ncv"><form id="u9ncv"></form></table>
        1. <strike id="u9ncv"></strike>
        2. 使用幫助 | 聯系電話:400-880-0256 0769-23037585 21686281
          字體大小:返回
          您當前的位置:首頁 > 公告中心 > IT科技 > Go 語言的 10 個實用技術

          Go 語言的 10 個實用技術

          作者:admin 發表于:2014-08-26 點擊:1630  保護視力色:

          十條有用的 Go 技術

          這里是我過去幾年中編寫的大量 Go 代碼的經驗總結而來的自己的最佳實踐。我相信它們具有彈性的。這里的彈性是指:

          某個應用需要適配一個靈活的環境。你不希望每過 3 到 4 個月就不得不將它們全部重構一遍。添加新的特性應當很容易。許多人參與開發該應用,它應當可以被理解,且維護簡單。許多人使用該應用,bug 應該容易被發現并且可以快速的修復。我用了很長的時間學到了這些事情。其中的一些很微小,但對于許多事情都會有影響。

          所有這些都僅僅是建議,具體情況具體對待,并且如果有幫助的話務必告訴我。隨時留言:)

           

          1. 使用單一的 GOPATH

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

          許多人不同意這一觀點,接下來我會做一些澄清。像?etcd?或camlistore?這樣的大項目使用了像?godep?這樣的工具,將所有依賴保存到某個目錄中。也就是說,這些項目自身有一個單一的 GOPATH。它們只能在這個目錄里找到對應的版本。除非你的項目很大并且極為重要,否則不要為每個項目使用不同的 GOPATH。如果你認為項目需要一個自己的 GOPATH 目錄,那么就創建它,否則不要嘗試使用多個 GOPATH。它只會拖慢你的進度。

          2. 將 for-select 封裝到函數中

          如果在某個條件下,你需要從 for-select 中退出,就需要使用標簽。例如:

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

          如你所見,需要聯合break使用標簽。這有其用途,不過我不喜歡。這個例子中的 for 循環看起來很小,但是通常它們會更大,而判斷break的條件也更為冗長。

          如果需要退出循環,我會將 for-select 封裝到函數中:

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

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

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

          3. 在初始化結構體時使用帶有標簽的語法

          這是一個無標簽語法的例子:

          type T struct {
              Foo string
              Bar int
          }
          
          func main() {
              t := T{"example", 123} // 無標簽語法
              fmt.Printf("t %+v\n", t)
          }

          那么如果你添加一個新的字段到T結構體,代碼會編譯失?。?/p>

          type T struct {
              Foo string
              Bar int
              Qux string
          }
          
          func main() {
              t := T{"example", 123} // 無法編譯
              fmt.Printf("t %+v\n", t)
          }

          如果使用了標簽語法,Go 的兼容性規則(http://golang.org/doc/go1compat)會處理代碼。例如在向net包的類型添加叫做Zone的字段,參見:http://golang.org/doc/go1.1#library?;氐轿覀兊睦?,使用標簽語法:

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

          這個編譯起來沒問題,而且彈性也好。不論你如何添加其他字段到T結構體。你的代碼總是能編譯,并且在以后的 Go 的版本也可以保證這一點。只要在代碼集中執行go vet,就可以發現所有的無標簽的語法。

          4. 將結構體的初始化拆分到多行

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

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

          而是:

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

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

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

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

          type State int
          
          const (
              Running State = iota
              Stopped
              Rebooting
              Terminated
          )

          如果你創建了這個類型的一個變量,然后輸出,會得到一個整數(http://play.golang.org/p/V5VVFB05HB):

          func main() {
              state := Running
          
              // print: "state 0"
              fmt.Println("state ", state)
          }

          除非你回顧常量定義,否則這里的0看起來毫無意義。只需要為State類型添加String()方法就可以修復這個問題(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。顯然現在看起來可讀性好了很多。在你調試程序的時候,這會帶來更多的便利。同時還可以在實現 MarshalJSON()、UnmarshalJSON() 這類方法的時候使用同樣的手段。

          6. 讓 iota 從 a +1 開始增量

          在前面的例子中同時也產生了一個我已經遇到過許多次的 bug。假設你有一個新的結構體,有一個State字段:

          type T struct {
              Name  string
              Port  int
              State State
          }

          現在如果基于 T 創建一個新的變量,然后輸出,你會得到奇怪的結果(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字段沒有初始化,Go 默認使用對應類型的零值進行填充。由于State是一個整數,零值也就是0,但在我們的例子中它表示Running。

          那么如何知道 State 被初始化了?還是它真得是在Running模式?沒有辦法區分它們,那么這就會產生未知的、不可預測的 bug。不過,修復這個很容易,只要讓 iota 從 +1 開始(http://play.golang.org/p/VyAq-3OItv):

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

          現在t變量將默認輸出Unknown,不是嗎??:)?:

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

          不過讓 iota 從零值開始也是一種解決辦法。例如,你可以引入一個新的狀態叫做Unknown,將其修改為:

          const (
              Unknown State = iota
              Running
              Stopped
              Rebooting
              Terminated
          )

          7. 返回函數調用

          我已經看過很多代碼例如(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()
          }

          更簡單也更容易閱讀(當然,除非你要對某些內部的值做一些記錄)。

          8. 把 slice、map 等定義為自定義類型

          將 slice 或 map 定義成自定義類型可以讓代碼維護起來更加容易。假設有一個Server類型和一個返回服務器列表的函數:

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

          現在假設需要獲取某些特定名字的服務器。需要對 ListServers() 做一些改動,增加篩選條件:

          // ListServers 返回服務器列表。只會返回包含 name 的服務器??盏?name 將會返回所有服務器。
          func ListServers(name string) []Server {
              servers := []Server{
                  {Name: "Server1"},
                  {Name: "Server2"},
                  {Name: "Foo1"},
                  {Name: "Foo2"},
              }
          
              // 返回所有服務器
              if name == "" {
                  return servers
              }
          
              // 返回過濾后的結果
              filtered := make([]Server, 0)
          
              for _, server := range servers {
                  if strings.Contains(server.Name, name) {
                      filtered = append(filtered, server)
                  }
              }
          
              return filtered
          }

          現在可以用這個來篩選有字符串Foo的服務器:

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

          顯然這個函數能夠正常工作。不過它的彈性并不好。如果你想對服務器集合引入其他邏輯的話會如何呢?例如檢查所有服務器的狀態,為每個服務器創建一個數據庫記錄,用其他字段進行篩選等等……

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

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

          現在需要做的是只要為Servers類型添加一個新的Filter()方法:

          // Filter 返回包含 name 的服務器??盏?name 將會返回所有服務器。
          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
          }

          現在可以針對字符串Foo篩選服務器:

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

          哈!看到你的代碼是多么的簡單了嗎?還想對服務器的狀態進行檢查?或者為每個服務器添加一條數據庫記錄?沒問題,添加以下新方法即可:

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

          9. withContext 封裝函數

          有時對于函數會有一些重復勞動,例如鎖/解鎖,初始化一個新的局部上下文,準備初始化變量等等……這里有一個例子:

          func foo() {
              mu.Lock()
              defer mu.Unlock()
          
              // foo 相關的工作
          }
          
          func bar() {
              mu.Lock()
              defer mu.Unlock()
          
              // bar 相關的工作
          }
          
          func qux() {
              mu.Lock()
              defer mu.Unlock()
          
              // qux 相關的工作
          }

          如果你想要修改某個內容,你需要對所有的都進行修改。如果它是一個常見的任務,那么最好創建一個叫做withContext的函數。這個函數的輸入參數是另一個函數,并用調用者提供的上下文來調用它:

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

          只需要將之前的函數用這個進行封裝:

          func foo() {
              withLockContext(func() {
                  // foo 相關工作
              })
          }
          
          func bar() {
              withLockContext(func() {
                  // bar 相關工作
              })
          }
          
          func qux() {
              withLockContext(func() {
                  // qux 相關工作
              })
          }

          不要光想著加鎖的情形。對此來說最好的用例是數據庫鏈接?,F在對 withContext 函數作一些小小的改動:

          func withDBContext(fn func(db DB) error) error {
              // 從連接池獲取一個數據庫連接
              dbConn := NewDB()
          
              return fn(dbConn)
          }

          如你所見,它獲取一個連接,然后傳遞給提供的參數,并且在調用函數的時候返回錯誤。你需要做的只是:

          func foo() {
              withDBContext(func(db *DB) error {
                  // foo 相關工作
              })
          }
          
          func bar() {
              withDBContext(func(db *DB) error {
                  // bar 相關工作
              })
          }
          
          func qux() {
              withDBContext(func(db *DB) error {
                  // qux 相關工作
              })
          }

          你在考慮一個不同的場景,例如作一些預初始化?沒問題,只需要將它們加到withDBContext就可以了。這對于測試也同樣有效。

          這個方法有個缺陷,它增加了縮進并且更難閱讀。再次提示,永遠尋找最簡單的解決方案。

          10. 為訪問 map 增加 setter,getters

          如果你重度使用 map 讀寫數據,那么就為其添加 getter 和 setter 吧。通過 getter 和 setter 你可以將邏輯封分別裝到函數里。這里最常見的錯誤就是并發訪問。如果你在某個 goroutein 里有這樣的代碼:

          m["foo"] = bar

          還有這個:

          delete(m, "foo")

          會發生什么?你們中的大多數應當已經非常熟悉這樣的競態了。簡單來說這個競態是由于 map 默認并非線程安全。不過你可以用互斥量來保護它們:

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

          以及:

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

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

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

          使用接口可以對這一過程做進一步的改進。你可以將實現完全隱藏起來。只使用一個簡單的、設計良好的接口,然后讓包的用戶使用它們:

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

          這只是個例子,不過你應該能體會到。對于底層的實現使用什么都沒關系。不光是使用接口本身很簡單,而且還解決了暴露內部數據結構帶來的大量的問題。

          但是得承認,有時只是為了同時對若干個變量加鎖就使用接口會有些過分。理解你的程序,并且在你需要的時候使用這些改進。

          總結

          抽象永遠都不是容易的事情。有時,最簡單的就是你已經實現的方法。要知道,不要讓你的代碼看起來很聰明。Go 天生就是個簡單的語言,在大多數情況下只會有一種方法來作某事。簡單是力量的源泉,也是為什么在人的層面它表現的如此有彈性。

          如果必要的話,使用這些基數。例如將[]Server轉化為Servers是另一種抽象,僅在你有一個合理的理由的情況下這么做。不過有一些技術,如 iota 從 1 開始計數總是有用的。再次提醒,永遠保持簡單。

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

           

          Go 語言的 10 個實用技術,首發于博客 - 伯樂在線。

          在线亚洲AV日韩AV综合AV,国产订精品电影,久久国产精品蜜