Golang 在即刻後端的實踐

2021-12-31 即刻技術團隊

收錄於話題 #golang 1個

背景

隨著業務變遷,即刻後端服務內積累了大量的陳舊代碼,維護成本較高,代碼重構甚至重寫被提上了日程。相比起 Node.js ,Golang 有著一定的優點。由於即刻後端已經較好地服務化了,其他業務在 Go 上也有了一定的實踐,直接使用 Go 重寫部分即刻服務是一個可行的選擇。在此過程中我們可以驗證在同一個業務上兩種語言的差異,並且可以完善 Go 相關的配套設施。

改造成果

截至目前,即刻部分非核心服務已經通過 Go 重寫並上線。相比原始服務,新版服務的開銷顯著降低:

接口響應時長降低 50%

舊服務響應時間

新服務響應時間

內存佔用降低 95%

服務替換前後內存消耗趨勢

CPU 佔用降低 90%

服務替換前後 CPU 消耗趨勢

註:以上性能數據以用戶篩選服務為例,這是一個讀遠大於寫、任務單一的服務。由於在重寫的過程中,對原有的實現也進行了一定的優化,所以以上數據僅供參考,不完全代表 Go 和 Node 真實性能比較。

改造方案

第一步:重寫服務

在保證對外接口不變的情況下,需要重寫一遍整個業務核心邏輯。不過在重寫的過程當中,還是碰到一些問題:

以往的 Node 服務大多沒有顯式聲明接口的輸入輸出類型,重寫的時候需要找到所有相關欄位。

以往代碼絕大多數不包含單元測試,重寫之後需要理解業務需求並設計單元測試。

老代碼裡面大量使用了 any 類型,需要費一番功夫才能明確所有可能的類型。很多類型在 Node 裡面不需要非常嚴格,但是放到 Go 裡面就不容偏差。

總之,重寫不是翻譯,需要對業務深入理解,重新實現一套代碼。

第二步:正確性驗證

由於很多服務沒有完整的回歸測試,單純地依賴單元測試是遠遠不夠保證正確性的。

一般來說,只讀的接口可以通過數據對拍來驗證接口正確性,即對比相同輸入的新舊服務的輸出。對於小規模的數據集,可以通過在本地啟動兩個服務進行測試。但是一旦數據規模足夠大,就沒辦法完全在本地測試,一個辦法就是流量複製測試。

由於服務之間跨環境調用比較麻煩且影響性能,所以使用消息隊列複製請求異步對拍。

原始服務在每一次響應的時候,將輸入和輸出打包成消息發送至消息隊列。

在測試環境下的消費服務會接受消息,並將輸入重新發送至新版服務。

等到新版服務響應之後,消費服務會對比前後兩次響應體,如果結果不同則輸出日誌。

最後,只需要下載日誌到本地,根據測試數據逐一修正代碼即可。

第三步:灰度並逐步替換舊服務

等到對業務正確性胸有成竹,就可以逐步上線新版服務了。得益於服務拆分,我們可以在上下遊無感的情況下替換服務,只需要將對應服務的逐步替換為新的容器即可。

倉庫結構

項目結構是基於 Standard Go Project Layout 的 monorepo:

.├── build: 構建相關文件,可 symbolic link 至外部├── tools: 項目自定義工具├── pkg: 共享代碼│   ├── util│   └── ...├── app: 微服務目錄│   ├── hello: 示例服務│   │   ├── cmd│   │   │   ├── api│   │   │   │   └── main.go│   │   │   ├── cronjob│   │   │   │   └── main.go│   │   │   └── consumer│   │   │       └── main.go│   │   ├── internal: 具體業務代碼一律放在 internal 內,防止被其他服務引用│   │   │   ├── config│   │   │   ├── controller│   │   │   ├── service│   │   │   └── dao│   │   └── Dockerfile│   ├── user: 大業務拆分多個子服務示例│   │   ├── internal: 子業務間共享代碼│   │   ├── account:帳戶服務│   │   │   ├── main.go│   │   │   └── Dockerfile│   │   └── profile: 用戶主頁服務│   │       ├── main.go│   │       └── Dockerfile│   └── ...├── .drone.yml├── .golangci.yaml├── go.mod└── go.sum

這種模式帶來的好處:

