メインコンテンツまでスキップ

Goの基本文法

.NET (C#) の経験がある開発者向けに、変数宣言、定数、制御構文、ゼロ値、関数、複数戻り値、defer を解説します。

1. 変数宣言​

var と :=​

var count int // 型を明示 (ゼロ値 0 で初期化)
var name = "gopher" // 型推論
age := 30 // 短縮宣言 (関数内のみ)

var ( // まとめて宣言
host = "localhost"
port = 8080
)

a, b := 1, "two" // 複数同時
書き方使える場所用途
var x Tどこでもゼロ値で初期化したいとき・型を明示したいとき
var x = vどこでもパッケージ変数など
x := v関数内のみローカル変数の標準的な書き方

注意点:

  • 未使用のローカル変数はコンパイルエラーになる (C# は警告)。不要な戻り値は _ で捨てる
  • := は左辺に新しい変数が 1 つ以上必要。既存の変数だけなら = を使う
  • 型は右側ではなく名前の後ろに書く (var x int、func f(a int) string)
  • 暗黙の型変換はない。int と int64 の演算にも明示的な変換が必要
var i int = 10
var f float64 = float64(i) * 1.5 // 明示的な変換が必要

スコープとシャドーイング​

x := 1
if true {
x := 2 // 新しい変数 (外側の x を隠す)
_ = x
}
fmt.Println(x) // 1

特に err で起こりやすいバグです。if や for の内側で := を使うと、外側の変数が更新されません。

var result string
var err error
if cond {
result, err := doSomething() // 外側の result / err とは別物!
_, _ = result, err
}

2. 基本の型​

GoC#備考
boolbool
int, int8 ~ int64int, sbyte ~ longint はプラットフォーム依存 (64bit 環境で 64bit)
uint, uint8 ~ uint64uint, byte ~ ulong
bytebyteuint8 の別名
runechar (近い)Unicode コードポイント (int32 の別名)
float32, float64float, double
stringstring不変のバイト列 (UTF-8)
anyobjectinterface{} の別名

文字列​

s := "こんにちは"
fmt.Println(len(s)) // 15 (バイト数)
fmt.Println(len([]rune(s))) // 5 (文字数)

for i, r := range s { // rune 単位で走査 (i はバイト位置)
fmt.Println(i, string(r))
}

raw := `C:\path\to\file` // 生文字列 (C# の @"..." に近い)
  • 文字列は UTF-8 のバイト列。len はバイト数を返し、s[i] は byte
  • 文字単位で扱うときは []rune に変換するか、range で走査する
  • + での連結は可能だが、ループ内で多数連結するなら strings.Builder を使う (StringBuilder 相当)

3. 定数と iota​

const Pi = 3.14159
const MaxRetry = 3

const (
StatusPending = "pending"
StatusDone = "done"
)
  • const はコンパイル時に決まる値のみ。実行時の値や、スライス・マップ・構造体は定数にできない
  • 型を付けない定数 (untyped constant) は、必要に応じて型が決まる

iota による列挙​

Go に enum はありません。const と iota で表現します。

type Weekday int

const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
)

func (d Weekday) String() string {
switch d {
case Sunday:
return "Sunday"
case Monday:
return "Monday"
case Tuesday:
return "Tuesday"
default:
return "Unknown"
}
}
  • 値の範囲チェックは自動では行われない。Weekday(99) も作れる
  • ビットフラグは 1 << iota で表現できる

4. ゼロ値​

Go では、宣言しただけの変数は必ずその型のゼロ値で初期化されます。初期化し忘れによる未定義の状態がありません。

型ゼロ値
int, float64 など0
boolfalse
string"" (空文字列。null にならない)
ポインタ、slice、map、chan、func、interfacenil
struct全フィールドがゼロ値
配列全要素がゼロ値
var s string // ""
var p *int // nil
var m map[string]int // nil (読み取りは可能、書き込みは panic)
var xs []int // nil (len は 0、append は可能)

「ゼロ値を有用にする」(make the zero value useful) は Go の設計指針です。sync.Mutex や bytes.Buffer は、初期化なしでそのまま使えます。

null との違い​

  • string や構造体は null になりません。NullReferenceException の主な原因の多くが存在しません
  • 「値がない」ことを表現したいときはポインタ (*int) を使う。Nullable<T> 相当の標準型はない
  • nil スライスと空スライスは、ほとんどの場面で同じように振る舞う (JSON では null と [] で差が出る)

5. 制御構文​

if​

if x > 10 {
// ...
} else if x > 5 {
// ...
} else {
// ...
}

// 初期化文付き (スコープを if の中に限定)
if err := doSomething(); err != nil {
return err
}
  • 条件に括弧は不要、波括弧は必須 (1 行でも)
  • 条件は bool でなければならない (if x のような暗黙変換は不可)
  • 三項演算子 ? : はない

for (唯一のループ構文)​

while / do-while / foreach はなく、すべて for で書きます。

// 通常の for
for i := 0; i < 5; i++ {
fmt.Println(i)
}

// while 相当
for n < 100 {
n *= 2
}

// 無限ループ
for {
if done() {
break
}
}

// foreach 相当 (range)
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Println(i, v)
}
for _, v := range nums { _ = v } // インデックスを捨てる
for i := range nums { _ = i } // インデックスのみ

