規(guī)范旨在為日常Go項目開發(fā)提供一個代碼的規(guī)范指導,方便團隊形成一個統(tǒng)一的代碼風格,提高代碼的可讀性,規(guī)范性和統(tǒng)一性。本規(guī)范將從命名規(guī)范,注釋規(guī)范,代碼風格和 Go 語言提供的常用的工具這幾個方面做一個說明。該規(guī)范參考了 go 語言官方代碼的風格制定。
命名是代碼規(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) }
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)建時間):
例如 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 字符
我們使用Goland開發(fā)工具,可以直接使用快捷鍵:ctrl+alt+L,即可。
Go語言中是不需要類似于Java需要冒號結尾,默認一行就是一條數據
如果你打算將多個語句寫在同一行,它們則必須使用。
括號和空格方面,也可以直接使用 gofmt 工具格式化(go 會強制左大括號不換行,換行會報語法錯誤),所有的運算符和操作數之間要留空格。
// 正確的方式 if a > 0 { ? } ? // 錯誤的方式 if a>0 // a ,0 和 > 之間應該空格 { // 左大括號不可以換行,會報語法錯誤 ? }
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"
但是如果是引入本項目中的其他包,最好使用相對路徑。
// 錯誤寫法 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中有哪些命令。
亮點:
使用replace替換無法直接獲取的package 依賴包沖突問題 自動查找包依賴
兩個等級: [S] 建議, [M] 必須。以下是細節(jié)。
總而言之,文件名和目錄名,包名都必須小寫。數據類型變量和參數等定義最好使用駝峰大小寫法,不要使用下劃線或者中劃線
用了近十年的 C# 轉到 Go 是一個有趣的旅程。有時,我陶醉于 Go 的簡潔[1];也有些時候,當熟悉的 OOP (面向對象編程)模式[2]無法在 Go 代碼中使用的時候會感到沮喪。幸運的是,我已經摸索出了一些寫 HTTP 服務的模式,在我的團隊中應用地很好。
當在公司項目上工作時,我傾向把可發(fā)現(xiàn)性放在最高的優(yōu)先級上。這些應用會在接下來的 20 年運行在生產環(huán)境中,必須有眾多的開發(fā)人員和網站可靠性工程師(可能是指運維)來進行熱補丁,維護和調整工作。因此,我不指望這些模式能適合所有人。
Mat Ryer 的文章[3]是我使用 Go 試驗 HTTP 服務的起點之一,也是這篇文章的靈感來源。
一個 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
最重要的:每個包只負責意見事情,一件事情!
└── service-api/
├── cfg/
├── middleware/
├── routes/
| ├── makes/
| | └── models/**
| ├── create.go
| ├── create_test.go
| ├── get.go
| └── get_test.go
├── webserver/
├── main.go
└── routebinds.go
如果你最終采用了這種模式,或者有其他的想法我們可以討論,我樂意聽到這些想法!
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 桌面應用程序,有幾種方法可以做到:
我已經寫過有關構建一個簡單的 electron 應用程序的文章[10],因此本文將探討如何使用 Lorca 和 Webview 構建應用程序,然后比較這三種的不同。
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[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 的一些其他部分使得用它變得有些困難:
我不確定有多少是Webview,有多少是 NSWindow。我需要進行更多的調查和學習,才能更清楚地說明這些發(fā)生的原因。
我之前的文章[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
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。