ブログに戻る

Go の var キーワード:ゼロ値、:= とパッケージレベル変数

Go の var キーワードの仕組みを解説します。宣言の書き方、ゼロ値、var と := の違い、シャドーイング、パッケージレベル変数と初期化の順序、センチネルエラー、コンパイル時のインターフェースチェックを扱います。

Go の var キーワード:ゼロ値、:= とパッケージレベル変数

var キーワードは変数を宣言します。var port int、var host = "localhost"、var ratio float64 = 0.25 のように、名前に型か初期値、またはその両方を付けます。値を指定せずに宣言した変数は、その型のゼロ値から始まります。ゼロ値は、数値なら 0、文字列なら ""、真偽値なら false、ポインター、slice、map、channel、関数、interface なら nil です。関数の中では、多くの Go のコードが短い形式の name := value を使います。ただし、パッケージレベルで変数を宣言する方法は var しかありません(Go spec)。

要約

  • var name Type は変数をゼロ値で宣言します。var name = value は値から型を推論します。var name Type = value はその両方を行います。var ( ... ) ブロックは複数の宣言をまとめます。
  • Go の変数はすべて初期化されます。初期化されていないメモリは存在しません。sync.Mutex、sync.WaitGroup、bytes.Buffer のような型は、ゼロ値のまま使えます。
  • := は宣言と代入を 1 ステップで行いますが、使えるのは関数の中だけです。パッケージレベルでは var が必要です。
  • := の左辺には、新しい名前が少なくとも 1 つ必要です。同じスコープにすでにある名前は再利用され、外側のスコープにある名前はシャドーイングされます。
  • if ブロックの中で := を使って外側の変数をシャドーイングしてしまうのは、よくあるバグです。コンパイラはこれを受け入れますが、shadow アナライザーで検出できます。
  • 使われていないローカル変数は declared and not used というコンパイルエラーになります。使われていないパッケージレベル変数はコンパイルできます。
  • パッケージレベル変数は、init() 関数が実行される前に、依存関係の順に初期化されます。パッケージレベル変数の典型例は、センチネルエラーとコンパイル済みの正規表現です。
  • var _ http.Handler = (*Server)(nil) と書くと、型がインターフェースを実装しているかをコンパイラにチェックさせられます。

Go で変数を宣言するには?

書き方は 3 つあり、さらにそれらを自由に組み合わせられるグループ化ブロックが 1 つあります。

example.gogo
package main

import (
	"fmt"
	"time"
)

var (
	serviceName        = "billing-api"
	maxRetries   int64 = 3
	requestCount int
)

func main() {
	var port int             // type only: starts at 0
	var host = "localhost"   // value only: the type is inferred as string
	var ratio float64 = 0.25 // both
	var readTimeout, writeTimeout time.Duration
	fmt.Println(serviceName, maxRetries, requestCount, port, host, ratio, readTimeout, writeTimeout)
}

このプログラムは billing-api 3 0 0 localhost 0.25 0s 0s と出力します。

型だけを書いて値を書かなければ、変数はその型のゼロ値になります。値だけを書いて型を書かなければ、変数は値の型になります。"localhost" や 0.25 のような型なし定数の場合、それはデフォルトの型で、string、int、float64、rune、bool、complex128 のいずれかです。両方を書くと、値は宣言した型に変換されます。普通の数値リテラルから int64 や float32 の変数を作るにはこの形を使います。var maxRetries int64 = 3 は int64 です。var maxRetries = 3 は int です。

1 つの宣言に複数の名前を並べることもできます。var readTimeout, writeTimeout time.Duration は同じ型の変数を 2 つ宣言し、どちらも 0s になります。グループ化した var ( ... ) ブロックはパッケージレベルでよく使われ、関連する設定を 1 か所にまとめられます。const ブロックと違い、直前の行の式は繰り返されません。値のない名前はすべてゼロ値になります。

Go のほかの名前と同じく、パッケージレベルで宣言した変数は、名前が大文字で始まればエクスポートされます。http.DefaultClient や os.Args は、エクスポートされたパッケージレベル変数です。エクスポートされた名前がパッケージをまたいでどう働くかは、Go の package キーワードで解説しています。