// map と文字列
for k, v := range map[string]int{"a": 1} { _, _ = k, v }

// 整数の range (Go 1.22 以降)
for i := range 5 {
fmt.Println(i) // 0..4
}
  • Go 1.22 以降は、ループ変数が反復ごとに新しく作られる。クロージャや goroutine でループ変数を捕捉する際の古い罠は解消されている (go.mod の go 行が 1.22 以上の場合)
  • map の走査順はランダム。順序が必要ならキーを取り出してソートする
  • break / continue にはラベルを付けて外側のループを制御できる
outer:
for _, row := range matrix {
for _, v := range row {
if v < 0 {
break outer
}
}
}

switch​

switch day {
case "sat", "sun": // 複数の値
fmt.Println("weekend")
case "mon":
fmt.Println("start")
default:
fmt.Println("weekday")
}

// 条件式の switch (if-else の連鎖の代わり)
switch {
case score >= 90:
grade = "A"
case score >= 70:
grade = "B"
default:
grade = "C"
}
  • フォールスルーしない: break は不要。次の case へ進みたいときのみ fallthrough を明示する
  • case には定数以外の式も書ける
  • 型による分岐は type switch を使う (C# のパターンマッチ switch に近い)
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return "string: " + x
case nil:
return "nil"
default:
return fmt.Sprintf("other: %T", x)
}
}

6. 関数​

基本​

func add(a, b int) int {
return a + b
}

func greet(name string, times int) {
for range times {
fmt.Println("Hello,", name)
}
}
  • 関数はパッケージレベルの第一級の値。変数に代入したり、引数に渡したりできる
  • オーバーロードはない。同じ名前の関数を、引数違いで複数定義できない
  • デフォルト引数・オプション引数もない。構造体や関数オプションパターンで代用する

可変長引数​

func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}

sum(1, 2, 3)
xs := []int{1, 2, 3}
sum(xs...) // スライスを展開して渡す (params 相当)

無名関数とクロージャ​

counter := func() func() int {
c := 0
return func() int {
c++
return c
}
}()

fmt.Println(counter(), counter()) // 1 2

複数戻り値​

C# のタプル / out パラメータの代わりに、関数が複数の値を返せます。

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

q, err := divide(10, 2)
if err != nil {
return err
}
  • 最後の戻り値に error を置くのが標準 (詳細はGoのエラーハンドリング)
  • 不要な値は _ で捨てる: q, _ := divide(10, 2)
  • TryParse パターンは、(value, ok bool) で表す: v, ok := m["key"]

名前付き戻り値​

func minMax(xs []int) (min, max int) {
min, max = xs[0], xs[0]
for _, x := range xs[1:] {
if x < min {
min = x
}
if x > max {
max = x
}
}
return // naked return: min, max を返す
}
  • 戻り値に名前を付けると、ドキュメントとして意味が伝わり、defer の中で戻り値を書き換えられる
  • naked return (引数なしの return) は、短い関数に限定する。長い関数では読みにくくなる

7. defer​

defer は、関数が終了するときに実行する処理を予約します。C# の try / finally や using の代わりです。

func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 関数を抜けるときに必ず実行

return io.ReadAll(f)
}