持續集成與構建

靜態檢查

項目使用 golangci-lint 靜態檢查。每一次代碼 push,Github Action 會自動運行 golangci-lint,非常快且方便,如果發生了錯誤會將警告直接 comment 的 PR 上。

golangci-lint 本身不包含 lint 策略,但是可以集成各式 linter 以實現非常細緻的靜態檢查,把潛在錯誤扼殺在搖籃。

測試+構建鏡像

為了更快的構建速度,我們嘗試過在 GitHub Action 上構建鏡像,通過 matrix 特性可以良好地支持 monorepo。但是構建鏡像畢竟相對耗時,放在 GitHub Action 上構建會耗費大量的 GitHub Action 額度,一旦額度用完會影響正常開發工作。

最終選擇了自建的 Drone 來構建,通過 Drone Configuration Extension 也可以自定義複雜的構建策略。

通常來講,我們希望 CI 系統構建策略足夠智能,能夠自動分辨哪些代碼是需要構建,哪些代碼是需要測試的。在開發初期,我也深以為然,通過編寫腳本分析整個項目的依賴拓撲,結合文件變動,找到所有受到影響的 package,進而執行測試和構建。看上去非常美好,但是現實是,一旦改動公共代碼,幾乎所有服務都會被重新構建,簡直就是噩夢。這種方式可能更加適合單元測試,而不是打包。

於是,我現在選擇了一種更加簡單粗暴的策略,以 Dockerfile 作為構建的標誌:如果一個目錄包含 Dockerfile,那麼表示此目錄為「可構建「的;一旦此目錄子文件發生變動(新增或者修改),則表示此 Dockerfile 是「待構建「的。Drone 會為每一個待構建的 Dockerfile 啟動一個 pipeline 進行構建。

有幾點是值得注意的:

由於構建的時候不但需要拷貝當前服務的代碼,同時需要拷貝共享代碼,構建的時候就需要將上下文目錄設置在根目錄,並將服務目錄作為參數傳入方便構建:
docker build --file app/hello/Dockerfile --build-arg TARGET="./app/hello" .

鏡像名會被默認命名為從內向外的文件夾名的拼接,如./app/live/chat/Dockerfile 在構建之後會生成 {registry}/chat-live-app:{branch}-{commitId} 形式的鏡像。

所有構建(包括下載依賴、編譯)由 Dockerfile 定義,避免在 CI 主流程上引入過多邏輯降低靈活度。通過 Docker 本身的緩存機制也能使構建速度飛快。

一個問題,一旦服務目錄之外的共享代碼發生變化,Drone 無法感知並構建受到影響的服務。解決方案是在 git commit message 內加上特定的欄位,告知 Drone 執行相應的構建。

配置管理

在 Node 項目裡面,我們通常使用 node-config 來為不同環境配置不同的配置。Go 生態內並沒有現成的工具可以直接完成相同的工作,不過可以嘗試拋棄這種做法。

正如 Twelve-Factor 原則所推崇的,我們要儘可能通過環境變量來配置服務,而不是多個不同的配置文件。事實上,在 Node 項目當中,除開本地開發環境,我們往往也是通過環境變量動態配置,多數的 test.json/beta.json 直接引用了 production.json。

我們將配置分為兩部分:

單一配置文件
我們在服務內通過文件的方式,定義一份完整的配置,作為基礎配置,並且可以在本地開發的時候使用。

動態環境變量

當服務部署到線上之後,在基礎配置的基礎上,我們將環境變量注入到配置當中。

我們可以在服務目錄中編寫一份 config.toml(選擇任何喜歡的配置格式),並編寫基礎的配置,作為本地開發的時候使用。

port=3000sentryDsn="https://project@sentry.io"
[mongodb]url="mongodb://localhost:27017"database="db"

當在線上運行的時候,我們還需要在配置當中注入環境變量。可以使用 Netflix/go-env 將環境變量注入配置數據結構中:

type MongoDBConfig struct {    URL      string `toml:"url" env:"MONGO_URL,MONGO_URL_ACCOUNT"`    Database string `toml:"database"`}
type Config struct { Port int `toml:"port" env:"PORT,default=3000"` SentryDSN string `toml:"sentryDsn"` MongoDB *MongoDBConfig `toml:"mongodb"`}
var configToml string
func ParseConfig() (*Config, error) { var cfg Config if _, err := toml.Decode(configToml, &cfg); err != nil { return nil, err } if _, err := env.UnmarshalFromEnviron(&cfg); err != nil { return nil, err } return &cfg, nil}

上面代碼還使用了最新的 Go1.16 embed 功能,只需要一行 Compiler Directive 就可以將任意文件一併打包進入最終構建出來二進位文件內,構建鏡像只需要拷貝單個可執行文件即可,降低構建發布的複雜度。

服務調用

代碼管理

即刻後端有多種語言的服務(Node/Java/Go),各個服務重複定義類型會造成人力浪費和不統一,故通過 ProtoBuf 定義類型,再用 protoc 生成對應的代碼,並在一個倉庫內維護各個語言的 client。

.├── go│   ├── internal: 內部實現,如 http client 封裝│   ├── service│   │   ├── user│   │   │   ├── api.go: 接口定義與實現│   │   │   ├── api_mock.go: 通過 gomock 生成的接口 mock│   │   │   └── user.pb.go: 通過 protoc 生成的類型文件│   │   ├── hello│   │   └── ...│   ├── go.mod│   ├── go.sum│   └── Makefile├── java├── proto│   ├── user│   │   └── user.proto│   ├── hello│   │   └──  hello.proto│   └── ...└── Makefile

每一個服務通過一個獨立的 package 對外暴露接口,每一個服務都由四部分組成:

ProtoBuf

正如上面所說,為了降低內部接口對接和維護成本,我們選擇使用 ProtoBuf 定義類型,並生成了 Go 類型。雖然使用 ProtoBuf 定義,但服務之間依然通過 JSON 傳遞數據,數據序列化和反序列化成了問題。

為了簡化 ProtoBuf 和 JSON 互相轉換,Google 提供了一個叫做 jsonpb 的包,這個包在原生 json 的基礎上實現了 Enum Name(string) 和 Value(int32) 互相轉換,以兼容傳統的 string enum;還支持了 oneof 類型。上面的能力都是 Go 原生的 json 所無法實現的。如果使用原生 json 序列化 proto 類型,將會導致 enum 無法輸出字符串和 oneof 完全無法輸出。

這麼說起來,是不是我們在代碼全部都使用 jsonpb 替換掉原生 json 就好了?並不是,jsonpb 只支持對 proto 類型序列化:

func Marshal(w io.Writer, m proto.Message) error

除非所有對外讀寫接口的類型都用 ProtoBuf 定義,否則就不能一路使用 jsonpb 。

不過天無絕人之路,Go 的原生 json 定義了兩個接口:

// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
MarshalJSON() ([]byte, error)
}

// Unmarshaler is the interface implemented by types
// that can unmarshal a JSON description of themselves.
// The input can be assumed to be a valid encoding of
// a JSON value. UnmarshalJSON must copy the JSON data
// if it wishes to retain the data after returning.
//
// By convention, to approximate the behavior of Unmarshal itself,
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}

任何類型只要實現了這兩個接口,在被(反)序列化的時候就能調用自己的邏輯進行操作,類似 Hook 函數。那樣,只需要為所有的 proto 類型實現這兩個接口:當 json 嘗試(反)序列化自己,就轉而使用 jsonpb 進行。

func (msg *Person) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := (&jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: false,
}).Marshal(&buf, msg)
return buf.Bytes(), err
}

func (msg *Person) UnmarshalJSON(b []byte) error {
return (&jsonpb.Unmarshaler{
AllowUnknownFields: true,
}).Unmarshal(bytes.NewReader(b), msg)
}

經過一番尋找,最後找到了一個 protoc 插件 protoc-gen-go-json :它可以在生成 proto 類型的同時,為所有類型實現 json.Marshaler 和 json.Unmarshaler。這樣一來就不需要為了序列化兼容而妥協,並且對代碼也沒有任何侵入性。

發布

由於是獨立維護的倉庫,需要以 Go module 的形式引入項目內使用。得益於 Go module 的設計,版本發布可以和 GitHub 無縫結合在一起,效率非常高。

