【後端專場 學習資料一】第五屆位元組跳動青訓營

語言: CN / TW / HK

第五屆位元組跳動青訓營講師非常用心給大家整理了課前、中、後的學習內容,同學們自我評估,選擇性查漏補缺,便於大家更好的跟上講師們的節奏,祝大家學習愉快,多多提問交流~

Go 語言基礎 - 基礎語法

概述

本節課程主要分為四個方面:

  1. Go 語言簡介
  2. Go 語言開發入門,包括開發環境配置、基礎語法、標準庫
  3. Go 實戰,包括三個實戰專案

課前部分主要羅列課程中涉及到的概念。對於不熟悉的概念,同學們可以提前查詢預習;課中部分主要羅列每一部分的關鍵思路,幫助同學們跟上課程的進度;課後部分是一些問題,幫助同學們在課後梳理本課程的重點。

課前 (必須)

安裝 Go 語言

  1. 訪問 https://go.dev/ ,點選 Download ,下載對應平臺安裝包,安裝即可
  2. 如果無法訪問上述網址,可以改為訪問 https://studygolang.com/dl 下載安裝
  3. 如果訪問 github 速度比較慢,建議配置 go mod proxy,參考 https://goproxy.cn/ 裡面的描述配置,下載第三方依賴包的速度可以大大加快

配置 Go 語言開發環境

可以選擇安裝 VS Code , 或者 Goland ,對於 VS Code,需要安裝 Go 外掛

下載課程示例程式碼

  1. Windows 平臺建議安裝 git,其它系統自帶,安裝教程
  2. 開啟 https://github.com/wangkechun/go-by-example 克隆課程示例專案
  3. 進入課程示例專案程式碼目錄,執行 go run example/01-hello/main.go 如果正確輸出 hello world,則說明環境配置正確

【可選】 學習 Go 語言基礎語法

空餘時間閱讀 Go語言聖經(中文版)

課後

Go 語言學習路線圖

Go 語言進階 - 工程進階

概述

本節課程主要分為四個方面:

  1. 併發程式設計
  2. 依賴管理
  3. 單元測試
  4. 專案實戰

詳述

  • 羅列課程中涉及到的概念和相關資料,對於不熟悉的知識點,希望同學們可以提前查詢預習,屆時跟上直播課程進度。
  • 【必須】課程內容相關程式碼連結:https://github.com/Moonlight-Zhao/go-project-example/tree/V0

併發程式設計

  • 協程Goroutine
  • 通道Channel
  • 鎖Lock https://pkg.go.dev/sync
  • 執行緒同步WaitGroup https://pkg.go.dev/sync

屬於程式設計進階內容,考慮到工程專案的可用性和可靠性,工程實踐中經常會用到。

依賴管理

  • Gopath
  • Go Vendor
  • Go Module : https://go.dev/blog/using-go-modules

瞭解Go依賴管理演進的歷程,通過課程學習以及課後實踐能能夠熟練使用go module 管理依賴。

單元測試

  • 單元測試概念和規則:https://go.dev/doc/tutorial/add-a-test;https://pkg.go.dev/testing
  • Mock測試:https://github.com/bouk/monkey
  • 基準測試:https://pkg.go.dev/testing#hdr-Benchmarks

專案實戰

需求模型來源

青訓營話題頁https://forum.juejin.cn/youthcamp/post/7081211487762513928?from=1

需求

  1. 實現一個展示話題(標題,文字描述)和回帖列表的後端http介面;
  2. 本地檔案儲存資料

元件及技術點

  • web框架:Gin - https://github.com/gin-gonic/gin#quick-start

    • 瞭解go web框架的簡單使用
  • 分層結構設計:https://github.com/bxcodec/go-clean-arch

    • 瞭解分層設計的概念
  • 檔案操作:讀檔案https://pkg.go.dev/io

  • 資料查詢:索引https://www.baike.com/wikiid/5527083834876297305?prd=result_list&view_id=5di0ak8h3ag000

課後實踐

  1. 支援對話題釋出回帖。
  2. 回帖id生成需要保證不重複、唯一性。
  3. 新加回帖追加到本地檔案,同時需要更新索引,注意Map的併發安全問題

Go 框架三件套詳解(Web/RPC/ORM)

環境搭建部分

搭建課程所需要的開發環境以及安裝需要用到的軟體。

