「プログラム構造」
書籍:プログラミング言語Go
第2章 「プログラム構造」 の要点と思われる箇所を自分のメモ用にまとめました。
2.1 名前
- 名前の最初の文字が大文字か小文字かでその名前がパッケージの境界を越えて見えるか否かが決まる。大文字から始まる場合は公開を意味する。
- Goはキャメルケース(単語のはじめを大文字にして単語を繋げる記法。一番最初の文字は小文字だとlowerCamel(<=これが例), 大文字だとUpperCamel(<=これが例))で書くのが慣習らしい。
- HTMLなどの頭文字の集合は常に大文字か常に小文字で書くのが慣習らしい。HTML, htmlはOKだがHtmlはダメ。
2.2 宣言
- ソースコードの.goファイルはどのパッケージに属すかを宣言するpackage宣言から始まり、import宣言が続き、型、変数、関数のパッケージレベルの宣言が任意の順で宣言される。
- 大域的な場所に宣言された小文字から始まるエンティティは、パッケージ内で可視となる。
2.3 変数
- 変数宣言の = の右辺が省略されるとその変数の型のゼロ値となる。
- 数値は0、boolはfalse、文字列は ""、インタフェースと参照型はnilがゼロ値。配列や構造体の合成型のゼロ価はすべての要素やフィールドでゼロ値をとる。
- ゼロ値の利用により、Goには未初期化の変数はない。
- 複数の変数を宣言し、リストで初期化できる。
.go
var i, j, k int
var b, f, s = true, 2.3, "four"
- パッケージレベルの変数はmainが始まる前に初期化される。ローカル変数は関数の実行中の宣言に出会ったときに初期化される。
2.3.1 省略変数宣言
- 関数内でローカル変数を宣言して初期化するために利用。大域的なスコープでは利用できない。
- 複数の変数を同一の省略変数宣言で宣言して初期化できるが、可読性が向上する場合にのみ利用すべき。forループの初期化部分のような短くて自然なグループ化などでのみ利用が推奨。
- 複数の変数を省略変数宣言で宣言する場合、すべてが新しい変数宣言になるわけではない。err などよく利用する変数の場合、二度目の同一スコープで省略宣言で利用するときは代入のように働く。
- 省略変数宣言で宣言する変数は少なくとも1つは新しい変数宣言でなければならない。新しい変数宣言がないとコンパイルエラーになる。
2.3.2 ポインタ
- 変数に&をつけると変数のポインタに、ポインタに*をつけるとポインタの変数の値にアクセスできる。
.go
x := 1
p := &x // pは*int型で、xを指している
fmt.Println(*p) // "1"
*p = 2 // x = 2と同じ
fmt.Println(x) // "2"
- ポインタのゼロ値はnil。
- 関数がローカル変数のアドレスを返すのは安全。
- ポインタ引数を関数に渡すことで関数が間接的に渡された変数を更新することができる。
- ポインタは変数のエイリアス。スライス、マップ、チャネルなど他の参照型の値をコピーしたときにもエイリアスが発生する。
2.3.3 new関数
- newは構文上の利便性であるだけで基本的な概念ではない。あまり使わない。
- newは事前宣言された関数で、予約語ではないので、関数内で別の概念にnewという名前を使える。
2.3.4 変数の生存期間
- 変数は到達不可能になるまで生存する。ローカル変数のポインタが返される関数が定義された場合、関数を抜けても返されるローカル変数は到達可能であるため生存し続ける。
このような変数は関数からエスケープしているという。パフォーマンスを最適化する際は、エスケープを意識すると良い。 - ガベージコレクションは正しいプログラムを作成するために役立つが、メモリについて考える重荷を取り除くものではない。変数の生存期間を意識することは重要。
2.4 代入
2.4.1 ダブル代入
- ダブル代入は複数の変数へ一度に代入できる機能。2つの変数の値を交換する場合などに役立つ。
.go
x, y = y, x
a[i], a[j] = a[j], a[i]
- スタイルの問題として式が複雑な場合はダブル代入は避けるべき。複雑な式への条件演算子の利用などと同じ。
- 代表的な利用方法としてはos.Openのようにerrorや、okと呼ばれるboolが返されるケース。
2.4.2 代入可能性
- プログラム内で代入が暗黙的に発生することが数多くある。関数の引数やreturn文、スライスのリテラル式など。
- 代入可能性の規則はさまざまな型に対応する規則がある。
- == と != で比較できるかどうかは代入可能性と関係する。第一オペランドまたは第二オペランドがもう片方のオペランドに代入可能である必要がある。
2.5 型宣言
- type宣言は既存の方と同じ基底型を持つ新たな名前付き方を定義するもので、パッケージレベルで書かれることが一般。
- type宣言も小文字始まりだとパッケージ内のみに公開、大文字始まりの名前だとパッケージ外に公開される。
- 同じ基底型のtype宣言でも両者を比較することはできない。型が区別されるので不注意な変数の組み合わせが避けられる。
- すべての型Tに対して値xをT型に変換するための返還演算 T(x) がある。型変換は返還前後の型が同じ基底型を持つか、両方の型が同じ基底型の変数を指すポインタ型の場合に許される。
- 名前付き型はその型の値に対して新たな振る舞いを定義することができる。定義された振る舞いはその型のメソッドと呼ばれる。
- 名前付き型にメソッドを関連付ける場合、func ("型の変数名" "型名") "関数名"("引数") "戻り値型" { で定義する。
.go
func (c Celsius) String() strint { ... }
- 名前付き型にStringメソッドを定義すると暗黙で文字列変換する場合にStringメソッドの値を利用してもらえる。
2.6 パッケージとファイル
- 個々のパッケージはその宣言に対して別々の名前空間として機能を果たす。
- パッケージ外に公開するものは識別子の先頭を大文字で始める。
- 詳細なドックコメントはたいていそれだけを目的とするファイルにかかれ、慣習により doc.go と呼ばれる。
2.6.1 インポート
- インポートしたパッケージを参照しないとコンパイルエラーとなる。デバッグでfmtをちょっと利用したい時など面倒になる。
- 上記問題を解決するため golang/org/x/tools/cmd/goimportsツールがあり、必要に応じてimportを挿入・削除してくれる。
2.6.2 パッケージ初期化
- パッケージレベルの変数の初期化は依存関係が解決され初期化順が決まる。
- 初期化式による初期化が難しい複雑な変数は、init関数の仕組みを使うのが良い。initの例は以下参照。
- init関数は呼び出すことも参照することもできないが、プログラムが開始した時点でinit関数は宣言順に自動的に実行される。
.go
var pc [256]byte
func init() {
for i := range pc { // for i, _ := range pc { と同じ。
pc[i] = pc[1/2] + byte(i&1)
}
}
2.7 スコープ
- 変数宣言は同じパッケージ内のどのファイルからも参照できるが、importはそのファイル内のみで有効。
- 個々の宣言が異なるレキシカルスコープにあれば同じ名前を複数宣言できる。が、コードの読み手を混乱させるのであまり利用すべきでない。
- レキシカルブロックは必ずしも明示的に {} で囲われているわけではない。for文の初期化節など。
- 異なるレキシカルスコープの変数名を := で宣言してしまうと、別の変数扱いになるので注意。
.go
var cwd string
func init() {
cwd, err := os.Getwd() // コンパイルエラー: 未使用: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
- 上記例は init() 内でcwdを利用していると、コンパイルエラーにならずに思わぬバグになりうる。init() 内ではなるべく := を使わないのが良さそう。
.go
var cwd string
func init() {
var err error
cwd, err = os.Getwd() // コンパイルエラー: 未使用: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
練習問題解答例