Go のゼロ値とは?

ゼロ値とは、初期化せずに宣言した変数が持つ値です。Go には初期化されていないメモリがないので、どの変数も存在した瞬間から値が決まっています。

型ゼロ値
boolfalse
int、float64 などの数値型0
string""(空文字列)
ポインター、slice、map、channel、関数、interfacenil
配列すべての要素がゼロ値
structすべてのフィールドがゼロ値

同じルールは、new で確保したメモリ、複合リテラルで省略した struct のフィールド、make で作った slice の要素にも当てはまります。email フィールドのない JSON からデコードした User 構造体は、ゴミの値ではなく Email == "" になります。

Go でゼロ値が便利なのはなぜか?

うまく設計された型はコンストラクタなしでゼロ値のまま動き、標準ライブラリもこの性質を前提にしています。ゼロ値の sync.Mutex はロックされていない mutex です。sync.WaitGroup はカウントがゼロのグループで、bytes.Buffer は書き込める空のバッファです。こうしたフィールドで組み立てた struct は、宣言した時点ですぐに使えます。

example.gogo
package main

import (
	"bytes"
	"fmt"
	"sync"
)

type RequestCounter struct {
	mu    sync.Mutex
	total int
}

func (c *RequestCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.total++
}

func main() {
	var counter RequestCounter // ready to use, no constructor needed
	var wg sync.WaitGroup
	for range 10 {
		wg.Go(counter.Inc)
	}
	wg.Wait()

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "handled %d requests", counter.total)
	fmt.Println(buf.String())
}

このプログラムは handled 10 requests と出力します。3 つの変数のどれにも New 関数は必要ありませんでした。Effective Go は、自分で定義する型も同じように設計し、var x T だけで T を使い始められるようにすることを勧めています。

すぐには使えないゼロ値は?

map です。程度は下がりますが、ポインターと channel も該当します。nil の slice は append、len、range で使えるので、var tags []string のあとに tags = append(tags, "go") と書いても問題ありません。nil の map は読み取りはできますが書き込みはできず、書き込むと assignment to entry in nil map で panic します。nil ポインターの参照外しは panic し、nil の channel への送信は永遠にブロックします。これらのケースと、代わりに make を使うべき場面については、Go の var と make の違いで説明しています。

Go の var と := の違いは?

:= は短い変数宣言です。retries := 3 は var retries = 3 と同じ意味で、新しい変数を宣言し、値から型を推論します。キーワードと型を書かずに済むので、関数の中では、特に関数呼び出しの戻り値を受け取るときに、こちらが普通の選択です。

example.gogo
resp, err := http.Get(healthURL)

違いは、それぞれを書ける場所です。:= は関数本体の中でしか使えません。パッケージレベルでは、すべての宣言がキーワードで始まる必要があるので、次のコードは失敗します。

example.gogo
package main

port := 8080

func main() {}
example.texttext
./main.go:3:1: syntax error: non-declaration statement outside function body

パッケージレベルでも var port = 8080 ならコンパイルできます。

関数の中ではどちらの形もコンパイルできます。使い分けは、いくつかの慣習で決まります。

  1. 代入する値があるときは := を使います。cfg := loadConfig() や n, err := w.Write(body) などです。
  2. 意図してゼロ値を使いたいときは var を使います。var buf bytes.Buffer、var wg sync.WaitGroup、あるいは append で埋めていく slice の var users []User などです。var と書けば、ゼロ値が出発点であることが読み手に伝わります。
  3. リテラルのデフォルトの型が合わないときは、型付きの var を使います。limit := int64(1 << 40) より var limit int64 = 1 << 40 や var threshold float32 = 0.8 のほうが読みやすくなります。
  4. 変数にあとから別々の分岐で値を入れるときは var を使います。var status string と宣言し、続く if/else で値を設定する場合などです。

Go では := で変数を再宣言できるのか?

