
こんにちは。よっしーです(^^)
本日は、Go言語を効果的に使うためのガイドラインについて解説しています。
背景
Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。
ゴルーチン
これらはゴルーチンと呼ばれています。なぜなら、既存の用語—スレッド、コルーチン、プロセスなど—は不正確な含意を伝えるからです。ゴルーチンには単純なモデルがあります:それは、同じアドレス空間内で他のゴルーチンと並行して実行される関数です。それは軽量で、スタック空間の割り当て以上のコストはほとんどかかりません。そして、スタックは小さく始まるため、安価であり、必要に応じてヒープストレージを割り当て(そして解放し)ることで成長します。
ゴルーチンは複数のOSスレッドに多重化されるため、1つがI/Oを待つなどでブロックしても、他は実行を続けます。その設計は、スレッドの作成と管理の複雑さの多くを隠します。
関数またはメソッドの呼び出しにgo
キーワードを前置すると、新しいゴルーチンでその呼び出しが実行されます。呼び出しが完了すると、ゴルーチンは静かに終了します。(効果は、コマンドをバックグラウンドで実行するためのUnixシェルの&
記法に似ています。)
go list.Sort() // list.Sortを並行実行;完了を待たない。
関数リテラルはゴルーチンの呼び出しで便利です。
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // 括弧に注意 - 関数を呼び出す必要があります。
}
Goでは、関数リテラルはクロージャです:実装は、関数によって参照される変数が、それらがアクティブである限り生き残ることを確実にします。
これらの例は、関数が完了を通知する方法がないため、あまり実用的ではありません。そのためには、チャンネルが必要です。
ゴルーチンとは?
ゴルーチンは、Goにおける軽量な並行実行の仕組みです。従来のスレッドよりもはるかに軽量で、簡単に使用できます。
基本的な使用法
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello, %s! (%d)\n", name, i+1)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
fmt.Println("=== 基本的なゴルーチンの例 ===")
// 通常の関数呼び出し(同期実行)
fmt.Println("同期実行:")
sayHello("Alice")
fmt.Println("\n並行実行:")
// ゴルーチンでの実行(非同期実行)
go sayHello("Bob")
go sayHello("Charlie")
// メインゴルーチンが終了する前に少し待つ
time.Sleep(2 * time.Second)
fmt.Println("メイン終了")
}
関数リテラル(無名関数)との組み合わせ
package main
import (
"fmt"
"time"
)
func Announce(message string, delay time.Duration) {
// 無名関数をゴルーチンで実行
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // () で関数を実際に呼び出し
}
func main() {
fmt.Println("=== 関数リテラルの例 ===")
// 複数のアナウンスを並行実行
Announce("メッセージ1(1秒後)", 1*time.Second)
Announce("メッセージ2(2秒後)", 2*time.Second)
Announce("メッセージ3(500ms後)", 500*time.Millisecond)
// 全てのアナウンスが表示されるまで待機
time.Sleep(3 * time.Second)
fmt.Println("全アナウンス完了")
}
クロージャの特性
package main
import (
"fmt"
"time"
)
func closureExample() {
fmt.Println("=== クロージャの例 ===")
message := "外部変数"
counter := 0
// クロージャ:外部の変数にアクセス可能
for i := 0; i < 3; i++ {
go func(id int) {
counter++ // 外部変数にアクセス
fmt.Printf("ゴルーチン %d: %s, カウンター=%d\n", id, message, counter)
}(i)
time.Sleep(100 * time.Millisecond)
}
time.Sleep(1 * time.Second)
fmt.Printf("最終カウンター: %d\n", counter)
}
func main() {
closureExample()
}
実用的な例:並行ダウンロード
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func fetchURL(url string, result chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
result <- fmt.Sprintf("エラー %s: %v", url, err)
return
}
defer resp.Body.Close()
// レスポンスサイズを取得
body, err := io.ReadAll(resp.Body)
if err != nil {
result <- fmt.Sprintf("読み込みエラー %s: %v", url, err)
return
}
duration := time.Since(start)
result <- fmt.Sprintf("完了 %s: %d bytes, %v", url, len(body), duration)
}
func concurrentDownloadExample() {
fmt.Println("=== 並行ダウンロードの例 ===")
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/json",
"https://httpbin.org/status/200",
}
results := make(chan string, len(urls))
// 全URLを並行でダウンロード
for _, url := range urls {
go fetchURL(url, results)
}
// 結果を受信
for i := 0; i < len(urls); i++ {
fmt.Println(<-results)
}
}
func main() {
concurrentDownloadExample()
}
ワーカープールパターン
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Task struct {
ID int
Data string
}
type Result struct {
TaskID int
Output string
Worker int
}
func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
// タスク処理をシミュレート
processingTime := time.Duration(rand.Intn(1000)) * time.Millisecond
time.Sleep(processingTime)
result := Result{
TaskID: task.ID,
Output: fmt.Sprintf("処理済み: %s", task.Data),
Worker: id,
}
results <- result
fmt.Printf("ワーカー %d: タスク %d 完了\n", id, task.ID)
}
}
func workerPoolExample() {
fmt.Println("=== ワーカープールの例 ===")
const numWorkers = 3
const numTasks = 10
tasks := make(chan Task, numTasks)
results := make(chan Result, numTasks)
var wg sync.WaitGroup
// ワーカーを開始
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, tasks, results, &wg)
}
// タスクを送信
go func() {
for i := 1; i <= numTasks; i++ {
tasks <- Task{
ID: i,
Data: fmt.Sprintf("データ-%d", i),
}
}
close(tasks)
}()
// 結果収集
go func() {
wg.Wait()
close(results)
}()
// 結果を表示
for result := range results {
fmt.Printf("結果: %+v\n", result)
}
fmt.Println("全タスク完了")
}
func main() {
workerPoolExample()
}
生産者・消費者パターン
package main
import (
"fmt"
"sync"
"time"
)
func producer(name string, data chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
defer close(data)
for i := 1; i <= 5; i++ {
value := i * 10
data <- value
fmt.Printf("Producer %s: %d を生産\n", name, value)
time.Sleep(200 * time.Millisecond)
}
fmt.Printf("Producer %s 完了\n", name)
}
func consumer(name string, data <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for value := range data {
// データ処理をシミュレート
time.Sleep(300 * time.Millisecond)
fmt.Printf("Consumer %s: %d を消費\n", name, value)
}
fmt.Printf("Consumer %s 完了\n", name)
}
func producerConsumerExample() {
fmt.Println("=== 生産者・消費者の例 ===")
data := make(chan int, 3) // バッファ付きチャンネル
var producerWg sync.WaitGroup
var consumerWg sync.WaitGroup
// 生産者を開始
producerWg.Add(1)
go producer("P1", data, &producerWg)
// 消費者を開始
consumerWg.Add(2)
go consumer("C1", data, &consumerWg)
go consumer("C2", data, &consumerWg)
// 生産完了を待ってから消費完了を待つ
producerWg.Wait()
consumerWg.Wait()
fmt.Println("全体完了")
}
func main() {
producerConsumerExample()
}
タイムアウト処理
package main
import (
"fmt"
"time"
)
func longRunningTask(id int, duration time.Duration, result chan<- string) {
time.Sleep(duration)
result <- fmt.Sprintf("タスク %d 完了 (%v)", id, duration)
}
func timeoutExample() {
fmt.Println("=== タイムアウト処理の例 ===")
result := make(chan string, 1)
timeout := 2 * time.Second
// 長時間実行されるタスクを開始
go longRunningTask(1, 3*time.Second, result)
select {
case res := <-result:
fmt.Println("結果:", res)
case <-time.After(timeout):
fmt.Printf("タイムアウト: %v 以内に完了しませんでした\n", timeout)
}
// 短時間で完了するタスク
go longRunningTask(2, 1*time.Second, result)
select {
case res := <-result:
fmt.Println("結果:", res)
case <-time.After(timeout):
fmt.Printf("タイムアウト: %v 以内に完了しませんでした\n", timeout)
}
}
func main() {
timeoutExample()
}
実際の問題を解決する例:ファイル並行処理
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type FileInfo struct {
Name string
Size int
}
func processFile(file FileInfo, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
// ファイル処理をシミュレート(サイズに応じて処理時間が変わる)
processingTime := time.Duration(file.Size/100) * time.Millisecond
time.Sleep(processingTime)
results <- fmt.Sprintf("処理完了: %s (%d bytes, %v)",
file.Name, file.Size, processingTime)
}
func fileProcessingExample() {
fmt.Println("=== ファイル並行処理の例 ===")
// テスト用のファイルリスト
files := []FileInfo{
{"document1.txt", rand.Intn(1000) + 100},
{"image1.jpg", rand.Intn(2000) + 500},
{"video1.mp4", rand.Intn(5000) + 1000},
{"document2.txt", rand.Intn(1000) + 100},
{"image2.jpg", rand.Intn(2000) + 500},
}
results := make(chan string, len(files))
var wg sync.WaitGroup
start := time.Now()
// 全ファイルを並行処理
for _, file := range files {
wg.Add(1)
go processFile(file, results, &wg)
}
// 完了待ちとチャンネルクローズ
go func() {
wg.Wait()
close(results)
}()
// 結果を受信
for result := range results {
fmt.Println(result)
}
totalTime := time.Since(start)
fmt.Printf("全ファイル処理完了: %v\n", totalTime)
}
func main() {
fileProcessingExample()
}
重要なポイント
1. ゴルーチンの特徴
- 軽量: スレッドよりもはるかに少ないメモリ使用量
- 簡単:
go
キーワードを付けるだけで並行実行 - 効率的: OSスレッドに多重化されて実行
2. 基本的な使用法
go functionName() // 関数を並行実行
go func() { ... }() // 無名関数を並行実行
3. メモリ管理
- 小さなスタック: 最初は2KB程度で開始
- 動的拡張: 必要に応じて自動的に拡張
- ガベージコレクション: 不要になったら自動回収
4. クロージャの活用
- 外部変数にアクセス可能
- 変数の寿命がゴルーチンと連動
- 関数リテラルで柔軟な処理が可能
5. 注意点
- メインゴルーチン終了: メインが終わると全ゴルーチンも終了
- 完了通知: チャンネルやWaitGroupで同期が必要
- 競合状態: 共有変数アクセスには注意
6. 実用的なパターン
- ワーカープール: 限られた数のワーカーでタスク処理
- 生産者・消費者: データの生成と処理を分離
- タイムアウト: 長時間処理の制限
- 並行ダウンロード: ネットワーク処理の並行化
ゴルーチンは、Goの並行性を支える核心技術です。従来のスレッドプログラミングの複雑さを大幅に軽減し、直感的で効率的な並行プログラムを書くことができます。
おわりに
本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント