整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Go語言編碼規(guī)范

          Go語言編碼規(guī)范


          規(guī)范旨在為日常Go項目開發(fā)提供一個代碼的規(guī)范指導,方便團隊形成一個統(tǒng)一的代碼風格,提高代碼的可讀性,規(guī)范性和統(tǒng)一性。本規(guī)范將從命名規(guī)范,注釋規(guī)范,代碼風格和 Go 語言提供的常用的工具這幾個方面做一個說明。該規(guī)范參考了 go 語言官方代碼的風格制定。

          命名規(guī)范

          命名是代碼規(guī)范中很重要的一部分,統(tǒng)一的命名規(guī)則有利于提高的代碼的可讀性,好的命名僅僅通過命名就可以獲取到足夠多的信息。

          Go在命名時以字母a到Z或a到Z或下劃線開頭,后面跟著零或更多的字母、下劃線和數字(0到9)。Go不允許在命名時中使用@、$和%等標點符號。Go是一種區(qū)分大小寫的編程語言。因此,Manpower和manpower是兩個不同的命名。

          當命名(包括常量、變量、類型、函數名、結構字段等等)以一個大寫字母開頭,如:Group1,那么使用這種形式的標識符的對象就可以被外部包的代碼所使用(客戶端程序需要先導入這個包),這被稱為導出(像面向對象語言中的 public)。 命名如果以小寫字母開頭,則對包外是不可見的,但是他們在整個包的內部是可見并且可用的(像面向對象語言中的 private )。

          包命名

          保持package的名字和目錄保持一致,盡量采取有意義的包名,簡短,有意義,盡量和標準庫不要沖突。包名應該為小寫單詞,不要使用下劃線或者混合大小寫。

          package demo
          
          package main
          

          文件命名

          盡量采取有意義的文件名,簡短,有意義,應該為小寫單詞,使用下劃線分隔各個單詞。

          my_test.go

          結構體命名

          采用駝峰命名法,首字母根據訪問控制大寫或者小寫。

          struct 申明和初始化格式采用多行,例如下面:

          // 多行申明
          type User struct{
           Username string
           Email string
          }
          ?
          // 多行初始化
          u :=User{
           Username: "avatar",
           Email: "avatar@gmail.com",
          }
          

          接口命名

          命名規(guī)則基本和上面的結構體類型。

          單個函數的結構名以 “er” 作為后綴,例如 Reader , Writer 。

          type Reader interface {
           Read(p []byte) (n int, err error)
          }
          

          變量命名

          • 和結構體類似,變量名稱一般遵循駝峰法,首字母根據訪問控制原則大寫或者小寫,但遇到特有名詞時,需要遵循以下規(guī)則:
          • 如果變量為私有,且特有名詞為首個單詞,則使用小寫,如 apiClient
          • 其它情況都應當使用該名詞原有的寫法,如 APIClient、repoID、UserID
          • 錯誤示例:UrlArray,應該寫成 urlArray 或者 URLArray
          • 若變量類型為 bool 類型,則名稱應以 Has, Is, Can 或 Allow 開頭
          var isExist bool
          var hasConflict bool
          var canManage bool
          var allowGitHook bool
          

          常量命名

          常量均需使用全部大寫字母組成,并使用下劃線分詞。

          const APP_VER="1.0"
          

          如果是枚舉類型的常量,需要先創(chuàng)建相應類型:

          type Scheme string
          ?
          const (
           HTTP Scheme="http"
           HTTPS Scheme="https"
          )
          

          關鍵字

          下面的列表顯示了Go中的保留字。這些保留字不能用作常量或變量或任何其他標識符名稱。

          注釋

          Go提供C風格的/ /塊注釋和C ++風格的//行注釋。行注釋是常態(tài);塊注釋主要顯示為包注釋,但在表達式中很有用或禁用大量代碼。

          • 單行注釋是最常見的注釋形式,你可以在任何地方使用以 // 開頭的單行注釋
          • 多行注釋也叫塊注釋,均已以 / 開頭,并以 / 結尾,且不可以嵌套使用,多行注釋一般用于包的文檔描述或注釋成塊的代碼片段

          go 語言自帶的 godoc 工具可以根據注釋生成文檔,生成可以自動生成對應的網站( http://golang.org 就是使用 godoc 工具直接生成的),注釋的質量決定了生成的文檔的質量。每個包都應該有一個包注釋,在package子句之前有一個塊注釋。對于多文件包,包注釋只需要存在于一個文件中,任何一個都可以。包評論應該介紹包,并提供與整個包相關的信息。它將首先出現(xiàn)在godoc頁面上,并應設置下面的詳細文檔。

          詳細的如何寫注釋可以 參考:http://golang.org/doc/effective_go.html#commentary

          包注釋

          每個包都應該有一個包注釋,一個位于package子句之前的塊注釋或行注釋。包如果有多個go文件,只需要出現(xiàn)在一個go文件中(一般是和包同名的文件)即可。 包注釋應該包含下面基本信息(請嚴格按照這個順序,簡介,創(chuàng)建人,創(chuàng)建時間):

          • 包的基本簡介(包名,簡介)
          • 創(chuàng)建者,格式: 創(chuàng)建人: rtx 名
          • 創(chuàng)建時間,格式:創(chuàng)建時間: yyyyMMdd

          例如 util 包的注釋示例如下:

          // util 包, 該包包含了項目共用的一些常量,封裝了項目中一些共用函數。
          // 創(chuàng)建人: xlli5
          // 創(chuàng)建時間: 20190522
          

          結構(接口)注釋

          每個自定義的結構體或者接口都應該有注釋說明,該注釋對結構進行簡要介紹,放在結構體定義的前一行,格式為: 結構體名, 結構體說明。同時結構體內的每個成員變量都要有說明,該說明放在成員變量的后面(注意對齊),實例如下:

          // User , 用戶對象,定義了用戶的基礎信息
          type User struct{
           Username string // 用戶名
           Email string // 郵箱
          }
          

          函數(方法)注釋

          每個函數,或者方法(結構體或者接口下的函數稱為方法)都應該有注釋說明,函數的注釋應該包括三個方面(嚴格按照此順序撰寫):

          • 簡要說明,格式說明:以函數名開頭,“,”分隔說明部分
          • 參數列表:每行一個參數,參數名開頭,“,”分隔說明部分
          • 返回值: 每行一個返回值

          示例如下:

          // NewtAttrModel , 屬性數據層操作類的工廠方法
          // 參數:
          // ctx : 上下文信息
          // 返回值:
          // 屬性操作類指針
          func NewAttrModel(ctx *common.Context) *AttrModel {
          }
          

          代碼邏輯注釋

          對于一些關鍵位置的代碼邏輯,或者局部較為復雜的邏輯,需要有相應的邏輯說明,方便其他開發(fā)者閱讀該段代碼,實例如下:

          // 從 Redis 中批量讀取屬性,對于沒有讀取到的 id , 記錄到一個數組里面,準備從 DB 中讀取
          xxxxx
          xxxxxxx
          xxxxxxx
          

          注釋風格

          統(tǒng)一使用中文注釋,對于中英文字符之間嚴格使用空格分隔, 這個不僅僅是中文和英文之間,英文和中文標點之間也都要使用空格分隔,例如:

          // 從 Redis 中批量讀取屬性,對于沒有讀取到的 id , 記錄到一個數組里面,準備從 DB 中讀取
          

          上面 Redis 、 id 、 DB 和其他中文字符之間都是用了空格分隔。

          建議全部使用單行注釋
          和代碼的規(guī)范一樣,單行注釋不要過長,禁止超過 120 字符
          

          代碼風格

          縮進和折行

          • 縮進直接使用 gofmt 工具格式化即可(gofmt 是使用 tab 縮進的)
          • 折行方面,一行最長不超過120個字符,超過的請使用換行展示,盡量保持格式優(yōu)雅

          我們使用Goland開發(fā)工具,可以直接使用快捷鍵:ctrl+alt+L,即可。

          語句的結尾

          Go語言中是不需要類似于Java需要冒號結尾,默認一行就是一條數據

          如果你打算將多個語句寫在同一行,它們則必須使用。

          括號和空格

          括號和空格方面,也可以直接使用 gofmt 工具格式化(go 會強制左大括號不換行,換行會報語法錯誤),所有的運算符和操作數之間要留空格。

          // 正確的方式
          if a > 0 {
          ?
          }
          ?
          // 錯誤的方式
          if a>0 // a ,0 和 > 之間應該空格
          { // 左大括號不可以換行,會報語法錯誤
          ?
          }
          

          import 規(guī)范

          import在多行的情況下,goimports會自動幫你格式化,但是我們這里還是規(guī)范一下import的一些規(guī)范,如果你在一個文件里面引入了一個package,還是建議采用如下格式:

          import (
           "fmt"
          )
          

          如果你的包引入了三種類型的包,標準庫包,程序內部包,第三方包,建議采用如下方式進行組織你的包:

          import (
           "encoding/json"
           "strings"
          ?
           "myproject/models"
           "myproject/controller"
           "myproject/utils"
          ?
           "github.com/astaxie/beego"
           "github.com/go-sql-driver/mysql"
          )
          

          有順序的引入包,不同的類型采用空格分離,第一種實標準庫,第二是項目包,第三是第三方包。

          在項目中不要使用相對路徑引入包:

          // 這是不好的導入
          import "../net"
          ?
          // 這是正確的做法
          import "github.com/repo/proj/src/net"
          

          但是如果是引入本項目中的其他包,最好使用相對路徑。

          錯誤處理

          • 錯誤處理的原則就是不能丟棄任何有返回err的調用,不要使用 _ 丟棄,必須全部處理。接收到錯誤,要么返回err,或者使用log記錄下來
          • 盡早return:一旦有錯誤發(fā)生,馬上返回
          • 盡量不要使用panic,除非你知道你在做什么
          • 錯誤描述如果是英文必須為小寫,不需要標點結尾
          • 采用獨立的錯誤流進行處理
          // 錯誤寫法
          if err !=nil {
           // error handling
          } else {
           // normal code
          }
          ?
          // 正確寫法
          if err !=nil {
           // error handling
           return // or continue, etc.
          }
          // normal code
          

          測試

          單元測試文件名命名規(guī)范為 example_test.go 測試用例的函數名稱必須以 Test 開頭,盡量避免使用 main 方法測試。例如:TestExample 每個重要的函數都要首先編寫測試用例,測試用例和正規(guī)代碼一起提交方便進行回歸測試。

          配置文件

          編寫代碼時提供三套配置文件,分別是開發(fā)環(huán)境 [dev] , 測試環(huán)境 [test] , 現(xiàn)網環(huán)境 [prod] 。 目錄如下:

          ./
          ├── dev
          │ ├── seelog.xml
          │ └── service.yml
          ├── prod
          │ ├── seelog.xml
          │ └── service.yml
          └── test
           ├── seelog.xml
           └── service.yml
          

          常用工具

          代碼格式化

          go 語言本身在代碼規(guī)范性這方面也做了很多努力,很多限制都是強制語法要求,例如左大括號不換行,引用的包或者定義的變量不使用會報錯,此外 go 還是提供了很多好用的工具幫助我們進行代碼的規(guī)范,

          gofmt 大部分的格式問題可以通過gofmt解決, gofmt 自動格式化代碼,保證所有的 go 代碼與官方推薦的格式保持一致,于是所有格式有關問題,都以 gofmt 的結果為準。

          goimport 我們強烈建議使用 goimport ,該工具在 gofmt 的基礎上增加了自動刪除和引入包。

          go get golang.org/x/tools/cmd/goimports

          go vet vet工具可以幫我們靜態(tài)分析我們的源碼存在的各種問題,例如多余的代碼,提前return的邏輯,struct的tag是否符合標準等。

          go get golang.org/x/tools/cmd/vet

          使用如下:

          go vet .

          依賴包管理

          GoModule

          Golang官方在1.11版本初步引入的GoModule模塊。1.12版本正式開始支持。GoModule是官方提供的包管理解決方案。通過GoModule,開發(fā)者可以把工程放在GOPATH之外的位置。相比于之前的包管理方案: dep,vendor。GoModule的管理方案更加靈活。 可以運行go mod help來看看GoModule中有哪些命令。

          • go mod init [module]:初始化.mod 包管理文件到當前工程。
          • go mod vendor:vendor版本的解決方案,將依賴復制到vendor下面。
          • go mod tidy:移除未用的模塊,以及添加缺失的模塊。
          • go mod verify:驗證所有模塊是否正確。

          亮點:

          使用replace替換無法直接獲取的package 依賴包沖突問題 自動查找包依賴

          總結

          兩個等級: [S] 建議, [M] 必須。以下是細節(jié)。

          代碼組織結構

          • [M] 一個目錄只包含一個包,模塊復雜拆分子模塊/子目錄
          • [S] 內部項目GOPATH如果指向多個工作目錄。公開項目為第一個工作區(qū)間(即 go get 默認下載到第一個目錄)
          • [M] 非測試文件 (*_test.go) 禁止使用,簡化包
          • [M] 禁止相對路徑導入包
          • [S] 建議goimports或者IDE管理import
          • [S] 建議使用GoModule管理第三方包

          代碼風格

          • [M] 提交代碼時 gofmt 格式化代碼, golint 檢查代碼(使用IDE時默認這兩個工具會自動用到)
          • [S] json 字符串建議使用反單引號(`)
          • [M] 文件名必須小寫,允許下劃線'_’,但頭尾不能。避免與 _test.go 或者系統(tǒng)相關 _386.go 等沖突
          • [S] 文件名以功能為指引,不需要再出現(xiàn)模塊名
          • [M] 目錄名必須小寫,允許中劃線'-',但頭尾不能
          • [S] 不建議目錄名出現(xiàn)下劃線'_'
          • [M] 包名必須全部小寫,無下劃線,越短越好,盡量不要與標準庫重名,禁止通過中劃線連接多個單詞
          • [S] 包名盡量與目錄名一致
          • [M] 函數名和結構體名必須為大小寫駝峰模式,最好不帶特殊字符如劃線等
          • [S] 函數名建議動詞或者動賓結構單詞,結構體建議名詞或者動名詞
          • [S] 常量和枚舉名,大小寫駝峰法,不允許下劃線,第三方包例外。
          • [M] 函數參數首字母小寫,不能有下劃線,按大小駝峰法
          • [S] 函數參數按緊密程度安排位置,同類型參數應該相鄰
          • [S] 參數不大于5個
          • [M] 變量名不允許下劃線,大小寫駝峰法,局部變量首字母小寫,全局變量首字母大寫
          • [S] 避免全局變量多使用, for 循環(huán)可用單字母
          • [M] 接口名大小寫駝峰法,首字母大寫,不能下劃線,名詞
          • [S] 接口名 'er' 結尾
          • [M] 復雜功能請多寫注釋備注,注釋表達需清晰,不要啰嗦。注釋標準暫時不強制,最好參考 godoc ,如包注釋使用 /**/ ,首字母大寫,注釋后空一行,函數注釋寫在函數上方等。

          總而言之,文件名和目錄名,包名都必須小寫。數據類型變量和參數等定義最好使用駝峰大小寫法,不要使用下劃線或者中劃線

          單元測試/程序效率

          • [S] 建議少使用 main 方法測試,而是使用 _test.go 做測試
          • [M] 與其他語言類似,避免多級 if 或者 for 嵌用,代碼層次需簡單,繞腦層次少
          • [M] 避免有歧義的命名,如 IsTrue 變量,if(!IsTrue).
          • [M] 請熟悉 Go 語言各特征,避免低效用法。

          用了近十年的 C# 轉到 Go 是一個有趣的旅程。有時,我陶醉于 Go 的簡潔[1];也有些時候,當熟悉的 OOP (面向對象編程)模式[2]無法在 Go 代碼中使用的時候會感到沮喪。幸運的是,我已經摸索出了一些寫 HTTP 服務的模式,在我的團隊中應用地很好。

          當在公司項目上工作時,我傾向把可發(fā)現(xiàn)性放在最高的優(yōu)先級上。這些應用會在接下來的 20 年運行在生產環(huán)境中,必須有眾多的開發(fā)人員和網站可靠性工程師(可能是指運維)來進行熱補丁,維護和調整工作。因此,我不指望這些模式能適合所有人。

          Mat Ryer 的文章[3]是我使用 Go 試驗 HTTP 服務的起點之一,也是這篇文章的靈感來源。

          代碼組成

          Broker

          一個 Broker 結構是將不同的 service 包綁定到 HTTP 邏輯的膠合結構。沒有包作用域結級別的變量被使用。依賴的接口得益于了 Go 的組合[4]的特點被嵌入了進來。

          type Broker struct {
              auth.Client             // 從外部倉庫導入的身份驗證依賴(接口)
              service.Service         // 倉庫的業(yè)務邏輯包(接口)
          
              cfg    Config           // 該 API 服務的配置
              router *mux.Router      // 該 API 服務的路由集
          }
          

          broker 可以使用阻塞[5]函數 New() 來初始化,該函數校驗配置,并且運行所有需要的前置檢查。

          func New(cfg Config, port int) (*Broker, error) {
              r := &Broker{
                  cfg: cfg,
              }
          
              ...
          
              r.auth.Client, err = auth.New(cfg.AuthConfig)
              if err != nil {
                  return nil, fmt.Errorf("Unable to create new API broker: %w", err)
              }
          
              ...
          
              return r, nil
          }
          

          初始化后的 Broker 滿足了暴露在外的 Server 接口,這些接口定義了所有的,被 route 和 中間件(middleware)使用的功能。service 包接口被嵌入,這些接口與 Broker 上嵌入的接口相匹配。

          type Server interface {
              PingDependencies(bool) error
              ValidateJWT(string) error
          
              service.Service
          }
          

          web 服務通過調用 Start() 函數來啟動。路由綁定通過一個閉包函數[6]進行綁定,這種方式保證循環(huán)依賴不會破壞導入周期規(guī)則。

          func (bkr *Broker) Start(binder func(s Server, r *mux.Router)) {
              ...
          
              bkr.router = mux.NewRouter().StrictSlash(true)
              binder(bkr, bkr.router)
          
              ...
          
              if err := http.Serve(l, bkr.router); errors.Is(err, http.ErrServerClosed) {
                  log.Warn().Err(err).Msg("Web server has shut down")
              } else {
                  log.Fatal().Err(err).Msg("Web server has shut down unexpectedly")
              }
          }
          

          那些對故障排除(比如,Kubernetes 探針[7])或者災難恢復方案方面有用的函數,掛在 Broker 上。如果被 routes/middleware 使用的話,這些僅僅被添加到 webserver.Server 接口上。

          func (bkr *Broker) SetupDatabase() { ... }
          func (bkr *Broker) PingDependencies(failFast bool)) { ... }
          

          啟動引導

          整個應用的入口是一個 main 包。默認會啟動 Web 服務。我們可以通過傳入一些命令行參數來調用之前提到的故障排查功能,方便使用傳入 New() 函數的,經過驗證的配置來測試代理權限以及其他網絡問題。我們所要做的只是登入運行著的 pod 然后像使用其他命令行工具一樣使用它們。

          func main() {
              subCommand := flag.String("start", "", "start the webserver")
          
              ...
          
              srv := webserver.New(cfg, 80)
          
              switch strings.ToLower(subCommand) {
              case "ping":
                  srv.PingDependencies(false)
              case "start":
                  srv.Start(BindRoutes)
              default:
                  fmt.Printf("Unrecognized command %q, exiting.", subCommand)
                  os.Exit(1)
              }
          }
          

          HTTP 管道設置在 BindRoutes() 函數中完成,該函數通過 ser.Start() 注入到服務(server)中。

          func BindRoutes(srv webserver.Server, r *mux.Router) {
              r.Use(middleware.Metrics(), middleware.Authentication(srv))
              r.HandleFunc("/ping", routes.Ping()).Methods(http.MethodGet)
          
              ...
          
              r.HandleFunc("/makes/{makeID}/models/{modelID}", model.get(srv)).Methods(http.MethodGet)
          }
          

          中間件

          中間件(Middleware)返回一個帶有 handler 的函數,handler 用來構建需要的 http.HandlerFunc。這使得 webserver.Server 接口被注入,同時所有的安靜檢查只在啟動時執(zhí)行,而不是在所有路由調用的時候。

          func Authentication(srv webserver.Server) func(h http.Handler) http.Handler {
              if srv == nil || !srv.Client.IsValid() {
                  log.Fatal().Msg("a nil dependency was passed to authentication middleware")
              }
          
              // additional setup logic
              ...
          
              return func(next http.Handler) http.Handler {
                  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                      token := strings.TrimSpace(r.Header.Get("Authorization"))
                      if err := srv.ValidateJWT(token); err != nil {
                          ...
                          w.WriteHeader(401)
                          w.Write([]byte("Access Denied"))
          
                          return
                      }
          
                      next.ServeHTTP(w, r)
                  }
              }
          }
          

          路由

          路由有著與中間件有著類似的套路——簡單的設置,但是有著同樣的收益。

          func GetLatest(srv webserver.Server) http.HandlerFunc {
              if srv == nil {
                  log.Fatal().Msg("a nil dependency was passed to the `/makes/{makeID}/models/{modelID}` route")
              }
          
              // additional setup logic
              ...
          
              return func(w http.ResponseWriter, r *http.Request) {
                  ...
          
                  makeDTO, err := srv.Get
              }
          }
          

          目錄結構

          代碼的目錄結構對可發(fā)現(xiàn)性進行了高度優(yōu)化。

          ├── app/
          |   └── service-api/**
          ├── cmd/
          |   └── service-tool-x/
          ├── internal/
          |   └── service/
          |       └── mock/
          ├── pkg/
          |   ├── client/
          |   └── dtos/
          ├── (.editorconfig, .gitattributes, .gitignore)
          └── go.mod
          
          • app/ 用于項目應用——這是新來的人了解代碼傾向的切入點。dd
            • ./service-api/ 是該倉庫的微服務 API;所有的 HTTP 實現(xiàn)細節(jié)都在這里。
          • cmd/ 是存放命令行應用的地方。
          • internal/ 是不可以被該倉庫以外的項目引入的一個特殊目錄[8]
            • ./service/ 是所有領域邏輯(domain logic)所在的地方;可以被 service-apiservice-tool-x,以及任何未來直接訪問這個目錄可以帶來收益的應用或者包所引入。
          • pkg/ 用于存放鼓勵被倉庫以外的項目所引入的包。
            • ./client/ 是用于訪問 service-api 的 client 庫。其他團隊可以使用而不是自己寫一個 client,并且我們可以借助我們在 cmd/ 里面的 CI/CD 工具來 “dogfood it[9]” (使用自己產品的意思)。
            • ./dtos/ 是存放項目的數據傳輸對象,不同包之間共享的數據且以 json 形式在線路上編碼或傳輸的結構體定義。沒有從其他倉庫包導出的模塊化的結構體。/internal/service 負責 這些 DTO (數據傳輸對象)和自己內部模型的相互映射,避免實現(xiàn)細節(jié)的遺漏(如,數據庫注釋)并且該模型的改變不破壞下游客戶端消費這些 DTO。
          • .editorconfig,.gitattributes,.gitignore 因為所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore[10]
          • go.mod 甚至可以在有限制的且官僚的公司環(huán)境[11]工作。

          最重要的:每個包只負責意見事情,一件事情!

          HTTP 服務結構

          └── service-api/
              ├── cfg/
              ├── middleware/
              ├── routes/
              |   ├── makes/
              |   |   └── models/**
              |   ├── create.go
              |   ├── create_test.go
              |   ├── get.go
              |   └── get_test.go
              ├── webserver/
              ├── main.go
              └── routebinds.go
          
          • ./cfg/ 用于存放配置文件,通常是以 JSON 或者 YAML 形式保存的純文本文件,它們也應該被檢入到 Git 里面(除了密碼,秘鑰等)。
          • ./middleware 用于所有的中間件。
          • ./routes 采用類似應用的類 RESTFul 形式的目錄對路由代碼進行分組和嵌套。
          • ./webserver 保存所有共享的 HTTP 結構和接口(Broker,配置,Server等等)。
          • main.go 啟動應用程序的地方(New()Start())。
          • routebinds.go BindRoutes() 函數存放的地方。

          你覺得呢?

          如果你最終采用了這種模式,或者有其他的想法我們可以討論,我樂意聽到這些想法!


          via: https://www.dudley.codes/posts/2020.05.19-golang-structure-web-servers/

          作者:James Dudley[12]譯者:dust347[13]校對:unknwon[14]

          本文由 GCTT[15] 原創(chuàng)編譯,Go 中文網[16] 榮譽推出

          參考資料

          [1]

          簡潔: https://www.youtube.com/watch?v=rFejpH_tAHM

          [2]

          模式: https://en.wikipedia.org/wiki/Software_design_pattern

          [3]

          Mat Ryer 的文章: https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

          [4]

          Go 的組合: https://www.ardanlabs.com/blog/2015/09/composition-with-go.html

          [5]

          阻塞: https://stackoverflow.com/questions/2407589/what-does-the-term-blocking-mean-in-programming

          [6]

          閉包函數: https://gobyexample.com/closures

          [7]

          Kubernetes 探針: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)0

          [8]

          特殊目錄: https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface

          [9]

          dogfood it: https://en.wikipedia.org/wiki/Eating_your_own_dog_food

          [10]

          所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore: https://www.dudley.codes/posts/2020.02.16-git-lost-in-translation/

          [11]

          有限制的且官僚的公司環(huán)境: https://www.dudley.codes/posts/2020.04.02-golang-behind-corporate-firewall/

          [12]

          James Dudley: https://www.dudley.codes/

          [13]

          dust347: https://github.com/dust347

          [14]

          unknwon: https://github.com/unknwon

          [15]

          GCTT: https://github.com/studygolang/GCTT

          [16]

          Go 中文網: https://studygolang.com/

          想構建一個本地 Go 桌面應用程序,有幾種方法可以做到:

          1. Electron[1]:將 Node.js[2]Chromium[3] 瀏覽器綁定在一起,以創(chuàng)建一個打包的本地 Web 應用程序。可與 Go 框架(例如 go-app[4]go-astilectron[5])一起使用。
          2. Lorca[6]:使用本地安裝的 Chrome 瀏覽器,通過 dev-tools communication protocol[7] 實現(xiàn)。
          3. Webview[8]:使用 webview[9] 創(chuàng)建一個本地窗口,并使用 CGo 綁定在其中渲染應用程序。

          我已經寫過有關構建一個簡單的 electron 應用程序的文章[10],因此本文將探討如何使用 Lorca 和 Webview 構建應用程序,然后比較這三種的不同。

          Lorca

          Go 中一個簡單的 Lorca[11] 應用:

          func main() {
           // Create UI with data URI
           ui, _ := lorca.New("data:text/html,"+url.PathEscape(`
           <html>
            <head><title>Hello</title></head>
            <body><h1>Hello, world!</h1></body>
           </html>
           `), "", 600, 200)
           defer ui.Close()
           // Create a GoLang function callable from JS
           ui.Bind("hello", func() string { return "World!" })
           // Call above `hello` function then log to the JS console
           ui.Eval("hello().then( (x) => { console.log(x) })")
           // Wait until UI window is closed
           <-ui.Done()
          }
          

          因為復雜性被隱藏了,所以看起來非常簡單!上面的代碼打開一個 Chome 窗口,通過 websocket 連接到其 dev-tools[12] 端點,發(fā)送要加載的 HTML,并提供 Go 和 JS 之間的通信:

          更酷的是,您可以在 Chrome 中調用 JS 函數并在 Go 中獲取輸出

          n := ui.Eval(`Math.random()`).Float()
          fmt.Println(n)
          

          使用這個庫是如此容易,如此直觀,如此實用,以至于我剛使用它時感到困惑。我以為一定有陷阱,不會這么簡單。但是沒有,它就是這么簡單。

          另外一個好處是您可以使用 Chrome 開發(fā)工具來幫助調試任何問題或調整布局。另外,鑒于我自 2014 年以來[13]一直在寫 Promise,我喜歡使用 JS Promise 在 Go 和 JS 之間實現(xiàn)異步調用。

          Lorca 的最大缺點是,由于它使用 Chrome,因此某些應用程序詳細信息(如系統(tǒng)菜單,圖標,標題)無法自定義。然后,需要在應用程序優(yōu)化和簡單應用程序之間進行權衡。根據您構建內容的不同,有好有弊,例如,如果您正在構建內部工具,那會很好,但是對于企業(yè)應用程序,這可能看起來并不好。

          Webview

          Webview[14] 是一個庫,可幫助直接在本地組件之上構建 Web 應用程序。執(zhí)行此操作的代碼如下:

          func main() {
           w := webview.New(true)
           defer w.Destroy()
           w.SetSize(600, 200, webview.HintNone)
           // Create a GoLang function callable from JS
           w.Bind("hello", func() string { return "World!" })
          
           // Create UI with data URI
           w.Navigate(`data:text/html,
            <!doctype html>
            <html>
             <head><title>Hello</title></head>
             <body><h1>Hello, world!</h1></body>
             <script> hello().then((x) => { console.log(x) }) </script>
            </html>`)
          
           w.Run()
          }
          

          這與 Lorca 非常相似,我認為 Lorca 也是基于 Webview 的。盡管與 Lorca 相似,但輸出還是有些不同:

          從上圖可以看到 Webview 應用程序窗口沒有陰影,沒有邊框,并且在屏幕的左下角進行了初始化。可以通過將Window返回一個 unsafe.Pointer到 OS 依賴的窗口對象的方法(在 macOS 中是 NSWindow)進行定制。這是開始難的地方。

          要使用該 Window 對象,我們必須將 Go 的綁定寫入本地組件。舉例來說,如果我們希望我們的窗口居中啟動,我們會調用 NSWindow 的 Center 方法。因此,我們需要在三個文件中寫綁定(改編自 gogoa[15]):

          ns_window.go

          package main
          // #cgo CFLAGS: -x objective-c
          // #cgo LDFLAGS: -framework Cocoa
          //#include "ns_window.h"
          import "C"
          import "unsafe"
          type NSWindow struct {
           ptr unsafe.Pointer
          }
          func (self *NSWindow) Center() {
           C.Center(self.ptr)
          }
          

          ns_window.h

          #include <Cocoa/Cocoa.h>
          void Center(void *);
          

          ns_window.m

          #include "ns_window.h"
          void Center(void *self) {
            NSWindow *window = self;
            [window center];
          }
          

          然后在main()函數中,我們可以將窗口居中:

          window := NSWindow {w.Window()}
          window.Center()
          

          與 Lorca 不同,Webview 可以針對我們的應用程序進行完全自定義。問題在于它需要一些工作。

          Webview 的一些其他部分使得用它變得有些困難:

          1. 如果使用 Bazel 和 gazelle,則webview生成的Build.bazel文件不正確,clinkopts=["-framework WebKit"] 必須對其進行修補。
          2. 調用 w.Init 僅在w.Navigate被調用時有效,但隨后w.Eval調用將停止工作。
          3. 要設置標題,您可以如上所述編寫綁定,或者您必須使用Dispatch方法w.Dispatch(func() { w.SetTitle("Title") })。

          我不確定有多少是Webview,有多少是 NSWindow。我需要進行更多的調查和學習,才能更清楚地說明這些發(fā)生的原因。

          Electron

          之前的文章[16]是關于構建一個簡單的 Electron 應用程序的,該應用程序如下所示:

          Electron 用于許多大型產品,例如 VSCode。這可能是因為將所有內容捆綁到一個應用程序中使可移植性變得更加簡單,并且可以廣泛地定制應用程序。將應用程序與瀏覽器和 Node.js 捆綁在一起的不利之處在于,它導致程序 非常龐大

          讓 Go 與 Electron 一起工作也有些困難。但有一些框架可以簡化[17]此過程,例如 go-astilectron[18],不過這些框架很復雜,并且大多數功能不完整。另一種方法可能是使用我之前寫過的[19] Go 編譯為 WASM ,但這也不是簡單的解決方案。

          Electron 的優(yōu)勢在于它是便攜式的,可定制的,并且經過了應用程序分發(fā)的嚴格測試。只是和 Go 結合有點復雜。

          三者比較

          我認為要進行的主要比較是可定制性與簡單性。到目前為止,Lorca 是最簡單的,其可定制性非常有限,Webview 可以完全自定義,但有些困難,而 Electron 則可以完全自定義,但很難與 Go 一起使用。

          同樣,框架之間的捆綁包大小也有很大差異。Lorca 的二進制文件大小為 8.7 MB,Webview 的大小為 3.7Mb,Electron 的大小為 157Mb

          調試工具也有所不同:Lorca 和 Electron 使用 Chrome 開發(fā)工具,而 Webview 使用 Safari 開發(fā)工具。

          結論

          Lorca 和 Webview 都可以與 Go 一起很好地使用,最終二進制較小,并且具有類似的 API。主要區(qū)別在于基礎渲染器(本機)和調試工具。

          我認為 Electron 與 Go 一起使用可能太復雜了,但沒有太多困難。

          一個潛在的工作流程是在開發(fā)和 Webview 分發(fā)期間使用 Lorca。Lorca 提供了用于調試和開發(fā)的熟悉工具,其中 Webview 提供了可分發(fā)的可定制性。Lorca 也是很好的備份,可以交叉編譯到 Webview 不支持的其他操作系統(tǒng)。

          注意:還有更多類似的選項,wails[20]gotk[21] 可以提供其他方式來構建/分發(fā)應用程序。

          作者:Graham Jenson

          原文鏈接:https://maori.geek.nz/golang-desktop-app-webview-vs-lorca-vs-electron-a5e6b2869391

          譯者:polaris

          參考資料

          [1]

          Electron: https://www.electronjs.org/

          [2]

          Node.js: https://nodejs.org/

          [3]

          Chromium: https://www.chromium.org/

          [4]

          go-app: https://github.com/maxence-charriere/go-app

          [5]

          go-astilectron: https://github.com/asticode/go-astilectron

          [6]

          Lorca: https://github.com/zserge/lorca

          [7]

          dev-tools communication protocol: https://chromedevtools.github.io/devtools-protocol/

          [8]

          Webview: https://github.com/webview/webview

          [9]

          webview: https://developer.apple.com/documentation/webkit/webview

          [10]

          構建一個簡單的 electron 應用程序的文章: https://maori.geek.nz/building-an-electron-app-with-bazel-d124ed550957

          [11]

          Lorca: https://github.com/zserge/lorca

          [12]

          dev-tools: https://chromedevtools.github.io/devtools-protocol/

          [13]

          自 2014 年以來: https://maori.geek.nz/jquery-promises-and-deferreds-i-promise-this-will-be-short-d10275f82717

          [14]

          Webview: https://github.com/webview/webview

          [15]

          gogoa: https://github.com/alediaferia/gogoa

          [16]

          之前的文章: https://maori.geek.nz/building-an-electron-app-with-bazel-d124ed550957

          [17]

          簡化: https://github.com/asticode/go-astilectron

          [18]

          go-astilectron: https://github.com/asticode/go-astilectron

          [19]

          之前寫過的: https://maori.geek.nz/a-web-app-using-bazel-golang-wasm-and-proto-c020914f4341

          [20]

          wails: https://github.com/wailsapp/wails

          [21]

          gotk: https://github.com/gotk3/gotk3


          主站蜘蛛池模板: 国产伦精品一区二区三区视频小说 | 国产精品一区二区在线观看| 久久中文字幕一区二区| 色精品一区二区三区| 久久中文字幕无码一区二区| 精品国产a∨无码一区二区三区| 亚洲一区二区三区在线网站| 精品女同一区二区三区免费站| 亚洲欧美日韩中文字幕在线一区| 成人免费视频一区| 一区二区三区电影网| 亚洲一区二区三区夜色| 久久人妻无码一区二区| 手机看片福利一区二区三区| 亚洲AV无码一区东京热久久| 无码人妻久久一区二区三区免费丨 | 午夜无码一区二区三区在线观看| 国产一区二区三区在线影院 | 国产香蕉一区二区精品视频| 无码日韩人妻AV一区免费l| 亚洲日韩精品一区二区三区无码 | 97久久精品午夜一区二区| 亚洲AV无码国产一区二区三区| 人妻无码一区二区视频| 一本久久精品一区二区| 久久精品无码一区二区app| 亚洲一区爱区精品无码| 国产日产久久高清欧美一区| 精品一区二区无码AV| 亚洲AV无码一区二区乱子仑| 色综合一区二区三区| 中文字幕AV一区二区三区| 午夜精品一区二区三区在线视| 久久精品午夜一区二区福利| 国内精品视频一区二区三区| 蜜桃无码AV一区二区| 精品久久久久中文字幕一区| 亚洲日韩国产一区二区三区在线 | 丰满人妻一区二区三区视频53| 97久久精品一区二区三区| 无码精品人妻一区二区三区免费看|