部分的にはできます。:= の左辺には新しい変数が少なくとも 1 つ必要で、同じスコープですでに宣言済みの名前は、再宣言されずに代入されます。この仕組みのおかげで、関数の中で 1 つの err を複数の呼び出しで使い回せます。

example.gogo
package main

import "os"

func main() {
	f, err := os.Open("config.yaml")
	if err != nil {
		return
	}
	defer f.Close()

	info, err := f.Stat() // ok: info is new, err is reused
	_ = info
	f, err := os.Open("secrets.yaml")
	_ = f
}
example.texttext
./main.go:14:9: no new variables on left side of :=

info, err := f.Stat() は、info が新しいのでコンパイルできます。最後の := では f も err もすでに存在するので、宣言するものがありません。これらに代入するには、普通の = を使って f, err = os.Open("secrets.yaml") と書きます。var にはこのような例外はありません。同じスコープで var err error を 2 回書くと、err redeclared in this block で失敗します。

Go の変数のシャドーイングとは?

シャドーイングとは、内側のスコープの宣言が、外側のスコープにある変数と同じ名前を使うことです。内側の変数は、内側のスコープが終わるまで外側の変数を隠します。if、for、switch、{ } のブロックはどれも新しいスコープを作り、:= は常に現在のスコープで宣言します。この 2 つのルールが重なると、次のようなバグを書きやすくなります。

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func listenAddr(rawPort string) (string, error) {
	port := 8080
	if rawPort != "" {
		port, err := strconv.Atoi(rawPort)
		if err != nil {
			return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
		}
		fmt.Println("using PORT", port)
	}
	return fmt.Sprintf(":%d", port), nil
}

func main() {
	addr, err := listenAddr("9090")
	fmt.Println(addr, err)
}

このプログラムは using PORT 9090 と出力し、続けて :8080 <nil> と出力します。if ブロックの中では err が新しいので := が使え、その結果 port も新しく宣言されます。パースした値は内側の port に入り、閉じ括弧で消えてしまいます。関数が返すのは外側の port で、値は 8080 のままです。これを可能にしている if 文のスコープのルールは、Go の if キーワードで解説しています。

直すには、新しい変数を宣言するのではなく、外側の変数に代入します。先に err を宣言し、= を使います。

example.gogo
if rawPort != "" {
	var err error
	port, err = strconv.Atoi(rawPort)
	if err != nil {
		return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
	}
}

デフォルトの go vet のチェックはシャドーイングを報告しません。ネストした if の中の err のように、意図的なシャドーイングが多いからです。Go チームの shadow アナライザーなら報告します。

example.texttext
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow) ./...
example.texttext
./main.go:11:3: declaration of "port" shadows declaration at line 9

golangci-lint でも、govet リンターで shadow を有効にすれば同じチェックを実行できます。

Go が「declared and not used」と報告するのはなぜか?

コンパイラは、一度も読み取られないローカル変数をすべて拒否します。上のコードで内側の port がコンパイルできるのは、出力の行があるからにすぎません。この行がなければ、コンパイラは declared and not used: port を報告し、バグの場所を示します。値を代入するだけでは、使ったことになりません。

example.gogo
package main

import "net/http"

func main() {
	resp, err := http.Get("https://example.com")
	if err != nil {
		return
	}
	retries := 0
	retries = 3
}
example.texttext
./main.go:6:2: declared and not used: resp
./main.go:10:2: declared and not used: retries

このルールが適用されるのはローカル変数だけです。使われていないパッケージレベル変数、関数の引数、名前付きの戻り値はコンパイルできます。まだ読まない値を残しておきたいときは、_ = resp のようにブランク識別子に代入するか、_, err := http.Get(url) のように最初から受け取らないようにします。

Go のパッケージレベル変数はどう動くのか?

関数の外で宣言した変数はパッケージに属します。パッケージ内のすべてのファイルから見え、プログラムが終わるまで存在し、main が始まる前に初期化されます。パッケージレベル変数は、ソースに書かれた順ではなく、依存関係の順に初期化されます。

example.gogo
package main