學習如何安裝 Docker/Postman/Git/Golang

  • 安裝 Minikube 或 Docker Desktop 用於使用 Docker 安裝教程

    • 可以使用 Minikube 或者使用 Docker Desktop 啟動 Docker
  • 安裝 Postman

  • 安裝 Git 安裝教程

  • 安裝 Go(Golang >= 1.15) 安裝教程

框架體驗部分

提前體驗一下課程涉及的 HTTP/RPC/ORM 框架

HTTP 框架 Hertz 初體驗

通過閱讀 https://www.cloudwego.io/zh/docs/hertz/getting-started/ 嘗試執行 Hertz 的示例程式碼

  • Hertz 框架地址: https://github.com/cloudwego/hertz

RPC 框架 Kitex 初體驗

通過閱讀 https://www.cloudwego.io/zh/docs/kitex/getting-started/ 嘗試執行 Kitex 的示例程式碼

  • kitex 暫時沒有針對 Windows 做支援,如果本地開發環境是 Windows 建議使用 WSL2
  • KItex 框架地址: https://github.com/cloudwego/kitex/

ORM 框架 Gorm 初體驗

通過閱讀 https://gorm.cn/docs/#Install 嘗試執行 Gorm 的示例程式碼

  • Gorm 框架地址: https://github.com/go-gorm/gorm

其它知識

  • 瞭解一下什麼IDL以及IDL的語法

  • 瞭解一下什麼是 opentracing 以及 etcd

Etcd 與 Opentracing 是什麼

IDL 是什麼

  • 瞭解 IDL 是什麼 https://zh.m.wikipedia.org/zh-hans/%E6%8E%A5%E5%8F%A3%E6%8F%8F%E8%BF%B0%E8%AF%AD%E8%A8%80

  • Thrift IDL 語法 https://thrift.apache.org/docs/idl

  • proto3 IDL 語法 https://developers.google.com/protocol-buffers/docs/proto3

高質量程式設計與效能調優實戰

課程概述

  • 介紹編碼規範,幫助大家寫出高質量程式
  • 介紹 Go 語言的效能優化建議,分析對比不同方式對效能的影響和背後的原理
  • 講解常用效能分析工具 pprof 的使用和工作原理,熟悉排查程式效能問題的基本流程
  • 分析效能調優實際案例,介紹實際效能調優時的工作內容

課前

  • 課程內容概要

image.png

實踐準備 (必須)

  • 克隆 https://github.com/wolfogre/go-pprof-practice 到本地,保證能夠編譯執行
  • 嘗試使用 test 命令,編寫並執行簡單測試 https://go.dev/doc/tutorial/add-a-test
  • 嘗試使用 -bench 引數,對編寫的函式進行效能測試,https://pkg.go.dev/testing#hdr-Benchmarks

推薦閱讀

  • Go 程式碼 Review 建議https://github.com/golang/go/wiki/CodeReviewComments
  • Uber 的 Go 編碼規範,https://github.com/uber-go/guide

課中

高質量程式設計

簡介

  • 編寫的程式碼能夠達到正確可靠、簡潔清晰、無效能隱患的目標就能稱之為高質量程式碼
  • 實際應用場景千變萬化,各種語言的特性和語法各不相同,但是高質量程式設計遵循的原則是相通的
  • 高質量的程式設計需要注意以下原則:簡單性、可讀性、生產力

常見編碼規範

程式碼格式
  • 使用 gofmt 自動格式化程式碼,保證所有的 Go 程式碼與官方推薦格式保持一致

總結

  • 提升可讀性,風格一致的程式碼更容易維護、需要更少的學習成本、團隊合作成本,同時可以降低 Review 成本
註釋
  • 註釋應該解釋程式碼作用

    • 適合註釋公共符號,https://github.com/golang/go/blob/master/src/os/file.go#L313
  • 註釋應該解釋程式碼如何做的

    • 適合註釋方法,https://github.com/golang/go/blob/master/src/net/http/client.go#L678
  • 註釋應該解釋程式碼實現的原因

    • 解釋程式碼的外部因素,https://github.com/golang/go/blob/master/src/net/http/client.go#L521
  • 註釋應該解釋程式碼什麼情況會出錯
  • 公共符號始終要註釋

    • 包中宣告的每個公共的符號:變數、常量、函式以及結構都需要添加註釋
    • https://github.com/golang/go/blob/master/src/io/io.go#L638
    • https://github.com/golang/go/blob/master/src/io/io.go#L455