挙動のルール​

  1. LIFO (後入れ先出し) で実行される
  2. 引数は defer を書いた時点で評価される
  3. 関数を抜ける全経路 (return / panic) で実行される
  4. ループ内の defer は、関数終了まで溜まる
func main() {
for i := 0; i < 3; i++ {
defer fmt.Println("deferred", i)
}
fmt.Println("done")
}
// 出力:
// done
// deferred 2
// deferred 1
// deferred 0
func trace() {
x := 1
defer fmt.Println("x =", x) // 引数は今の値 (1) で確定
x = 2
}
// 出力: x = 1

ループ内での defer に注意​

// 悪い例: 全ファイルが関数終了まで閉じられない
for _, p := range paths {
f, _ := os.Open(p)
defer f.Close()
}

// 良い例: 関数に切り出す
for _, p := range paths {
if err := process(p); err != nil {
return err
}
}

func process(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// ...
return nil
}

典型的な用途​

mu.Lock()
defer mu.Unlock() // ロックの解放

tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback() // Commit 済みなら無害

start := time.Now()
defer func() {
slog.Info("done", "elapsed", time.Since(start))
}() // 無名関数で計測

defer と recover を使った panic の扱いは、Goのエラーハンドリングを参照してください。

8. .NET との対応表​

.NET (C#)Go
var / 型推論var / :=
null / defaultゼロ値 (zero value)
try / finally / usingdefer
タプル / out パラメータ複数戻り値
while / foreach / forfor のみ (range を含む)
switch 式 / パターンマッチswitch / type switch
enumconst + iota
params可変長引数 (...T)
ラムダ式無名関数 (関数リテラル)
メソッドのオーバーロードなし
省略可能な引数なし (構造体や関数オプションで代用)
三項演算子なし (if を使う)

9. C# 開発者がつまずきやすいポイント​

  • 未使用の変数・import はエラー: 試行錯誤中に頻繁に発生する。_ で明示的に捨てるか、削除する
  • := によるシャドーイング: 内側のスコープで err を再宣言し、外側が更新されない
  • nil マップへの書き込みは panic: make(map[string]int) または リテラルで初期化する
  • 暗黙の型変換がない: int と int64、float64 の混在は明示的に変換する
  • defer はループ内で溜まる: 関数に切り出して早めに解放する
  • 文字列の長さはバイト数: 日本語を扱うときは rune を意識する
  • switch はフォールスルーしない: C の経験があると逆に戸惑う。C# と同じ感覚で OK
  • for の range は値のコピー: for _, v := range items の v を書き換えても元の要素は変わらない (インデックスで書き込む)
  • 波括弧の位置は固定: { は必ず同じ行に書く。改行すると自動セミコロン挿入でコンパイルエラーになる (gofmt が整形する)

10. コード例​

C#​

public static (int Quotient, string? Error) Divide(int a, int b)
{
if (b == 0) return (0, "division by zero");
return (a / b, null);
}

public static string ReadFirstLine(string path)
{
using var reader = new StreamReader(path);
return reader.ReadLine() ?? "";
}

foreach (var (i, v) in nums.Select((v, i) => (i, v)))
{
Console.WriteLine($"{i}: {v}");
}

Go​

package main

import (
"bufio"
"errors"
"fmt"
"os"
)

type Level int

const (
Debug Level = iota
Info
Warn
)

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

func readFirstLine(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // using 相当

scanner := bufio.NewScanner(f)
if scanner.Scan() {
return scanner.Text(), nil
}
return "", scanner.Err()
}

func main() {
q, err := divide(10, 2)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("quotient:", q)

nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("%d: %d\n", i, v)
}

line, err := readFirstLine("sample.txt")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(line, Info)
}

11. まとめ​

  • 変数は var と := で宣言する。関数内では := が標準。未使用変数はコンパイルエラーで、暗黙の型変換もない。
  • ゼロ値が必ず設定されるため、初期化漏れや null 参照が起きにくい。ただし nil の map / slice / ポインタには注意する。
  • ループは for のみ。range で foreach 相当、switch はフォールスルーしない。列挙は const + iota で表現する。
  • 関数は複数戻り値を返せる。(T, error) と (T, bool) が基本形で、オーバーロードやデフォルト引数はない。
  • defer は try / finally / using の代わり。LIFO・引数は即時評価・ループ内では溜まることを押さえる。

次は型と構造体を参照してください。

参考資料​