import (
	"errors"
	"fmt"
)

var (
	usersURL = apiBase + "/users"
	apiBase  = apiHost + "/v2"
	apiHost  = "https://api.internal"
)

var ErrNotFound = errors.New("user not found")

func init() {
	fmt.Println("init sees", usersURL)
}

func main() {
	err := fmt.Errorf("load user 42: %w", ErrNotFound)
	fmt.Println(usersURL, errors.Is(err, ErrNotFound))
}

このプログラムは init sees https://api.internal/v2/users と出力し、続けて https://api.internal/v2/users true と出力します。usersURL は最初に宣言されていますが、apiBase に依存し、apiBase は apiHost に依存しています。そのためコンパイラは逆の順に初期化します。互いに依存しない変数は、宣言した順に初期化されます。2 つの変数が互いに依存していると、initialization cycle for a というエラーになります。

すべてのパッケージレベル変数に値が入ったあと、init() 関数がファイルごとに、ソースに現れる順に実行されます。1 つのパッケージに複数の init() 関数を書くことができ、同じファイルに複数あっても構いません。インポートしたパッケージが先に完全に初期化されるので、自分の init() が実行される時点では、インポートしたパッケージはすべて使える状態です。init() とブランクインポートの関係については、Go の import キーワードで解説しています。

センチネルエラーを var で宣言するのはなぜか?

errors.New は関数呼び出しなので、その結果は定数にできないからです。センチネルエラーとは、呼び出し側が errors.Is で比較するためのパッケージレベル変数です。

example.gogo
var (
	ErrNotFound   = errors.New("user not found")
	ErrEmailTaken = errors.New("email already registered")
)

errors.Is はエラーの値を比較します。errors.New は、同じテキストでも呼び出すたびに別の値を返します。そのため、エラーはパッケージレベルで一度だけ作って共有する必要があります。標準ライブラリの例には io.EOF、sql.ErrNoRows、http.ErrServerClosed があります。

作るのにコストがかかり、起動後に変わらないほかの値も同じです。var slugPattern = regexp.MustCompile("^[a-z0-9-]+$") は、プログラムの起動時に一度だけ正規表現をコンパイルします。パターンが不正なら、最初のリクエストで失敗するのではなく、起動時に panic します。

Go でグローバル変数を使うべきか?

センチネルエラー、コンパイル済みのパターン、ルックアップテーブルのように、起動後に固定される値なら使って構いません。変更される状態には、たいてい使うべきではありません。パッケージレベルの var db *sql.DB や var cfg Config は、パッケージ内のすべての goroutine とすべてのテストで共有されます。並行して書き込むには mutex が必要です。異なる値を使うテストは並列に実行できません。また、関数のシグネチャを見ても、その関数がグローバル変数に依存していることはわかりません。

よくある代替策は、依存するものを struct にまとめて渡すことです。

example.gogo
type Server struct {
	db     *sql.DB
	logger *slog.Logger
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
	// uses s.db and s.logger instead of package-level variables
}

こうすれば、各テストが自分専用のデータベースとロガーを持つ Server を作れます。

Go の var _ = とは何か?

ブランク識別子 _ を名前にして変数を宣言する書き方です。値は評価されたあと、捨てられます。最もよく使われるのは、型がインターフェースを実装しているかをコンパイル時にチェックする用途です。

example.gogo
type Server struct{}

func (s *Server) ServeHTTp(w http.ResponseWriter, r *http.Request) {}

var _ http.Handler = (*Server)(nil)

上のメソッド名にはタイプミスがあり、この宣言によってビルドエラーになります。

example.texttext
./main.go:9:22: cannot use (*Server)(nil) (value of type *Server) as http.Handler value in variable declaration: *Server does not implement http.Handler (missing method ServeHTTP)
		have ServeHTTp(http.ResponseWriter, *http.Request)
		want ServeHTTP(http.ResponseWriter, *http.Request)

