Go言語の文法解説
基本文法から構造体・インターフェース、goroutineによる並行処理、データ構造・アルゴリズムの実装まで、基本情報技術者試験の合格を目指せるレベルで解説します。
文法解説
1. コメント
//で行コメント、/* */で複数行コメントを書きます。関数やエクスポートする識別子の直前に書くコメントはドキュメントコメントとして扱われます。
// これは行コメントです
package main
import "fmt"
/*
複数行にわたる
コメントです
*/
// Greet は挨拶メッセージを返します(ドキュメントコメント)
func Greet(name string) string {
return "こんにちは、" + name + "さん"
}
func main() {
fmt.Println(Greet("太郎"))
}
2. 変数宣言と静的型付け・基本型
Goは静的型付け言語で、コンパイル時に型が確定します。varで型を明示して宣言するか、関数内では:=で型推論による短縮宣言ができます。基本型にはint、float64、string、bool、byte、runeなどがあります。
var age int = 20 // 型を明示
var name string = "太郎"
var height float64 // ゼロ値 0.0 で初期化される
height = 172.5
weight := 60.5 // 型推論(float64)
isStudent := true // 型推論(bool)
const Pi = 3.14159 // 定数はconstで宣言(再代入不可)
fmt.Println(name, age, height, weight, isStudent, Pi)
3. 制御構文(if / for / switch)
Goのループ構文はforだけです。while文やdo-while文は存在せず、条件式のみのforでwhile相当の処理を書きます。条件分岐にはifとswitchを使います。
score := 75
if score >= 80 {
fmt.Println("優")
} else if score >= 60 {
fmt.Println("良")
} else {
fmt.Println("再履修")
}
// 通常のfor
for i := 0; i < 3; i++ {
fmt.Println("i =", i)
}
// while相当(条件式のみのfor)
n := 5
for n > 0 {
n--
}
// 無限ループ + break
count := 0
for {
count++
if count >= 3 {
break
}
}
switch {
case score >= 80:
fmt.Println("A評価")
case score >= 60:
fmt.Println("B評価")
default:
fmt.Println("C評価")
}
4. 関数(複数戻り値・可変長引数・無名関数・defer)
Goの関数は複数の値を同時に返すことができ、値とエラーを同時に返すパターンがよく使われます。...で可変長引数、func(...) {...}で無名関数(クロージャ)を書けます。deferは関数終了時に実行される処理を予約します。
// 複数戻り値
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("ゼロ除算はできません")
}
return a / b, nil
}
// 可変長引数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
fmt.Println("合計:", sum(1, 2, 3, 4))
// 無名関数(クロージャ)
double := func(x int) int { return x * 2 }
fmt.Println("2倍:", double(21))
// defer: main関数終了直前に実行される
defer fmt.Println("処理を終了します")
fmt.Println("処理を実行中...")
}
5. 配列とスライス
配列(array)は長さが固定ですが、実務では可変長のスライス(slice)を使うのが一般的です。スライスは配列への参照(ポインタ・長さ・容量)を持つ軽量な構造です。
var arr [3]int = [3]int{1, 2, 3} // 配列(長さ固定)
numbers := []int{1, 2, 3} // スライス
numbers = append(numbers, 4, 5)
// make で容量を指定して生成
scores := make([]int, 0, 10)
scores = append(scores, 90, 80, 70)
// スライスの一部を取り出す(部分スライス)
sub := numbers[1:3] // [2 3]
for i, v := range numbers {
fmt.Println(i, v)
}
fmt.Println("長さ:", len(numbers), "容量:", cap(numbers), sub)
6. マップ
マップ(map)はキーと値のペアを扱う連想配列(ハッシュテーブル)です。存在確認には「カンマOKイディオム」を使います。
scores := map[string]int{
"math": 90,
"english": 80,
}
scores["science"] = 75
// カンマOKイディオムでキーの存在を確認
if value, ok := scores["math"]; ok {
fmt.Println("mathの点数:", value)
}
delete(scores, "english") // キーの削除
for subject, point := range scores {
fmt.Println(subject, point)
}
7. 構造体・メソッド・ポインタ
structでデータをまとめ、レシーバを使ってメソッドを定義します。値レシーバは構造体をコピーして受け取り、ポインタレシーバ(*T)は元の値を直接変更できます。フィールドの更新には基本的にポインタレシーバを使います。
type Animal struct {
Name string
Age int
}
// 値レシーバ:読み取り専用の処理に向く
func (a Animal) Speak() string {
return a.Name + "が鳴きました"
}
// ポインタレシーバ:フィールドを変更できる
func (a *Animal) Birthday() {
a.Age++
}
func main() {
dog := Animal{Name: "ポチ", Age: 2}
fmt.Println(dog.Speak())
dog.Birthday() // Goが自動的に &dog に変換して呼び出す
fmt.Println("年齢:", dog.Age)
// ポインタの基本
p := &dog // pはdogのアドレスを持つポインタ
p.Name = "コロ" // (*p).Name の糖衣構文
fmt.Println(dog.Name)
}
8. インターフェース(構造的部分型)
インターフェースはメソッドの集合として振る舞いを定義します。Goのインターフェースは構造的部分型(Duck Typing的な性質)を持ち、implementsのような明示的な宣言をせずとも、必要なメソッドを実装するだけで自動的にそのインターフェースを満たします。
type Speaker interface {
Speak() string
}
type Cat struct{ Name string }
func (c Cat) Speak() string {
return c.Name + ": ニャー"
}
type Dog struct{ Name string }
func (d Dog) Speak() string {
return d.Name + ": ワン"
}
// Speakerを満たす型なら何でも受け取れる(ポリモーフィズム)
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
animals := []Speaker{Cat{Name: "タマ"}, Dog{Name: "ポチ"}}
for _, a := range animals {
announce(a)
}
}
9. エラーハンドリング(error型・panic/recover)
Goには例外機構がなく、通常のエラーはerror型の戻り値として表現し、呼び出し側でif err != nilを確認します。回復不能な致命的エラーにはpanicを使い、recoverで捕捉してプログラムのクラッシュを防ぐことができます。
import "errors"
var ErrNotFound = errors.New("見つかりません")
func find(id int) (string, error) {
if id != 1 {
return "", ErrNotFound
}
return "太郎", nil
}
// panicとrecoverで異常終了を防ぐ
func safeDivide(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("回復しました:", r)
result = 0
}
}()
return a / b // b が 0 だと panic: runtime error
}
func main() {
if name, err := find(2); err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println(name)
}
fmt.Println(safeDivide(10, 0)) // 0(recoverにより継続実行)
}
10. goroutineとchannel(CSPモデルによる並行処理)
GoはCSP(Communicating Sequential Processes)モデルに基づく並行処理をサポートします。goキーワードで軽量スレッドであるgoroutineを起動し、goroutine同士はchannelを介してデータをやり取りすることで、メモリの共有ではなく通信によって同期を取ります("Don't communicate by sharing memory; share memory by communicating.")。
func worker(id int, ch chan<- string) {
ch <- fmt.Sprintf("worker %d 完了", id)
}
func main() {
ch := make(chan string) // アンバッファードチャネル
for i := 1; i <= 3; i++ {
go worker(i, ch) // goroutineとして並行実行
}
for i := 0; i < 3; i++ {
fmt.Println(<-ch) // チャネルから受信(届くまでブロック)
}
// バッファ付きチャネルとselect文
results := make(chan int, 2)
results <- 1
results <- 2
close(results)
for v := range results {
fmt.Println("受信:", v)
}
}
11. sync.WaitGroupとsync.Mutex
複数のgoroutineの終了を待つにはsync.WaitGroupを使います。共有データへの同時アクセスによる競合状態(レースコンディション)を防ぐにはsync.Mutexで排他制御を行います。
import "sync"
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
counter := 0
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // 排他制御開始
counter++ // 複数goroutineから安全にアクセス
mu.Unlock() // 排他制御終了
}()
}
wg.Wait() // すべてのgoroutineの終了を待つ
fmt.Println("カウンタ:", counter) // 常に100
}
12. データ構造の実装:スタック
基本情報技術者試験でも頻出の「スタック(後入れ先出し・LIFO)」をスライスを使って実装します。PushはO(1)(平均)、PopはO(1)で処理できます。
type Stack struct {
items []int
}
// Push: 末尾に要素を追加する(計算量 O(1) 平均)
func (s *Stack) Push(v int) {
s.items = append(s.items, v)
}
// Pop: 末尾の要素を取り出す(計算量 O(1))
func (s *Stack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func main() {
s := &Stack{}
s.Push(1)
s.Push(2)
s.Push(3)
for {
v, ok := s.Pop()
if !ok {
break
}
fmt.Println(v) // 3, 2, 1 の順(後入れ先出し)
}
}
13. アルゴリズムと計算量(線形探索・二分探索)
探索アルゴリズムの代表例として、線形探索と二分探索を実装します。線形探索は先頭から順に比較するため計算量はO(n)です。二分探索はソート済み配列に対して探索範囲を毎回半分に絞り込むため計算量はO(log n)となり、データ件数が多いほど線形探索より効率的です。
// 線形探索:計算量 O(n)
func linearSearch(data []int, target int) int {
for i, v := range data {
if v == target {
return i
}
}
return -1
}
// 二分探索:計算量 O(log n)(dataは昇順ソート済みが前提)
func binarySearch(data []int, target int) int {
low, high := 0, len(data)-1
for low <= high {
mid := (low + high) / 2
switch {
case data[mid] == target:
return mid
case data[mid] < target:
low = mid + 1
default:
high = mid - 1
}
}
return -1
}
func main() {
data := []int{1, 3, 5, 7, 9, 11, 13}
fmt.Println("線形探索:", linearSearch(data, 7))
fmt.Println("二分探索:", binarySearch(data, 7))
}
14. パッケージ管理(go mod)
Goではgo modコマンドでモジュール(パッケージの依存関係)を管理します。プロジェクトのルートにgo.modファイルが生成され、依存パッケージのバージョンが記録されます。
# モジュールを初期化(go.mod を生成)
go mod init example.com/myapp
# 外部パッケージを取得して go.mod / go.sum に記録
go get github.com/google/uuid
# 使われていない依存を整理し、go.mod を整合させる
go mod tidy
# プログラムをビルド・実行
go build
go run main.go
コード内ではimport文でパッケージを取り込みます。標準ライブラリは"fmt"のようにパッケージ名のみ、外部パッケージはモジュールパスで指定します。
15. ガベージコレクションの基礎
Goはガベージコレクション(GC)を備えた言語で、開発者が明示的にメモリを解放する必要はありません。到達不能になったオブジェクトを自動的に検出し、ランタイムが並行して回収します。ただし、GCの負荷を意識した実装(不要な確保を減らす、goroutineのリークを防ぐ等)はパフォーマンスに直結します。
func createSlice() []int {
// この関数を抜けても numbers がどこかから参照され続ける限り
// GCは回収せず、参照が完全になくなった時点で回収対象になる
numbers := make([]int, 1000)
return numbers
}
func main() {
data := createSlice()
data = nil // 参照を切ることでGCの回収対象になりやすくなる
// runtime.GC() で明示的にGCを促すこともできる(通常は不要)
fmt.Println("Goランタイムが自動でメモリを管理します")
}
処理速度改善のポイント
Goはコンパイル型言語として高速ですが、書き方次第でさらにパフォーマンスを引き出せます。
- スライスは事前に容量を確保する:
make([]int, 0, 100)のようにcapを指定すると、appendによる再確保(メモリコピー)を減らせます。 - 文字列連結にはstrings.Builderを使う:+演算子での連結を繰り返すより、
strings.Builderを使う方がメモリ効率・速度で優れます。 - 値渡しとポインタ渡しを使い分ける:小さな構造体は値渡しでも問題ありませんが、大きな構造体はコピーコストを避けるためポインタ(
*Struct)で渡しましょう。 - goroutineを活用しつつリークに注意する:I/O待ちやCPUバウンドな処理を複数コアで並行実行できますが、終了しないgoroutine(goroutineリーク)はメモリを圧迫し続けます。
- ループ内でのdeferを避ける:deferは関数終了時まで実行されないため、ループ内で多用するとリソース解放が遅れメモリを圧迫します。
- sync.Poolでオブジェクトを再利用する:頻繁に生成・破棄されるオブジェクトを使い回すことでGCの負荷を減らせます。
- チャネルの使いすぎに注意する:チャネル経由の通信にはオーバーヘッドがあるため、単純な排他制御にはsync.Mutexの方が高速な場合があります。
- pprofでボトルネックを計測する:推測で最適化するのではなく、標準ツールのpprofやtesting.Bによるベンチマークで計測してから改善しましょう。