測試版本
go mod 支持直接拉取對應的分支的代碼作為依賴,不需要手動發布 alpha 版本,只需要在調用方的代碼執目錄執行 go get -u github.com/iftechio/rpc/go@{branch} 就可以直接下載對應開發分支的最新版本了。

正式版本
當改動合併進入主分支,只需通過 Github Release 就可以發布一個穩定版本(也可以在本地打 git tag),即可通過具體版本號拉到對應的倉庫快照:go get github.com/iftechio/rpc/go@{version}

由於 go get 本質上就是下載代碼,我們的代碼託管在 GitHub 上,所以在國內阿里雲上構建代碼時可能因為網絡原因出現拉取依賴失敗的情況(private mod 無法通過 goproxy 拉取)。於是我們改造了 goproxy,在集群內部署了一個 goproxy:

我們只需要執行如下代碼即可通過內部 goproxy 下載依賴:

GOPROXY="http://goproxy.infra:8081" \
GONOSUMDB="github.com/iftechio" \
go mod download

ContextContext provides a means of transmitting deadlines, caller cancellations, and other request-scoped values across API boundaries and between processes.

Context 是 Go 當中一個非常特別的存在,可以像一座橋一樣將整個業務串起來,使得數據和信號可以在業務鏈路上下遊之間傳遞。在我們的項目當中,context 也有不少的應用:

取消信號

每一個 http 請求都會攜帶一個 context,一旦請求超時或者 client 端主動關閉連接,最外層會將一個 cancel 信號通過 context 傳遞到整個鏈路當中,所有下遊調用立即結束運行。如果整個鏈路都遵循這個規範,一旦上遊關閉請求,所有服務都會取消當前的操作,可以減少大量無謂的消耗。

在開發的時候就需要注意:

大多數任務被取消的同時,會拋出一個 context.ErrCancelled 錯誤,以使調用者能夠感知異常並退出。但是 RPC 斷路器也會捕獲這個錯誤並記錄為失敗。極端場景下,客戶端不斷發起請求並立刻取消,就能夠使服務的斷路器紛紛打開,造成服務的不穩定。解決方案就是改造斷路器,對於特定的錯誤依然拋出,但不記錄為失敗。

分布式場景下絕大多數數據寫入無法使用事務,需要考慮一個操作如果被中途取消,最終一致性還能否得到保證?對於一致性要求高的操作,需要在執行前主動屏蔽掉 cancel 信號:

// 返回一個僅僅實現了 Value 接口的 context
// 只保留 context 內的數據,但忽略 cancel 信號

func DetachedContext(ctx context.Context) context.Context {
return &detachedContext{Context: context.Background(), orig: ctx}
}

type detachedContext struct {
context.Context
orig context.Context
}

func (c *detachedContext) Value(key interface{}) interface{} {
return c.orig.Value(key)
}

func storeUserInfo(ctx context.Context, info interface{}) {
ctx = DetachedContext(ctx)
saveToDB(ctx, info)
updateCahce(ctx, info)
}

上下文透傳

每一個請求進入的時候,http request context 都被攜帶上各種當前 request 的信息,比如 traceId、用戶信息,這些數據就能夠隨著 context 被一路透傳至業務整條鏈路,期間收集到的監控數據都會與這些數據進行關聯,便於監控數據聚合。

Context.Value should inform, not control.

使用 context 傳遞數據最需要注意的就是:context 的數據僅僅用於監控,切勿用於業務邏輯。所謂「顯式優於隱式」,由於 context 不直接對外暴露任何內部數據,使用 context 傳遞業務數據會使程序非常不優雅,而且難以測試。換句話說,任何一個函數哪怕傳入了的是 emptyCtx 也不應該影響正確性。

錯誤收集Errors are just values.

Go 的錯誤是一個普通的值(從外部看來就是一個字符串),這給收集錯誤帶來了一定的麻煩:我們收集錯誤不單需要知道那一行錯誤的內容,還需要知道錯誤的上下文信息。

Go1.13 引入了 error wrap 的概念,通過 Wrap/Unwrap 的設計, 就可以將一個 error 變成單向鍊表的結構,每一個節點上都能夠存儲自定義的上下文信息,並且可以使用一個 error 作為鍊表頭讀取後方所有錯誤節點。

對於單個錯誤來說,錯誤的 stacktrace 是最重要的信息之一。Go 通過 runtime.Callers 實現 stacktrace 收集:

Callers fills the slice pc with the return program counters of function invocations on the calling goroutine's stack.

可以看到, Callers 只能收集單個 goroutine 內的調用棧,如果希望收集到完整的 error trace,則需要在跨 goroutine 傳遞錯誤的時候,將 stacktrace 包含在 error 內部。這個時候就可以使用第三方庫 pkg/errors 的 errors.WithStack 或者 errors.Wrap 來實現,它們會創建一個新的 error 節點,並存入當時的調用棧:

// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}

func main() {
ch := make(chan error)
go func() {
err := doSomething()
ch <- errors.withStack(err)
}()
err := <-ch
fmt.Printf("%w", err)
}

最終的錯誤收集(往往在根部的 web 中間件上),可以直接使用 Sentry:

sentry.CaptureException(errors.WithStack(err)) // 最終上傳的時候也不忘收集 stacktrace

Sentry 會基於 errors.Unwrap 接口,取出每一層的 error。Sentry 針對每一層 error 能夠自動導出錯誤棧。由於 stacktrace 並非正式標準,Sentry 主動適配了幾個主流的 Stacktrace 方案,其中就包括 pkg/errors 的。

這樣就可以通過 Sentry 後臺查看完整的報錯信息。如下圖,每一個大的 section 都是一層 error,每一個 section 內都包含這個 error 內的上下文信息。

參考連結

TJ 談 Go 相比 Node 的生產力優勢 https://qr.ae/pNdNhU

Standard Go Project Layout https://github.com/golang-standards/project-layout

The Tweleve-Factor App https://12factor.net/

Go Wiki - Module: Releaseing Modules (V2 or Higher) https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher

How to correctly use context.Context in Go 1.7 https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39

Don’t just check errors, handle them gracefully https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