このチェックがなければ、タイプミスが表面化するのは *Server を http.Handler として渡す箇所だけです。それは別のパッケージかもしれませんし、そもそもどこにもないかもしれません。(*Server)(nil) は *Server に変換した nil ポインターなので、このチェックでメモリ割り当ては発生しません。メソッドのレシーバーが値レシーバーの場合は、代わりに Server{} を使います。

ブランク識別子は、複数の値を受け取る宣言でも使えます。var _, offset = time.Now().Zone() は 2 番目の結果だけを残します。

Go で複数の変数をまとめて宣言するには?

名前をカンマ区切りで並べ、同じ数の値のリストか、同じ数の値を返す関数呼び出しを書きます。

example.gogo
package main

import (
	"fmt"
	"strings"
)

var host, port = "localhost", 8080

func main() {
	var user, domain, found = strings.Cut("[email protected]", "@")
	fmt.Println(host, port, user, domain, found)
}

このプログラムは localhost 8080 ops levelupgo.dev true と出力します。型を書かなければ、各変数はそれぞれの値の型になるので、host は string、port は int です。var minPort, maxPort int = 1024, 65535 のように型を書くと、すべての変数がその型になります。

代入も同じように書けます。a, b = b, a は 2 つの変数の値を入れ替えます。Go は右辺の値をすべて評価してから代入するからです。

Go 1.22 からは、for 文で := を使って宣言したループ変数は、反復ごとに新しい変数になります。for _, job := range jobs の job をキャプチャしたクロージャや goroutine は、最後の job ではなく、その反復の job を参照します。これで直った goroutine のバグと、モジュールがどちらの振る舞いになるかの確認方法は Go の for キーワードで紹介しています。

Go で var と const はどう使い分けるのか?

タイムアウト、上限値、ヘッダー名のように、コードを書く時点でわかっていて決して変わらない値には const を使います。それ以外には var を使います。実行時に計算する値、設定から読み込む値、そして slice、map、struct、ポインターは定数にできないので、すべて var です。型なし定数、iota、enum については、Go の const キーワードで解説しています。

LevelUpGo で学ぶ

LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Go Basics では、最初のレッスンで定数や型と一緒に var、:=、ゼロ値を紹介します。Go Language Deep Dives にはスコープとシャドーイングを扱うレッスンがあり、シャドーイングされた変数を見つけて修正します。Training Ground には、コースの外で練習できる短い単独の演習があります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。

よくある質問

Go で var はキーワードですか?

はい。var は Go の 25 個の予約語の 1 つなので、変数名、関数名、型名には使えません。変数宣言を始めるキーワードで、1 つだけの var name Type = value と、まとめて書く var ( ... ) ブロックのどちらにも使います。

Go の var と := の違いは何ですか?

:= は var の短い形式で、常に型を推論し、関数の中でしか使えません。x := 5 も var x = 5 も、値が 5 の int を宣言します。パッケージレベルでは var が必須です。ゼロ値を使いたいときや、リテラルのデフォルトとは別の型にしたいときも、var のほうが意図が明確になります。また := では、左辺の名前のうち少なくとも 1 つが新しければ、新しい名前と既存の名前を混在させられます。

Go の変数のゼロ値とは何ですか?

初期化式なしで宣言した変数が持つ値です。数値なら 0、文字列なら ""、真偽値なら false、ポインター、slice、map、channel、関数、interface なら nil です。配列と struct は、すべての要素やフィールドがゼロ値になります。Go に初期化されていない変数はありません。

Go で関数の外で := を使えますか?

使えません。パッケージレベルでは、すべての宣言が var、const、type、func のようなキーワードで始まる必要があります。port := 8080 は syntax error: non-declaration statement outside function body で失敗します。代わりに var port = 8080 と書きます。

Go で「declared and not used」エラーが出るのはなぜですか?

使われていないローカル変数は、シャドーイングされた変数やエラーチェックの書き忘れなど、バグの兆候であることが多いため、Go のコンパイラはこれを拒否します。値を代入するだけでは、使ったことになりません。変数を読み取るか、削除するか、_ = x のようにブランク識別子に代入してください。使われていないパッケージレベル変数と関数の引数は許可されています。

出典

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成