總結

  • 程式碼是最好的註釋
  • 註釋應該提供程式碼未表達出的上下文資訊
命名規範
  • variable

    • 簡潔勝於冗長
    • 縮略詞全大寫,但當其位於變數開頭且不需要匯出時,使用全小寫
    • 變數距離其被使用的地方越遠,則需要攜帶越多的上下文資訊
    • 全域性變數在其名字中需要更多的上下文資訊,使得在不同地方可以輕易辨認出其含義
  • function

    • 函式名不攜帶包名的上下文資訊,因為包名和函式名總是成對出現的
    • 函式名儘量簡短
    • 當名為 foo 的包某個函式返回型別 Foo 時,可以省略型別資訊而不導致歧義
    • 當名為 foo 的包某個函式返回型別 T 時(T 並不是 Foo),可以在函式名中加入型別資訊
  • package

    • 只由小寫字母組成。不包含大寫字母和下劃線等字元
    • 簡短幷包含一定的上下文資訊。例如 schema、task 等
    • 不要與標準庫同名。例如不要使用 sync 或者 strings

總結

  • 關於命名的大多數規範核心在於考慮上下文
  • 人們在閱讀理解程式碼的時候也可以看成是計算機執行程式,好的命名能讓人把關注點留在主流程上,清晰地理解程式的功能,避免頻繁切換到分支細節,增加理解成本
控制流程
  • 避免巢狀,保持正常流程清晰
  • 如果兩個分支中都包含 return 語句,則可以去除冗餘的 else
  • 儘量保持正常程式碼路徑為最小縮排,優先處理錯誤情況/特殊情況,並儘早返回或繼續迴圈來減少巢狀,增加可讀性

    • Go 公共庫的程式碼
    • https://github.com/golang/go/blob/master/src/bufio/bufio.go#L277

總結

  • 線性原理,處理邏輯儘量走直線,避免複雜的巢狀分支
  • 提高程式碼的可讀性
錯誤和異常處理
  • 簡單錯誤處理

    • 優先使用 errors.New 來建立匿名變數來直接表示該錯誤。有格式化需求時使用 fmt.Errorf
    • https://github.com/golang/go/blob/master/src/net/http/client.go#L802
  • 錯誤的 Wrap 和 Unwrap

    • 在 fmt.Errorf 中使用 %w 關鍵字來將一個錯誤 wrap 至其錯誤鏈中
    • https://github.com/golang/go/blob/master/src/cmd/go/internal/work/exec.go#L983
    • Go1.13 在 errors 中新增了三個新 API 和一個新的 format 關鍵字,分別是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果專案執行在小於 Go1.13 的版本中,匯入 golang.org/x/xerrors 來使用。以下語法均已 Go1.13 作為標準。

  • 錯誤判定

    • 使用 errors.Is 可以判定錯誤鏈上的所有錯誤是否含有特定的錯誤。
    • https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/sumdb.go#L208
    • 在錯誤鏈上獲取特定種類的錯誤,使用 errors.As
    • https://github.com/golang/go/blob/master/src/errors/wrap_test.go#L255
  • panic

    • 不建議在業務程式碼中使用 panic
    • 如果當前 goroutine 中所有 deferred 函式都不包含 recover 就會造成整個程式崩潰
    • 當程式啟動階段發生不可逆轉的錯誤時,可以在 init 或 main 函式中使用 panic
    • https://github.com/Shopify/sarama/blob/main/examples/consumergroup/main.go#L94
  • recover

    • recover 只能在被 defer 的函式中使用,巢狀無法生效,只在當前 goroutine 生效
    • https://github.com/golang/go/blob/master/src/fmt/scan.go#L247
    • 如果需要更多的上下文資訊,可以 recover 後在 log 中記錄當前的呼叫棧。
    • https://github.com/golang/website/blob/master/internal/gitfs/fs.go#L228

總結

  • panic 用於真正異常的情況
  • error 儘可能提供簡明的上下文資訊,方便定位問題
  • recover 生效範圍,在當前 goroutine 的被 defer 的函式中生效