相關焦點

  • golang 編程風格最佳實踐
    【導讀】本文介紹了作者的 go 項目代碼風格規範最佳實踐。/doc/effective_go.htmluber golang 代碼規範 https://github.com/uber-go/guideuber golang 代碼規範中文 https://github.com/xxjwxc/uber_go_guide_cn代碼目錄規範
  • Node.js為何在後端開發中不受重視?
    語言只是一個工具,對高手來講Java、golang、python、Javascript都可以完成複雜的後端開發工作,這些語言最大的區別是生態。Java無疑是所有後端開發語言中的佼佼者,它的生態完善度超乎你的想像,這也是Java在後端開發領域無法撼動的根本。其他幾種語言我覺得沒有根本性的區別,論生態完善度,都是半斤八兩,論高並發,golang當之無愧,論語言友好度,Node.js可以排到首位。
  • 走進Golang之編譯器原理
    上面兩段文字來自 golang compile :https://github.com/golang/go/tree/master/src/cmd/compile這裡多說一句,我們常常在debug代碼的時候,需要禁止內聯,其實就是操作的這個階段。
  • Golang開源項目推薦(持續更新)
    適合學習者:這個開源項目是Go語言高級編程的開源書籍,適合 Go 語言進階學習者四、flipped-aurora/gin-vue-admin基於gin+vue搭建的(中)後臺系統框架,集成jwt鑑權,權限管理,動態路由,分頁封裝,多點登錄攔截,資源權限,上傳下載,代碼生成器,表單生成器,通用工作流等基礎功能,五分鐘一套CURD前後端代碼
  • 《8小時轉職Golang工程師》
    置頂 本視頻偏入門級,主要是針對後端想快速低成本掌握golang開發人群學習,如您已經掌握golang請繞行。
  • Golang後臺單元測試實踐
    單元測試的時機編碼前:TDDTest-Driven Development, 測試驅動開發,是敏捷開發的⼀項核⼼實踐和技術,也是⼀種設計⽅法論。TDD原理是開發功能代碼之前,先編寫測試⽤例代碼,然後針對測試⽤例編寫功能代碼,使其能夠通過。
  • Golang指南:頂級Golang框架、IDE和工具列表
    (點擊尾部閱讀原文前往)原文:https://dzone.com/articles/golang-guide-a-list-of-top-golang-frameworks-ides自推出以來,Google的Go程式語言(Golang)越來越受主流用戶的歡迎。
  • 初學者入門 Golang 的學習型項目
    這些年 Go team 在不斷優化編譯器生成的目標代碼的性能,比如在 Go 1.7 版本中引入 ssa 後端。Go 1.10 延續著對目標代碼生成的進一步優化,雖說動作遠不如引入 ssa 這麼大。GC 的性能一直是廣大 Gopher 密切關注的事情,在 Go 1.9 中依舊繼續優化和改善,大多數程序使用 1.9 編譯後都能得到一定程度的性能提升。
  • golang mod 入門
    golang 提供了 go mod命令來管理包。/x/crypto/acme/autocert latestgo: finding golang.org/x/crypto/acme latestgo: finding golang.org/x/crypto latestbuild command-line-arguments: cannot find module for path _/home/gs/helloworld
  • Golang 入門 : 配置代理
    當我們使用 go get、go install、go mod 等命令時,類似於 golang.org/x/... 的包會是無法下載的。我們常見的 golang.org/x/... 包,一般在 GitHub 上都有官方的鏡像倉庫對應。比如 zieckey/golang.org 就是作為 golang.org/x 的鏡像庫存在的。我們可以手動下載或 clone 對應的 GitHub 倉庫到指定的目錄下,比如從 zieckey/golang.org 下載 x 目錄下的所有包。
  • 後端開發實踐:Spring Boot項目模板
    基於以上,我希望整理出一套公共性的項目模板出來,旨在儘量多地包含日常開發之所需,減少開發者的重複性工作以及提供一些最佳實踐。對於後端開發而言,我選擇了當前被行業大量使用的Spring Boot,基於此整理出了一套公共的、基礎性的實踐方式,在結合了自己的經驗以及其他項目的優秀實踐之後,總結出本文以饗開發者。
  • 構建微服務的十大 Golang 框架和庫
    Swagger 驗證 (go-swagger/go-swagger)這個包包含了 Swagger 2.0(也就是 OpenAPI 2.0)的 golang 實現:它知道如何序列化和反序列化 swagger 規範。
  • golang中的fallthrough
    收錄於話題 #golang因為fallthrough不能孤立使用,需要在switch語句中使用,使用方法和break接近。不加break和fallthrough先看代碼。
  • 怎樣構建Golang Dockerfiles?
    Docker 在 Dockerfile 文檔中一上來就強調:儘量減少層數是一個最佳實踐!這是一個重要的概念,必須從一開始就做好。你很容易就能寫一個包含很多層的 Dockerfile——它的語法就有這個傾向——結果你不知不覺中就會寫出很多效率低下的內容。最佳實踐是將構建的相關階段分組和連結在一起,例如下載依賴項、供應商文件夾集成或使用 RUN 命令設置構建環境等階段。
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • Golang的字符編碼與regexp
    需要注意的是,生成前綴字符串時其底層將調用 strings.Builder 的 WriteRune() 函數(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L147),內部將調用 utf8.EncodeRune() 強制轉換表達式的字符為 UTF-8 編碼(如:\xff => \xc3\xbf)。
  • golang標準庫log
    收錄於話題 #golang log.Print("my log") log.Printf("my log %d", 100) name := "tom" age := 20 log.Println(name, ",", age) log.Panic("致命錯誤!") // log.Fatal("致命錯誤!")
  • golang標準庫template
    "text/template")type Person struct { Name string可以使用管道符號|連結多個命令,用法和unix下的管道類似:|前面的命令將運算結果(或返回值)傳遞給後一個命令的最後一個位置。需要注意的是,並非只有使用了|才是pipeline。Go template中,pipeline的概念是傳遞數據,只要能產生數據的,都是pipeline。
  • Golang入門教程——面向對象篇
    今天是golang專題的第9篇文章,我們一起來看看golang當中的面向對象的部分。在現在高級語言當中,面向對象幾乎是不可或缺也是一門語言最重要的部分之一。golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。
  • 網易雲golang從入門到精通【好課分享】
    網易雲golang從入門到精通【好課分享】       那麼, 這種事實對本人來說意義重大, 相信對這個世界也是有一定意義的.似乎是一種巧合,但如果我們從一個更大的角度看待問題,這似乎是一種不可避免的事實. 這樣看來, 在這種不可避免的衝突下,我們必須解決這個問題.