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. 基本の型
| Go | C# | 備考 |
|---|---|---|
bool | bool | |
int, int8 ~ int64 | int, sbyte ~ long | int はプラットフォーム依存 (64bit 環境で 64bit) |
uint, uint8 ~ uint64 | uint, byte ~ ulong | |
byte | byte | uint8 の別名 |
rune | char (近い) | Unicode コードポイント (int32 の別名) |
float32, float64 | float, double | |
string | string | 不変のバイト列 (UTF-8) |
any | object | interface{} の別名 |
文字列
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 |
bool | false |
string | "" (空文字列。null にならない) |
ポインタ、slice、map、chan、func、interface | nil |
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)
}
挙動のルール
- LIFO (後入れ先出し) で実行される
- 引数は
deferを書いた時点で評価される - 関数を抜ける全経路 (
return/panic) で実行される - ループ内の
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 / using | defer |
タプル / out パラメータ | 複数戻り値 |
while / foreach / for | for のみ (range を含む) |
switch 式 / パターンマッチ | switch / type switch |
enum | const + iota |
params | 可変長引数 (...T) |
| ラムダ式 | 無名関数 (関数リテラル) |
| メソッドのオーバーロード | なし |
| 省略可能な引数 | なし (構造体や関数オプションで代用) |
| 三項演算子 | なし (if を使う) |
9. C# 開発者がつまずきやすいポイント
- 未使用の変数・import はエラー: 試行錯誤中に頻繁に発生する。
_で明示的に捨てるか、削除する :=によるシャドーイング: 内側のスコープでerrを再宣言し、外側が更新されないnilマップへの書き込みは panic:make(map[string]int)または リテラルで初期化する- 暗黙の型変換がない:
intとint64、float64の混在は明示的に変換する deferはループ内で溜まる: 関数に切り出して早めに解放する- 文字列の長さはバイト数: 日本語を扱うときは
runeを意識する switchはフォールスルーしない: C の経験があると逆に戸惑う。C# と同じ感覚で OKforの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・引数は即時評価・ループ内では溜まることを押さえる。
次は型と構造体を参照してください。