效能優化建議

  • 在滿足正確性、可靠性、健壯性、可讀性等質量因素的前提下,設法提高程式的效率
  • 效能對比測試程式碼,可參考 https://github.com/RaymondCode/go-practice
  • slice 預分配記憶體
    • 在儘可能的情況下,在使用 make() 初始化切片時提供容量資訊,特別是在追加切片時

    • 原理

      • https://ueokande.github.io/go-slice-tricks/

      • 切片本質是一個數組片段的描述,包括了陣列的指標,這個片段的長度和容量(不改變記憶體分配情況下的最大長度)

      • 切片操作並不複製切片指向的元素,建立一個新的切片會複用原來切片的底層陣列,因此切片操作是非常高效的

      • 切片有三個屬性,指標(ptr)、長度(len) 和容量(cap)。append 時有兩種場景:

        • 當 append 之後的長度小於等於 cap,將會直接利用原底層陣列剩餘的空間
        • 當 append 後的長度大於 cap 時,則會分配一塊更大的區域來容納新的底層陣列
      • 因此,為了避免記憶體發生拷貝,如果能夠知道最終的切片的大小,預先設定 cap 的值能夠獲得最好的效能

    • 另一個陷阱:大記憶體得不到釋放

      • 在已有切片的基礎上進行切片,不會建立新的底層陣列。因為原來的底層陣列沒有發生變化,記憶體會一直佔用,直到沒有變數引用該陣列
      • 因此很可能出現這麼一種情況,原切片由大量的元素構成,但是我們在原切片的基礎上切片,雖然只使用了很小一段,但底層陣列在記憶體中仍然佔據了大量空間,得不到釋放
      • 推薦的做法,使用 copy 替代 re-slice
  • map 預分配記憶體
    • 原理

      • 不斷向 map 中新增元素的操作會觸發 map 的擴容
      • 根據實際需求提前預估好需要的空間
      • 提前分配好空間可以減少記憶體拷貝和 Rehash 的消耗
  • 使用 strings.Builder
    • 常見的字串拼接方式

      • +
      • strings.Builder
      • bytes.Buffer
    • strings.Builder 最快,bytes.Buffer 較快,+ 最慢

    • 原理

      • 字串在 Go 語言中是不可變型別,佔用記憶體大小是固定的,當使用 + 拼接 2 個字串時,生成一個新的字串,那麼就需要開闢一段新的空間,新空間的大小是原來兩個字串的大小之和
      • strings.Builder,bytes.Buffer 的記憶體是以倍數申請的
      • strings.Builder 和 bytes.Buffer 底層都是 []byte 陣列,bytes.Buffer 轉化為字串時重新申請了一塊空間,存放生成的字串變數,而 strings.Builder 直接將底層的 []byte 轉換成了字串型別返回
  • 使用空結構體節省記憶體
    • 空結構體不佔據記憶體空間,可作為佔位符使用

    • 比如實現簡單的 Set

      • Go 語言標準庫沒有提供 Set 的實現,通常使用 map 來代替。對於集合場景,只需要用到 map 的鍵而不需要值
  • 使用 atomic 包
    • 原理

      • 鎖的實現是通過作業系統來實現,屬於系統呼叫,atomic 操作是通過硬體實現的,效率比鎖高很多
      • sync.Mutex 應該用來保護一段邏輯,不僅僅用於保護一個變數
      • 對於非數值系列,可以使用 atomic.Value,atomic.Value 能承載一個 interface{}
總結
  • 避免常見的效能陷阱可以保證大部分程式的效能
  • 針對普通應用程式碼,不要一味地追求程式的效能,應當在滿足正確可靠、簡潔清晰等質量要求的前提下提高程式效能

效能調優實戰

效能調優簡介

  • 效能調優原則

    • 要依靠資料不是猜測
    • 要定位最大瓶頸而不是細枝末節
    • 不要過早優化
    • 不要過度優化

效能分析工具

