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.gogopackage 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 には初期化されていないメモリがないので、どの変数も存在した瞬間から値が決まっています。
| 型 | ゼロ値 |
|---|---|
bool | false |
int、float64 などの数値型 | 0 |
string | ""(空文字列) |
| ポインター、slice、map、channel、関数、interface | nil |
| 配列 | すべての要素がゼロ値 |
| struct | すべてのフィールドがゼロ値 |
同じルールは、new で確保したメモリ、複合リテラルで省略した struct のフィールド、make で作った slice の要素にも当てはまります。email フィールドのない JSON からデコードした User 構造体は、ゴミの値ではなく Email == "" になります。
Go でゼロ値が便利なのはなぜか?
うまく設計された型はコンストラクタなしでゼロ値のまま動き、標準ライブラリもこの性質を前提にしています。ゼロ値の sync.Mutex はロックされていない mutex です。sync.WaitGroup はカウントがゼロのグループで、bytes.Buffer は書き込める空のバッファです。こうしたフィールドで組み立てた struct は、宣言した時点ですぐに使えます。
example.gogopackage 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.gogoresp, err := http.Get(healthURL)
違いは、それぞれを書ける場所です。:= は関数本体の中でしか使えません。パッケージレベルでは、すべての宣言がキーワードで始まる必要があるので、次のコードは失敗します。
example.gogopackage main port := 8080 func main() {}
example.texttext./main.go:3:1: syntax error: non-declaration statement outside function body
パッケージレベルでも var port = 8080 ならコンパイルできます。
関数の中ではどちらの形もコンパイルできます。使い分けは、いくつかの慣習で決まります。
- 代入する値があるときは
:=を使います。cfg := loadConfig()やn, err := w.Write(body)などです。 - 意図してゼロ値を使いたいときは
varを使います。var buf bytes.Buffer、var wg sync.WaitGroup、あるいはappendで埋めていく slice のvar users []Userなどです。varと書けば、ゼロ値が出発点であることが読み手に伝わります。 - リテラルのデフォルトの型が合わないときは、型付きの
varを使います。limit := int64(1 << 40)よりvar limit int64 = 1 << 40やvar threshold float32 = 0.8のほうが読みやすくなります。 - 変数にあとから別々の分岐で値を入れるときは
varを使います。var status stringと宣言し、続くif/elseで値を設定する場合などです。
Go では := で変数を再宣言できるのか?
部分的にはできます。:= の左辺には新しい変数が少なくとも 1 つ必要で、同じスコープですでに宣言済みの名前は、再宣言されずに代入されます。この仕組みのおかげで、関数の中で 1 つの err を複数の呼び出しで使い回せます。
example.gogopackage 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.gogopackage 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.gogoif 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.texttextgo 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.gogopackage 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.gogopackage 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.gogovar ( 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.gogotype 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.gogotype 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.gogopackage 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 のようにブランク識別子に代入してください。使われていないパッケージレベル変数と関数の引数は許可されています。
出典
- The Go Programming Language Specification, Variable declarations: https://go.dev/ref/spec#Variable_declarations
- The Go Programming Language Specification, Short variable declarations: https://go.dev/ref/spec#Short_variable_declarations
- The Go Programming Language Specification, The zero value: https://go.dev/ref/spec#The_zero_value
- The Go Programming Language Specification, Package initialization: https://go.dev/ref/spec#Package_initialization
- Effective Go, Variables and redeclaration: https://go.dev/doc/effective_go#redeclaration
- Effective Go, Allocation with new: https://go.dev/doc/effective_go#allocation_new
- Go FAQ, Can I stop these complaints about my unused variable/import?: https://go.dev/doc/faq#unused_variables_and_imports
- Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- shadow アナライザー: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow
