はじめに
Go(Golang)は、Googleが開発したシンプルで高速なプログラミング言語です。シンプルな文法、高速なコンパイル、強力な並行処理機能が特徴で、Docker、Kubernetes、Terraformなど多くの有名なツールがGoで書かれています。
この記事では、Goの基礎から実践的な並行処理まで、詳しく解説します。
環境構築
# macOS (Homebrew)
brew install go
# バージョン確認
go version
# 環境変数の確認
go env
# プロジェクトの初期化
mkdir my-project && cd my-project
go mod init github.com/username/my-project
ただ、実務で詰まりやすいのは文法ではなく
- goroutine を増やしすぎて止まらない
- context を渡さずにタイムアウトが効かない
- エラーの握りつぶしで障害解析ができない
- 並行処理の終了条件が曖昧でリークする
といった設計と運用の部分です。
この記事は構文の紹介に加えて
何を方針として固定すべきかの判断軸も先に置きます。
まず押さえる設計判断の軸
並行処理は 目的 から逆算する
- I/O待ちを隠したい(HTTP、DB、外部API)
- バックグラウンドで処理したい(ワーカー)
- スループットを上げたい(並列)
目的が違うと、必要な制御も違います。
goroutine は軽いですが、無限に安全ではありません。
goroutine と channel は終了条件がセット
- goroutine を起動したら、停止の合図を設計する
- channel を作ったら、closeする責任者を決める
ここが曖昧だとリークします。
context は境界で必ず受け取り 必ず渡す
HTTPハンドラ、RPC、ジョブ実行など「境界」から中に入るときに context.Context を最初に受け取り、下層へ渡す習慣を付けると障害対応が楽になります。
エラーは情報を足して返す
- 呼び出し元が判断できる情報(原因、種類)
- ログに出すべき情報(リクエストID、引数の一部)
を足して返すと、調査が速くなります。
最小チェックリスト(レビュー用)
- goroutine の起動に停止条件がある
- channel の close の責務が明確
- context が境界から渡っている
- エラーが握りつぶされていない
- 共有変数を複数goroutineから触っていない(必要なら mutex か channel)
基本文法
Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
# 実行
go run main.go
# ビルド
go build -o myapp main.go
./myapp
変数と定数
package main
import "fmt"
func main() {
// 変数宣言
var name string = "Go"
var age int = 15
var isAwesome bool = true
// 型推論
var city = "Tokyo"
// 短縮宣言(関数内のみ)
country := "Japan"
// 複数変数の宣言
var (
width = 100
height = 200
)
x, y, z := 1, 2, 3
// 定数
const Pi = 3.14159
const (
StatusOK = 200
StatusError = 500
)
// iota(連番定数)
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)
fmt.Println(name, age, isAwesome, city, country)
fmt.Println(width, height, x, y, z)
fmt.Println(Pi, StatusOK)
}
基本型
// 整数
var i int = 42 // プラットフォーム依存(32 or 64bit)
var i8 int8 = 127 // -128 〜 127
var i16 int16 // -32768 〜 32767
var i32 int32 // -2^31 〜 2^31-1
var i64 int64 // -2^63 〜 2^63-1
// 符号なし整数
var u uint = 42
var u8 uint8 = 255 // 0 〜 255 (byte)
var u16 uint16
var u32 uint32
var u64 uint64
// 浮動小数点
var f32 float32 = 3.14
var f64 float64 = 3.14159265359
// 複素数
var c64 complex64 = 1 + 2i
var c128 complex128
// 文字列
var s string = "Hello"
// 文字(rune = int32)
var r rune = 'あ'
// バイト(uint8のエイリアス)
var b byte = 65 // 'A'
// 真偽値
var flag bool = true
ゼロ値
var i int // 0
var f float64 // 0.0
var s string // ""
var b bool // false
var p *int // nil
var sl []int // nil
var m map[string]int // nil
文字列操作
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, World!"
// 長さ
fmt.Println(len(s)) // 13(バイト数)
// 文字列の結合
greeting := "Hello" + " " + "Go"
fmt.Println(greeting)
// strings パッケージ
fmt.Println(strings.ToUpper(s)) // "HELLO, WORLD!"
fmt.Println(strings.ToLower(s)) // "hello, world!"
fmt.Println(strings.Contains(s, "World")) // true
fmt.Println(strings.HasPrefix(s, "Hello")) // true
fmt.Println(strings.HasSuffix(s, "!")) // true
fmt.Println(strings.Index(s, "World")) // 7
fmt.Println(strings.Replace(s, "World", "Go", 1))
fmt.Println(strings.Split(s, ", ")) // ["Hello", "World!"]
fmt.Println(strings.TrimSpace(" hello ")) // "hello"
// 文字列のフォーマット
name := "Alice"
age := 25
message := fmt.Sprintf("Name: %s, Age: %d", name, age)
fmt.Println(message)
// 生文字列(バッククォート)
raw := `Line 1
Line 2
Line 3`
fmt.Println(raw)
}
配列とスライス
package main
import "fmt"
func main() {
// 配列(固定長)
var arr [5]int = [5]int{1, 2, 3, 4, 5}
arr2 := [...]int{1, 2, 3} // 要素数を自動で決定
fmt.Println(arr[0]) // 1
fmt.Println(len(arr)) // 5
// スライス(可変長)
slice := []int{1, 2, 3, 4, 5}
// make でスライス作成
slice2 := make([]int, 5) // 長さ5、容量5
slice3 := make([]int, 0, 10) // 長さ0、容量10
fmt.Println(len(slice3), cap(slice3)) // 0 10
// スライシング
fmt.Println(slice[1:3]) // [2 3]
fmt.Println(slice[:3]) // [1 2 3]
fmt.Println(slice[2:]) // [3 4 5]
// 要素の追加
slice = append(slice, 6, 7)
fmt.Println(slice) // [1 2 3 4 5 6 7]
// スライスの結合
slice4 := []int{8, 9}
slice = append(slice, slice4...)
// コピー
dst := make([]int, len(slice))
copy(dst, slice)
// 要素の削除(i番目)
i := 2
slice = append(slice[:i], slice[i+1:]...)
}
マップ
package main
import "fmt"
func main() {
// マップの宣言
var m map[string]int
m = make(map[string]int)
// リテラルで初期化
scores := map[string]int{
"Alice": 90,
"Bob": 85,
"Carol": 92,
}
// 要素へのアクセス
fmt.Println(scores["Alice"]) // 90
// 要素の追加・更新
scores["Dave"] = 88
scores["Alice"] = 95
// 要素の削除
delete(scores, "Bob")
// 存在確認
value, exists := scores["Eve"]
if exists {
fmt.Println(value)
} else {
fmt.Println("Not found")
}
// 長さ
fmt.Println(len(scores))
// イテレーション
for key, value := range scores {
fmt.Printf("%s: %d\n", key, value)
}
}
制御構文
package main
import "fmt"
func main() {
// if文
x := 10
if x > 5 {
fmt.Println("x > 5")
} else if x > 0 {
fmt.Println("0 < x <= 5")
} else {
fmt.Println("x <= 0")
}
// 初期化文付きif
if y := getValue(); y > 0 {
fmt.Println(y)
}
// for文(Goにはwhileがない)
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// while相当
count := 0
for count < 5 {
fmt.Println(count)
count++
}
// 無限ループ
// for {
// // break で抜ける
// }
// range
slice := []string{"a", "b", "c"}
for index, value := range slice {
fmt.Printf("%d: %s\n", index, value)
}
// インデックスのみ
for i := range slice {
fmt.Println(i)
}
// 値のみ(インデックス無視)
for _, value := range slice {
fmt.Println(value)
}
// switch文
day := "Monday"
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Friday":
fmt.Println("End of the work week")
case "Saturday", "Sunday":
fmt.Println("Weekend")
default:
fmt.Println("Midweek")
}
// 条件式なしのswitch
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}
}
func getValue() int {
return 42
}
関数
package main
import "fmt"
// 基本的な関数
func add(a int, b int) int {
return a + b
}
// 同じ型の引数は省略可能
func multiply(a, b int) int {
return a * b
}
// 複数の戻り値
func divide(a, b int) (int, int) {
return a / b, a % b
}
// 名前付き戻り値
func divideNamed(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // naked return
}
// 可変長引数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 関数を引数に取る
func apply(fn func(int, int) int, a, b int) int {
return fn(a, b)
}
// 関数を返す(クロージャ)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
fmt.Println(add(1, 2)) // 3
fmt.Println(multiply(3, 4)) // 12
q, r := divide(10, 3)
fmt.Println(q, r) // 3 1
fmt.Println(sum(1, 2, 3, 4)) // 10
// スライスを展開して渡す
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...)) // 15
// 無名関数
double := func(n int) int {
return n * 2
}
fmt.Println(double(5)) // 10
// 関数を引数に渡す
result := apply(multiply, 3, 4)
fmt.Println(result) // 12
// クロージャ
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
}
ポインタ
package main
import "fmt"
func main() {
x := 10
// ポインタの取得
p := &x
fmt.Println(p) // 0xc0000a0010(アドレス)
fmt.Println(*p) // 10(デリファレンス)
// ポインタを通じて値を変更
*p = 20
fmt.Println(x) // 20
// 新しいポインタを作成
var q *int = new(int)
*q = 30
fmt.Println(*q) // 30
}
// 値渡し(コピー)
func incrementValue(n int) {
n++
}
// ポインタ渡し(参照)
func incrementPointer(n *int) {
*n++
}
func demo() {
x := 10
incrementValue(x)
fmt.Println(x) // 10(変わらない)
incrementPointer(&x)
fmt.Println(x) // 11(変わる)
}
構造体
package main
import "fmt"
// 構造体の定義
type Person struct {
Name string
Age int
Email string
}
// 埋め込み(継承の代わり)
type Employee struct {
Person // 埋め込み
EmployeeID string
Department string
}
// メソッド
func (p Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s", p.Name)
}
// ポインタレシーバ(値を変更可能)
func (p *Person) SetAge(age int) {
p.Age = age
}
// 値レシーバ(値は変更されない)
func (p Person) GetAge() int {
return p.Age
}
func main() {
// 構造体の作成
p1 := Person{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
}
// フィールドの順序で初期化
p2 := Person{"Bob", 30, "bob@example.com"}
// ゼロ値で初期化
var p3 Person
p3.Name = "Carol"
// ポインタで作成
p4 := &Person{
Name: "Dave",
Age: 35,
}
// newを使用
p5 := new(Person)
p5.Name = "Eve"
fmt.Println(p1, p2, p3, p4, p5)
// メソッドの呼び出し
fmt.Println(p1.Greet())
p1.SetAge(26)
fmt.Println(p1.Age) // 26
// 埋め込み
emp := Employee{
Person: Person{
Name: "Frank",
Age: 28,
},
EmployeeID: "E001",
Department: "Engineering",
}
// 埋め込まれたフィールドに直接アクセス
fmt.Println(emp.Name) // Frank
fmt.Println(emp.Greet()) // 埋め込まれたメソッドも使える
}
インターフェース
package main
import (
"fmt"
"math"
)
// インターフェースの定義
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle 型
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Rectangle 型
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// インターフェースを引数に取る関数
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
// 空インターフェース(any型)
func printAny(v interface{}) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}
// 型アサーション
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case Circle:
fmt.Printf("Circle with radius: %.2f\n", v.Radius)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
c := Circle{Radius: 5}
r := Rectangle{Width: 3, Height: 4}
printShapeInfo(c)
printShapeInfo(r)
// スライスにまとめる
shapes := []Shape{c, r}
for _, s := range shapes {
printShapeInfo(s)
}
// 型アサーション
var s Shape = Circle{Radius: 3}
// 型を確認して変換
if circle, ok := s.(Circle); ok {
fmt.Println("Radius:", circle.Radius)
}
// 空インターフェース
printAny(42)
printAny("hello")
printAny(c)
// 型スイッチ
describe(42)
describe("hello")
describe(c)
}
エラーハンドリング
package main
import (
"errors"
"fmt"
)
// エラーを返す関数
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// カスタムエラー
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "must be non-negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "unrealistic value",
}
}
return nil
}
// エラーのラップ(Go 1.13+)
func processFile(filename string) error {
_, err := openFile(filename)
if err != nil {
return fmt.Errorf("failed to process %s: %w", filename, err)
}
return nil
}
func openFile(filename string) (string, error) {
return "", errors.New("file not found")
}
func main() {
// 基本的なエラーハンドリング
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
// カスタムエラー
if err := validateAge(-5); err != nil {
// 型アサーションでエラーの詳細を取得
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("Validation error - Field: %s, Message: %s\n", ve.Field, ve.Message)
}
}
// エラーのラップと展開
err = processFile("test.txt")
if err != nil {
fmt.Println(err)
// errors.Is でエラーを比較
// errors.As でエラーの型を取得
}
}
並行処理
goroutine
package main
import (
"fmt"
"sync"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello, %s! (%d)\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// goroutineの起動
go sayHello("Alice")
go sayHello("Bob")
// メインgoroutineが終了するとすべて終了
time.Sleep(time.Second)
// WaitGroup で待機
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Printf("Worker %d started\n", n)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Worker %d done\n", n)
}(i)
}
wg.Wait()
fmt.Println("All workers completed")
}
チャネル
package main
import (
"fmt"
"time"
)
func main() {
// チャネルの作成
ch := make(chan int)
// 送信(別のgoroutineで)
go func() {
ch <- 42
}()
// 受信
value := <-ch
fmt.Println(value) // 42
// バッファ付きチャネル
buffered := make(chan int, 3)
buffered <- 1
buffered <- 2
buffered <- 3
// buffered <- 4 // ブロックされる(バッファフル)
fmt.Println(<-buffered) // 1
fmt.Println(<-buffered) // 2
// チャネルのクローズ
close(buffered)
// クローズされたチャネルからの読み取り
value, ok := <-buffered
fmt.Println(value, ok) // 3 true
value, ok = <-buffered
fmt.Println(value, ok) // 0 false(クローズ済み)
// rangeでチャネルを読み取り
numbers := make(chan int)
go func() {
for i := 0; i < 5; i++ {
numbers <- i
}
close(numbers)
}()
for n := range numbers {
fmt.Println(n)
}
}
// 送信専用チャネル
func send(ch chan<- int, value int) {
ch <- value
}
// 受信専用チャネル
func receive(ch <-chan int) int {
return <-ch
}
select
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
// select で複数のチャネルを待機
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
}
// タイムアウト
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("Received:", v)
case <-time.After(time.Second):
fmt.Println("Timeout!")
}
// 非ブロッキング
select {
case v := <-ch:
fmt.Println("Received:", v)
default:
fmt.Println("No value available")
}
}
並行パターン
Worker Pool
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// ワーカーを起動
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// ジョブを送信
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// ワーカーの完了を待機
go func() {
wg.Wait()
close(results)
}()
// 結果を収集
for result := range results {
fmt.Println("Result:", result)
}
}
Fan-out / Fan-in
package main
import (
"fmt"
"sync"
)
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
in := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Fan-out: 複数のgoroutineで処理
c1 := square(in)
c2 := square(in)
c3 := square(in)
// Fan-in: 結果をマージ
for n := range merge(c1, c2, c3) {
fmt.Println(n)
}
}
Context によるキャンセル
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// タイムアウト付きコンテキスト
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go doWork(ctx, 1)
go doWork(ctx, 2)
// タイムアウトを待機
<-ctx.Done()
fmt.Println("Main: all workers should be cancelled")
time.Sleep(time.Second) // ワーカーの終了を待機
}
パッケージとモジュール
プロジェクト構成
myproject/
├── go.mod
├── go.sum
├── main.go
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ └── handler.go
│ └── service/
│ └── service.go
├── pkg/
│ └── utils/
│ └── utils.go
└── api/
└── routes.go
// go.mod
module github.com/username/myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)
# 依存関係の追加
go get github.com/gin-gonic/gin
# 依存関係の整理
go mod tidy
# ベンダリング
go mod vendor
テスト
// calculator.go
package calculator
func Add(a, b int) int {
return a + b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// calculator_test.go
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
// テーブル駆動テスト
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed numbers", -2, 3, 1},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
// ベンチマーク
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
# テストの実行
go test ./...
# 詳細表示
go test -v ./...
# カバレッジ
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# ベンチマーク
go test -bench=. ./...
まとめ
| 機能 | 説明 |
|---|---|
| goroutine | 軽量スレッド |
| channel | goroutine間の通信 |
| select | 複数チャネルの待機 |
| sync.WaitGroup | goroutineの完了待機 |
| sync.Mutex | 排他制御 |
| context | キャンセル・タイムアウト |
Goのシンプルさと強力な並行処理機能を活かして、効率的なプログラムを書きましょう!