效能調優的核心是效能瓶頸的分析,對於 Go 應用程式,最方便的就是 pprof 工具

  • pprof 功能說明
    • pprof 是用於視覺化和分析效能分析資料的工具
    • 可以知道應用在什麼地方耗費了多少 CPU、memory 等執行指標
  • pprof 實踐

    • https://github.com/wolfogre/go-pprof-practice

    • 前置準備,熟悉簡單指標,能夠編譯執行 pprof 測試專案

    • 實際分析排查過程

      • 排查 CPU 問題

        • 命令列分析

          • go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
        • top 命令

        • list 命令

        • 熟悉 web 頁面分析

        • 呼叫關係圖,火焰圖

        • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"

      • 排查堆記憶體問題

        • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
      • 排查協程問題

        • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
      • 排查鎖問題

        • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
      • 排查阻塞問題

        • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
  • pprof 的取樣過程和原理
    • CPU 取樣
    • 堆記憶體取樣
    • 協程和系統執行緒取樣
    • 阻塞操作和鎖競爭取樣

效能調優案例

  • 基本概念
    • 服務:能單獨部署,承載一定功能的程式
    • 依賴:Service A 的功能實現依賴 Service B 的響應結果,稱為 Service A 依賴 Service B
    • 呼叫鏈路:能支援一個介面請求的相關服務集合及其相互之間的依賴關係
    • 基礎庫:公共的工具包、中介軟體
  • 業務優化
    • 流程

      • 建立服務效能評估手段
      • 分析效能資料,定位效能瓶頸
      • 重點優化項改造
      • 優化效果驗證
    • 建立壓測評估鏈路

      • 服務效能評估
      • 構造請求流量
      • 壓測範圍
      • 效能資料採集
    • 分析效能火焰圖,定位效能瓶頸

      • pprof 火焰圖
    • 重點優化項分析

      • 規範元件庫使用
      • 高併發場景優化
      • 增加程式碼檢查規則避免增量劣化出現
      • 優化正確性驗證
    • 上線驗證評估

      • 逐步放量,避免出現問題
    • 進一步優化,服務整體鏈路分析

      • 規範上游服務呼叫介面,明確場景需求
      • 分析業務流程,通過業務流程優化提升服務效能
  • 基礎庫優化
    • 適應範圍更廣,覆蓋更多服務

    • AB 實驗 SDK 的優化

      • 分析基礎庫核心邏輯和效能瓶頸
      • 完善改造方案,按需獲取,序列化協議優化
      • 內部壓測驗證
      • 推廣業務服務落地驗證
  • Go 語言優化
    • 適應範圍最廣,Go 服務都有收益

    • 優化方式

      • 優化記憶體分配策略
      • 優化程式碼編譯流程,生成更高效的程式
      • 內部壓測驗證
      • 推廣業務服務落地驗證

課後

  • 瞭解下其他語言的編碼規範,是否和 Go 語言編碼規範有相通之處,注重理解哪些共同點
  • 編碼規範或者效能優化建議大部分是通用的,有沒有方式能夠自動化對程式碼進行檢測?
  • 從 https://github.com/golang/go/tree/master/src 中選擇感興趣的包,看看官方程式碼是如何編寫的
  • 使用 Go 進行併發程式設計時有哪些效能陷阱或者優化手段?
  • 在真實的線上環境中,每個場景或者服務遇到的效能問題也是各種各樣,搜尋下知名公司的官方公眾號或者部落格,裡面有哪些效能優化的案例?比如 https://eng.uber.com/category/oss-projects/oss-go/
  • Go 語言本身在持續更新迭代,每個版本在效能上有哪些重要的優化點?

參考資料

  • 熟悉 Go 語言基礎後的必讀內容,https://go.dev/doc/effective_go
  • Dave Cheney 關於 Go 語言程式設計實踐的演講記錄,https://dave.cheney.net/practical-go/presentations/qcon-china.html
  • 《程式設計的原則:改善程式碼質量的101個方法》,總結了很多程式設計原則,按照是什麼 -> 為什麼 -> 怎麼做進行了說明,https://mp.weixin.qq.com/s/vXSZOl2Gt7wcgq1OL9Cwow
  • 如何編寫整潔的 Go 程式碼,https://github.com/Pungyeon/clean-go-article
  • Go 官方部落格,有關於 Go 的最新進展,https://go.dev/blog/
  • Dave Cheney 關於 Go 語言程式設計高效能程式設計的介紹,https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html
  • Go 語言高效能程式設計,博主總結了 Go 程式設計的一些效能建議, https://geektutu.com/post/high-performance-go.html
  • Google 其他程式語言編碼規範,可以對照參考,https://zh-google-styleguide.readthedocs.io/en/latest/