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

本記事は、プログラミング言語Goを読みながらgolangの基礎知識についてメモしていく記事です。

今回の記事は「プログラム構造」についてです。

名前

  • 25種類予約語(keywords) を持っている。名前としては使えない
  • intやtrueなどの事前宣言(predeclared) を持っている
    • 予約はされていないので、宣言で使うことはできる
    • Go 1.18 で any と comparable、Go 1.21 で min、max、clear が事前宣言された識別子に加わった
  • 公開か非公開か
    • 関数内で宣言されたものは関数の中でしか使えない
    • 関数の外で宣言された場合はそれが属するパッケージの全てのファイルから見える
    • 名前が大文字で始まる宣言は公開(export) されているので、名前が属するパッケージ外からアクセス可能
      • fmt.Printfを参照しているようなこと
  • キャメルケース を使おう
  • HTMLとかASCIIみたいな全部大文字の言葉はそのまま大文字で使おう

宣言(declaration)

  • プログラムのエンティティに名前をつけ、その性質の一部あるいは全部を規定する
  • var,const,type,funcの4種類

変数

var name type = expression
  • expressionが省略されたらその型に対するゼロ値(数値なら0、booleanならfalse、文字列なら""、インタフェースと参照型ならnil)になる
    • この仕組によって、変数は常にその型に明確に定義された値を持つことが保証されている

省略変数宣言(short variable declaration)​

  • ローカル変数の型名を省略する宣言
name := expression
  • nameの型はexpressionによって決まる
  • 簡潔なのでこの書き方が基本
    • var宣言はローカル変数の型が初期化子の型と異なるため明示的な型を必要とする場合などに使われる
var biling float64 = 100
  • 注意:省略変数宣言は必ずしも左辺の全ての変数を宣言するわけではない
    • 左辺の変数のどれかが既に同じレキシカルブロック内で宣言されていた場合、省略変数宣言は代入のように働く
    • 少なくとも1つの新たな変数を宣言しないといけない

ポインタ​

大事なので、別途まとめる

new関数​

  • 組み込みのnew関数を使うことで、変数を作成できる
  • new(T)は、T型の無名変数(unnamed variable) を作成し、それをTのゼロ値に初期化し、*T型の値であるそのアドレスを返す
p := new(int) //*int型のpは無名のint変数。xという変数を定義して&xでアドレスを生成して、、みたいなことをする必要がない
fmt.Println(*p) //0
*p = 2 //無名のintに2を設定
fmt.Println(*p) //2
  • Go 1.26 以降は new(123) のように式も渡せる。その場合は式の型の変数が作られ、式の値で初期化される
  • new()はあまり使われない

変数の生存期間​

  • 変数の生存期間(lifetime) とは、プログラムを実行する際にその変数が存在する期間である
    • パッケージレベルの変数の生存期間は、プログラムの実行全体
    • ローカル変数は動的な生存期間を持つ
      • ローカル変数の新たなインスタンスは宣言文が実行されるごとに生成される
      • Go 1.22 以降は、for 文で宣言したループ変数も反復ごとに新しく作られる(それまではループ全体で1つの変数を使い回していた)
      • そして、変数は到達不能(unreachable)になるまで生存し続ける
  • 難しいが、ここで覚えておくべきは、、
    • パフォーマンスを最適化する時は変数のエスケープ(escape)に注意するということ
    • 長い期間存在するオブジェクト(グローバル変数)の中に、短命なオブジェクトへの不要なポインタを維持したりすると、ガーベジコレクタによる短命なオブジェクトのメモリ解放を阻害する可能性がある

代入

  • 代入演算子(assignment operator)
    • *=
  • インクリメント文・デクリメント文(代入演算子ではなく文。x++ は x += 1 と同じ)
    • ++
    • --

タプル代入(tuple assignment)​

  • 複数の変数に一度に代入する書き方
x,y = y,x
  • 以下の3つの演算は、2つの変数で受けると値と成否(ok)の2つの結果を返すので、タプル代入で受ける
    • v,ok := m[key]:マップ検索
    • v,ok := x.(T):型アサーション
    • v,ok := <-ch:チャネル受信

型宣言

  • type宣言
    • 既存の型と同じ基底型(underlying type)を持つ新たな名前付き型(named type)を定義する
      • 基底型と区別して、場合によっては基底型と互換性のない型を使う
  • 型宣言はパッケージレベルで書かれることがおおい
type name underlying-type

型変換​

  • 全ての型Tに対して、値xをT型へ変換する変換演算T(x)がある。以下の場合に変換が許される。
    • 両方の型が同じ基底型を持つ場合
    • 両方の型が名前なしのポインタ型で、同じ基底型の変数を指す場合
    • このほか、数値型どうしや、文字列と[]byte・[]runeの間なども変換できる
  • 数字を変換した場合、値の内部表現を変える可能性がある(浮動小数点を整数に変換すると小数点以下は破棄される)

パッケージとファイル

パッケージは別々の名前空間(namespace)としての機能を果たす 慣習により、パッケージ名はインポートパスの最後と一致する インポートしたパッケージが使われないとエラーになるので、ちょっと面倒(自動でインポートしたり削除したりしてくれるツールがあると便利)

init()​

個々のファイル内では、プログラムが開始した時点でinit関数は宣言されている順序で自動的に実行される

スコープ

  • 宣言のスコープとは、宣言された名前を使うにあたって、その宣言を参照できるソースコードの範囲のこと

    • 生存期間とは違う スコープはコンパイル時の特性、生存期間は実行時の特性
  • ブロックとは、波括弧で囲まれた一連の文

    • レキシカルブロック(lexical blocks)と呼んでいる
      • ようは静的にスコープを判断する、というgolangが採用した仕様
      • こちら参考に
  • 異なるレキシカルスコープであれば、同じ名前の宣言が複数含まれていても良い

  • コンパイラは名前への参照を見つけると、内側のレキシカルブロックからユニバースブロックに達するまで宣言を探す

    • なので、内側の宣言が優先され、外側の宣言にアクセスできなくなる
    • これを、外側の宣言をシャドーイング(shadow)すなわち隠蔽(hide)しているという
  • 例えば、以下のコードはコンパイルは通るが、意図どおりに動かない

    • init()の中で:=により宣言したローカル変数cwdがパッケージレベルのcwdを隠蔽するので、パッケージレベルのcwdには値が入らず、main()では空文字列が表示される
package main

import (
"fmt"
"log"
"os"
)

var cwd string

func init() {
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}

func main() {
fmt.Println(cwd)
}
  • 以下のように修正する必要がある
package main

import (
"fmt"
"log"
"os"
)

var cwd string

func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}

func main() {
fmt.Println(cwd)
}