並行プログラミング#
-
並行 はマルチスレッドプログラムが 1 つのコアの CPU 上で実行されること
-
並列 はマルチスレッドプログラムが複数のコア上で実行されること
-
Go はマルチコアの利点を最大限に活用し、高効率で実行できる
重要な概念
ゴルーチン#
- ゴルーチンのオーバーヘッドはスレッドよりも小さく、軽量スレッドと考えることができ、1 つの Go プログラムで数万のゴルーチンを作成できます。
Go でゴルーチンを開始するのは非常に簡単で、関数の前にgo
キーワードを追加するだけで関数にゴルーチンを開始できます。
CSP とチャネル#
CSP(Communicating Sequential Process)
Go では通信によるメモリ共有を推奨し、共有メモリによる通信を実現します。
では、どうやって通信するのか?それはチャネル
を通じてです。
チャネル#
構文:make(chan 要素タイプ, [バッファサイズ])
- バッファなしチャネル
make(chan int)
- バッファありチャネル
make(chan int, 2)
この図は非常に生き生きとしています~
以下は一例です:
- 最初のゴルーチンは生産者として
0~9
をsrc
に送信します - 2 番目のゴルーチンは消費者として
src
内の各数の平方を計算し、dest
に送信します - メインスレッドは
dest
内の各数を出力します
package main
func CalSquare() {
src := make(chan int) // 生産者
dest := make(chan int, 3) // 消費者 バッファ付きで生産者が速すぎる問題を解決
go func() { // このスレッドは0~9をsrcに送信します
defer close(src) // deferは関数の終了時に実行されることを示し、割り当てられたリソースを解放します。
for i := 0; i < 10; i++ {
// <- 演算子 左側はデータを収集する側、右側は送信するデータ
src <- i
}
}() // 即時実行
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
// 他の複雑な操作
println(i)
}
}
func main() {
CalSquare()
}
毎回順序通りに出力されることがわかります。これは Go が並行安全であることを示しています。
Go 言語は共有メモリの手法も保持しており、sync を使用して同期を行います。以下のように
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithLock() { // xを2000に加算するため、ロックを使用すると安全です
for i := 0; i < 2000; i++ {
lock.Lock() // ロック
x += 3
x -= 2
lock.Unlock() // アンロック
}
}
func addWithoutLock() { // ロックを使用しない
for i := 0; i < 2000; i++ {
x += 3
x -= 2
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second) // 1秒間スリープ
println("WithoutLock x =", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second) // 1秒間スリープ
println("WithLock x =", x)
}
func main() {
Add()
}
ps:何度も試しましたが衝突はありませんでした。計算を少し複雑にすると衝突が発生します。
依存管理#
大規模プロジェクトの開発では依存管理を避けることはできません。Go の依存は主に GOPATH -> Go Vendor -> Go Module の進化を経て、現在は主に Go Module の方式が採用されています。
- 環境ごとに依存のバージョンが異なるため、依存ライブラリのバージョンをどう制御するか?
GOPATH#
- プロジェクトコードは直接 src 下のコードに依存します
go get
を使用して最新バージョンのパッケージを src ディレクトリにダウンロードします
このようにすると、1 つの問題が発生します:複数バージョンの制御ができない(A、B が同じパッケージの異なるバージョンに依存する場合、困ります)
Go Vendor#
- プロジェクトディレクトリに新たに
vendor
フォルダを作成し、すべての依存パッケージをコピー形式でその中に置きます - vendor => GOPATH の方式で曲線的に救済します
ps:前端の package.json に似ていると感じます…… 依存の問題は本当に避けられません。
これにより新たな問題が発生しました:
- 依存のバージョンを制御できない
- プロジェクトを更新する際に依存の衝突が発生し、コンパイルエラーを引き起こす可能性があります。
Go Module#
go.mod
ファイルを使用して依存パッケージのバージョンを管理しますgo get/go mod
コマンドツールを使用して依存パッケージを管理します
最終目標を達成しました:バージョンルールを定義でき、プロジェクトの依存関係を管理できます。
Java の Maven に例えることができます。
依存設定 go.mod
#
依存識別構文:モジュールパス + バージョンで一意に識別します。
[モジュールパス][バージョン/擬似バージョン]
module example/project/app 依存管理の基本単位
go 1.16 標準ライブラリ
require ( 単位依存
example/lib1 v1.0.2
example/lib2 v1.0.0 // indirect
example/lib3 v0.1.0-20190725025543-5a5fe074e612
example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect
example/lib5/v3 v3.0.2
example/lib6 v3.2.0+incompatible
)
上記の点に注意が必要です:
- 主バージョン 2 以上のモジュールはパスの後に /vN サフィックスが追加されます
- go.mod ファイルがない主バージョン 2 以上の依存は
+incompatible
となります。
依存のバージョンルールはセマンティックバージョンとコミットベースの擬似バージョンに分かれます。
セマンティックバージョン#
形式は:${MAJOR}.${MINOR}.${PATCH}
V1.3.0、V2.3.0、 ……
- 異なる
MAJOR
バージョンは互換性のない APIを示します。- 同じライブラリであっても、MAJOR バージョンが異なると異なるモジュールと見なされます。
MINOR
バージョンは通常新しい関数や機能を追加し、後方互換性があります。PATCH
バージョンは一般的にバグを修正します。
コミットベースのバージョン#
形式は:${vx.0.0-yyyymmddhhmmss-abcdefgh1234}
- バージョンプレフィックスはセマンティックバージョンと同じです。
- タイムスタンプ(
yyyymmddhhmmss
)、つまりコミットの時間です。 - チェックサム(
abcdefgh1234
)、12 桁のハッシュプレフィックスです。- 各コミット後、Go は自動的に擬似バージョン番号を生成します。
小テスト#
- X プロジェクトが A、B の 2 つのプロジェクトに依存し、A、B がそれぞれ C プロジェクトの v1.3、v1.4 の 2 つのバージョンに依存している場合、依存図は上記の通り、最終的にコンパイル時に使用される C プロジェクトのバージョンは []{.gap} ? {.quiz}
- v1.3
- v1.4 {.correct}
- A が c を使用する際に v1.3 でコンパイルし、B が c を使用する際に v1.4 でコンパイルします
{.options}
答えは:B が最低の互換バージョンを選択します
これは Go のバージョン選択アルゴリズムで、最低の互換バージョンを選択し、1.4 バージョンは 1.3 に下位互換性があります(セマンティックバージョン)。なぜ 1.3 を選ばないのか?それは上位互換性がないからです。もし 1.5 があれば、1.4 が要求を満たす最低の互換バージョンだから選ばれません。
依存配布#
これらの依存はどこからダウンロードされるのでしょうか?それが依存配布です。
GitHub などのコードホスティングシステムの対応するリポジトリからダウンロードしますか?
GitHub は一般的なコードホスティングシステムプラットフォームですが、Go Modules
システムで定義された依存は、最終的に特定のコミットまたはバージョンに対応する多バージョンコード管理システムのプロジェクトに対応します。
go.mod
で定義された依存は、対応するリポジトリから指定されたソフトウェア依存をダウンロードすることで、依存配布を完了します。
問題もあります:
- ビルドの確定性を保証できない
- ソフトウェア作者がソフトウェアバージョンを直接変更し、次回のビルドで他のバージョンの依存を使用するか、依存バージョンが見つからないことがあります。
- 依存の可用性を保証できない
- ソフトウェア作者がコードプラットフォームからソフトウェアを削除し、依存が利用できなくなることがあります。
- サードパーティのコードホスティングプラットフォームへの負担が増加します。
これらの問題を解決するために Proxy 方式を使用します。
Go Proxy
は、ソースサイトのソフトウェアコンテンツをキャッシュするサービスサイトです。キャッシュされたソフトウェアバージョンは変更されず、ソースサイトのソフトウェアが削除された後でも利用可能です。
Go Proxy を使用すると、ビルド時に直接 Go Proxy サイトから依存を取得します。
Go Modules は **GOPROXY
環境変数 ** を使用して Go Proxy の使用方法を制御します。
サービスサイト URL リスト、direct はソースサイトを示します:GOPROXY="https://proxy1.cn, https://proxy2.cn,direct"
- GOPROXY はGo Proxy サイトの URL リストで、
direct
を使用してソースサイトを示すことができます。全体の依存尋ねるパスは、最初にproxy1
から依存をダウンロードし、proxy1
が存在しない場合はproxy2
を探し、proxy2
も存在しない場合はソースに戻って直接依存をダウンロードし、proxy
サイトにキャッシュします。
ツール#
go get example.org/pkg
サフィックス | 意味 |
---|---|
@update | デフォルト |
@none | 依存を削除 |
@v1.1.2 | タグバージョン、セマンティックバージョン |
@23dfdd5 | 特定のコミット |
master | ブランチの最新コミット |
go mod
サフィックス | 意味 |
---|---|
init | 初期化、go.mod ファイルを作成 |
download | モジュールをローカルキャッシュにダウンロード |
tidy | 必要な依存を追加し、不必要な依存を削除 |
go mod tidy は、コードをコミットする前に実行すると、プロジェクト全体のビルド時間を短縮できます。 |
テスト#
テストは一般的に回帰テスト、統合テスト、単体テストに分かれ、前から後にカバレッジが段階的に増大し、コストは段階的に低下します。したがって、単体テストのカバレッジはコードの質をある程度決定します。
- 回帰テストは一般的に QA の同僚が手動で端末を通じて固定の主流シナリオを回帰します。
- 統合テストはシステム機能の次元でテスト検証を行います。
- 単体テストは開発段階で、開発者が個別の関数やモジュールの機能検証を行います。
単体テストは主に入力、テストユニット、出力、および照合を含みます。
単位の概念は広く、インターフェース、関数、モジュールなどを含み、最終的な照合によってコードの機能が私たちの期待に一致することを保証します。
単体テストには以下の利点があります:
- 品質を保証
- 全体のカバレッジが十分な場合、新機能の正確性を保証し、既存のコードの正確性を損なわない。
- 効率を向上
- コードにバグがある場合、単体テストを通じて、短期間で問題を特定し修正できます。
Go の単体テストには以下のルールがあります:
- すべてのテストファイルは
_test.go
で終わる func TestXxx(testing.T)
- 初期化ロジックは
TestMain
関数に配置(テスト前のデータロード設定、テスト後のリソース解放など)
例:
main.go
package main
func HelloTom() string {
return "Jerry"
}
main_test.go
package main
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expect %s do not match actual %s", expectOutput, output)
}
}
実際のプロジェクトでは、単体テストのカバレッジ
- 一般的なプロジェクトの要求は 50%~60% のカバレッジ
- 重要な資金型サービスの場合、カバレッジは 80% に達することが要求されるかもしれません。
単体テストは安定性と冪等性を保証する必要があります。
- 安定性は相互隔離を指し、任意の時間、任意の環境でテストを実行できること。
- 冪等性は、各テスト実行が以前と同じ結果を生成するべきであることを指します。
この目的を達成するためにmock
メカニズムを使用します。
bouk/monkey: Monkey patching in Go
monkey はオープンソースのモックテストライブラリで、メソッドやインスタンスのメソッドをモックし、リフレクション、ポインタ代入を行います。Mockey Patch の作用域は Runtime にあり、実行時に Go の unsafe パッケージを通じて、メモリ内の関数 A のアドレスを実行時関数 B のアドレスに置き換えることができます。
Go 言語はベンチマークテストフレームワークも提供しています。
- ベンチマークテストは、プログラムの実行性能や CPU の消費度をテストすることを指します。
実際のプロジェクト開発では、コードの性能ボトルネック問題に頻繁に直面し、問題を特定するためにコードの性能分析を行う必要があります。これにはベンチマークテストを使用します。使用方法は単体テストに似ています。
fastrand
が言及されました。アドレス:bytedance/gopkg: Universal Utilities for Go
まとめと感想#
本講義では Go における並行管理、依存設定、テストについて主に説明しました。内容が多いため、しっかり消化する必要があります。後でプロジェクト実践のセクションがありますので、明日実践を行います。
本講義の内容は第 3 回青訓キャンプの赵征先生の講義に基づいています。