はじめに — この記事の読み方
この記事は、Go 言語の基本概念を 🧑🎓(ギャル)と 🧑🏫(熱血先生)の対話形式 で解説するものである。
対象読者は「他の言語(Python, Ruby, Java など)を触ったことがあるが、Go は初めて」という人。Go 独特の設計思想には「なぜそうなっているのか」が分かると一気に腹落ちするポイントが多い。各トピックの対話を通じて、その「なぜ」を掴んでほしい。
コード例はすべて Go 1.21+ で動作する。手元で go run main.go しながら読むと効果的である。
静的型付けって何が嬉しいの?
🧑🎓 Python だと型とか書かなくてもサクサク動くじゃん? なんで Go ってわざわざ型書くの? めんどくない?
🧑🏫 いい質問だ! 荷物の仕分け作業を想像してくれ! 動的型付けは「箱を開封して中身を確認してから仕分けする」やり方。静的型付けは「箱にラベルが貼ってあるから開封せずに仕分けできる」やり方なんだ!
動的型付けだと「実行してみるまで型の間違いに気づけない」。この Python コードを見てくれ!
def add(a, b):
return a + b
add("hello", 1) # 実行時に TypeError
これはテストで通すか実際に動かすまでエラーにならない。Go なら同じミスをコンパイル時に弾けるんだ!
func add(a, b int) int {
return a + b
}
add("hello", 1) // コンパイルエラー: cannot use "hello" (type string) as type int
ラベル(型情報)があるから、箱を開ける前に「これは入れる場所が違う」と分かるわけだぜ!
🧑🎓 あー、なるほどね〜。動かす前にミスわかるのはたしかにアガるかも。でもさ、毎回 int とか string とか書くのだるくない?
🧑🏫 そこだよ! 実は Go には型推論があるんだ! := を使えば、コンパイラが右辺から型を推論してくれる!
name := "Alice" // string と推論される
age := 30 // int と推論される
rate := 0.85 // float64 と推論される
明示的に型を書かなくても、コンパイラは name が string であることを知っている。name + 1 のような間違いはちゃんと弾いてくれるぞ!
🧑🎓 えっ、まじ? じゃあ書き心地は Python とあんま変わんないってこと?
🧑🏫 その通りだ! お前、センスあるな! 「型推論で書き心地は動的型付けに近く、安全性は静的型付け」— これが Go のバランスなんだ!
🧑🎓 てかそれめっちゃ賢いじゃん。いいとこ取りってやつ? それはアガる〜
🧑🏫 まさにそうだぜ! 楽に書けるのに安全、それが Go の設計哲学だ! 覚えとけ!
値渡しと参照渡し — Go は"全部コピー"
🧑🎓 てかさ、Go って関数に引数渡すとき、値渡し? 参照渡し? どっち系なの?
🧑🏫 よく聞いてくれた! ここ超大事だからな! Go は基本すべて値渡し だ! 書類のコピーを渡すイメージだぜ。コピーに何を書き込んでも原本には影響しない!
func double(n int) {
n = n * 2
fmt.Println("inside:", n) // inside: 20
}
func main() {
x := 10
double(x)
fmt.Println("outside:", x) // outside: 10(変わらない)
}
一方、ポインタ渡しは「原本の保管場所を教える」やり方だ。保管場所を知っているので、原本を直接書き換えられる!
🧑🎓 ふーん、なるほどね〜。じゃあ slice はどうなの? 関数の中で変更したら反映されるって聞いたんだけど
🧑🏫 おお、いいところ突くな! ここが初学者が一番混乱するポイントだ! slice の内部構造を知ると一発で理解できるぞ!
slice の実体(24バイトの struct)
┌──────────┬─────┬─────┐
│ pointer │ len │ cap │
│ (8byte) │(8) │(8) │
└──────────┴─────┴─────┘
│
▼
[背後の配列: a, b, c, d, ...]
封筒(24バイトの struct)はコピーされるが、中に入っている地図(pointer)は同じ場所を指している。だから封筒が2つあっても、地図をたどれば同じ宝物(配列)にたどり着くんだ!
func modify(s []int) {
s[0] = 999 // 同じ配列を指しているので反映される
}
func main() {
nums := []int{1, 2, 3}
modify(nums)
fmt.Println(nums) // [999 2 3]
}
🧑🎓 えー、じゃあ slice って実質参照渡しみたいなもんじゃん?
🧑🏫 要素の変更に関してはそうだ! だが append で容量(cap)を超えると話が変わるんだ! いいか、ここ絶対覚えとけよ!
func appendItem(s []int) {
s = append(s, 100)
fmt.Println("inside:", s)
}
func main() {
nums := make([]int, 3, 3)
appendItem(nums)
fmt.Println("outside:", nums) // [0 0 0](100 が反映されていない!)
}
🧑🎓 うそ、append したのに反映されないとかやばくない? バグかと思うじゃん!
🧑🏫 だろ!? だからここが超大事なんだ!
append で cap を超えると、Go は新しい配列を確保して要素をコピーする。関数内の s は新しい配列を指すが、呼び出し元の slice は古い配列を指したままである。封筒のコピーが持つ地図だけが新しい場所に書き換わり、元の封筒の地図は古いまま。対策はポインタで渡すか、戻り値で返すこと。
🧑🎓 なるほどね〜、封筒のコピーって考えるとわかりやすい。戻り値で返すのが安全ってことね
🧑🏫 その通りだぜ! s = append(s, x) の結果を必ず受け取る — これが Go の鉄則だ!
ポインタ — アドレスを渡すという発想
🧑🎓 ポインタってやつ、C言語のでしょ? まじ怖いんだけど〜
🧑🏫 安心しろ! Go のポインタは C より圧倒的に安全だ! ポインタ演算ができないし、ガベージコレクタがメモリを管理してくれる! 覚えるのは2つの記号だけだぜ!
「住所と家」で考えるとわかりやすい。& は「家の住所を教える」、* は「その住所に行って中を見る」操作だ!
x := 42
p := &x // & で家の住所を取得する
fmt.Println(*p) // * でその住所に行って中を見る → 42
*p = 100 // その住所に行って中身を書き換える
fmt.Println(x) // 100(p 経由で x が変更された)
🧑🎓 えっ、2つだけ? & が住所で * が訪問? それなら覚えられるかも
🧑🏫 だろ! シンプルだぜ! で、なんでポインタが必要かっていうと、さっきの値渡しの話と直結するんだ! 値渡しだと「家をまるごとコピーして渡す」ことになる。コピーした家をリフォームしても、元の家は変わらない。でも住所(ポインタ)を渡せば、相手は元の家に直接行けるんだ!
🧑🎓 あー、さっきの slice の話とつながるね。で、具体的にどういうとき使うの?
🧑🏫 ポインタが必要な理由は主に2つだ! よく聞けよ!
① 呼び出し元の値を変更したいとき
住所を教えれば、相手がその家の中身を直接変えられる。
func increment(n *int) {
*n++ // 住所に行って中身を +1
}
func main() {
x := 10
increment(&x) // x の住所を教える
fmt.Println(x) // 11
}
② 大きなデータのコピーを避けたいとき
豪邸をまるごとコピーするのは大変だが、住所を伝えるだけなら一瞬だ!
type BigData struct {
Records [10000]int
}
func processValue(d BigData) { /* 80KB のコピーが発生 */ }
func processPointer(d *BigData) { /* 8バイトのポインタだけ */ }
🧑🎓 なるほどね〜、住所渡すだけなら一瞬だもんね。てかそれめっちゃ賢い
🧑🏫 お前、呑み込み早いな! ただし nil ポインタには注意が必要だ! 存在しない住所に行こうとすると panic になるぞ!
var p *int
fmt.Println(*p) // panic: nil pointer dereference
🧑🎓 えっ、やばくない? panic って落ちるってこと?
🧑🏫 そうだ! だからこれだけは絶対覚えとけ!
ポインタのゼロ値は nil である。nil ポインタをデリファレンスすると panic が発生する。関数がポインタを受け取る場合、nil チェックを習慣にすること。
struct とメソッド — ポインタレシーバ vs 値レシーバ
🧑🎓 Go にはクラスがないって聞いたんだけど、まじ? オブジェクト指向っぽいことどうやんの?
🧑🏫 いい質問だ! Go では struct にメソッドを紐づけるんだ! クラスがなくてもオブジェクト指向的な設計は十分できるぞ!
type User struct {
Name string
Age int
}
func (u User) Greet() string {
return "Hi, I'm " + u.Name
}
🧑🎓 ふーん。でもさ、(u User) と (u *User) ってあるじゃん? あれ何が違うの?
🧑🏫 よく聞いてくれた! ここ超大事だからな! 値レシーバとポインタレシーバの違いだ!
func (u User) SetNameValue(name string) {
u.Name = name // コピーに対する変更なので反映されない
}
func (u *User) SetNamePointer(name string) {
u.Name = name // 元の struct を変更するので反映される
}
🧑🎓 あー、さっきの値渡しの話と同じパターンじゃん! コピーに書いても元は変わんないってやつ
🧑🏫 その通りだ! お前、Go のセンスあるぞ! 使い分けの基準はこうだ!
| 条件 | レシーバ |
|---|---|
| struct のフィールドを変更する | ポインタレシーバ *T
|
| 読み取りだけ&struct が小さい | 値レシーバ T
|
| 読み取りだけだが struct が大きい | ポインタレシーバ *T
|
実務では「迷ったらポインタレシーバ」で問題ないことが多いぞ!
🧑🎓 おっけ〜。でもさ、クラスがないってことは継承もないんでしょ? それでオブジェクト指向成り立つの? ぴえん
🧑🏫 泣くな! Go は継承の代わりに composition(埋め込み) を使うんだ! struct の中に別の struct を埋め込むことで、フィールドとメソッドを再利用できる!
type Animal struct {
Name string
}
func (a Animal) String() string { return a.Name }
type Dog struct {
Animal // 埋め込み。Dog は Animal のフィールドとメソッドを持つ
Breed string
}
func main() {
d := Dog{Animal: Animal{Name: "Pochi"}, Breed: "Shiba"}
fmt.Println(d.String()) // "Pochi" — Animal のメソッドが直接呼べる
}
🧑🎓 えー、継承っぽいけど継承じゃないの? なんか不思議
🧑🏫 「is-a(犬は動物である)」ではなく「has-a(犬は動物の性質を持つ)」で考えるのが Go 流だ! 実際、継承の複雑さ — ダイヤモンド問題とか — を避けつつ、コードの再利用は十分にできる。こっちの方がシンプルで強いんだぜ!
🧑🎓 なるほどね〜、継承の闇を回避しつつ再利用できるのはウケる。Go、意外と攻めてるね
interface — 暗黙的に満たす設計
🧑🎓 Java の interface と Go の interface って何が違うの? implements とか書くの?
🧑🏫 よく聞いてくれた! まず「もし interface がなかったら」を考えてみろ!
// interface がない世界
func MakeDogNoise(d Dog) { fmt.Println(d.Speak()) }
func MakeCatNoise(c Cat) { fmt.Println(c.Speak()) }
func MakeBirdNoise(b Bird) { fmt.Println(b.Speak()) }
// 新しい動物が増えるたびに関数を追加…
🧑🎓 えー、動物増えるたびに関数書くとかだるすぎじゃん
🧑🏫 だろ! interface があれば1つの関数で済むんだ! コンセント(interface)の形さえ合えば、どんな家電(実装)でも挿せるのと同じ発想だぜ!
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }
// 1つの関数であらゆる Speaker を受け取れる
func MakeNoise(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
MakeNoise(Dog{}) // Woof!
MakeNoise(Cat{}) // Meow!
}
そして Go の最大の特徴はここだ! implements キーワードが不要なんだ! メソッドをすべて持っていれば自動的に interface を満たす!
🧑🎓 まじ? 宣言しなくていいの? それって Python のダックタイピングと同じじゃない?
🧑🏫 発想は近い! だが大きな違いがあるぞ! Python は実行時に判定するので実行時エラーになる。Go はコンパイル時にチェックされる。これを Structural Typing と呼ぶんだ!
🧑🎓 へー。てかさ、後から interface を定義しても既存の struct に適用できるってこと?
🧑🏫 その通りだ! サードパーティの struct に対して、自分のコードで interface を定義して抽象化できるんだ。コンセントの規格を後から決めても、形が合う家電は最初から対応していた — そういうことだぜ!
🧑🎓 うそ、それやばくない? めっちゃ柔軟じゃん。他の言語だと既存のコードに手入れなきゃダメなのに
🧑🏫 そう! それが Go の interface の一番カッコいいところなんだ! 覚えとけ!
クリーンアーキテクチャ — interface で層を分離する
🧑🎓 Go の interface がクリーンアーキテクチャと相性いいって聞いたんだけど、なんで?
🧑🏫 おお、いいところに目をつけたな! まず「なぜ層を分けるのか」から考えよう。全部 main.go に書いたらどうなるか見てみろ!
// 全部 main.go に書いた例(ハンドラ・ロジック・DB操作が混在)
func main() {
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
db, _ := sql.Open("mysql", "user:pass@/mydb")
rows, _ := db.Query("SELECT id, name FROM users")
// ... JSON に変換して返す
})
http.ListenAndServe(":8080", nil)
}
🧑🎓 うわ、全部ごちゃ混ぜじゃん。これで「MySQL やめて PostgreSQL にして」って言われたら地獄じゃない?
🧑🏫 まさにそうだ! DB 操作がハンドラに直書きされているから、全箇所を書き直す羽目になる。テストも本物の DB がないと動かない!
レストランの厨房に例えると分かりやすいぞ。ホール(handler)、シェフ(usecase)、食材仕入れ(repository)、レシピ(domain)がすべて一人の仕事だったら、仕入れ先を変えるだけで厨房全体が混乱する。役割を分ければ、仕入れ先が変わってもシェフの仕事は変わらないんだ!
🧑🎓 なるほどね〜、レストランで考えるとわかりやすい。で、具体的にどう分けるの?
🧑🏫 クリーンアーキテクチャの4層構成はこうだ!
Handler(ホール)→ Usecase(シェフ)→ Repository(食材仕入れ)→ Domain(レシピ・interface定義)
ポイントは domain 層に interface を定義し、repository 層がそれを実装するという構造だ!
// domain/user.go — レシピと仕入れ先の「規格」を定義
type User struct {
ID int
Name string
}
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// repository/user_repository.go — 実際の仕入れ先(MySQL)
type userRepository struct {
db *sql.DB
}
func (r *userRepository) FindByID(id int) (*domain.User, error) {
// DB から取得する処理
}
func (r *userRepository) Save(user *domain.User) error {
// DB に保存する処理
}
// usecase/user_usecase.go — シェフは「規格」だけ知っていればいい
type UserUsecase struct {
repo domain.UserRepository
}
func NewUserUsecase(repo domain.UserRepository) *UserUsecase {
return &UserUsecase{repo: repo}
}
🧑🎓 あー、Usecase は interface しか知らないから、裏側が MySQL でも PostgreSQL でもどっちでもいいってこと?
🧑🏫 完璧だ! MySQL から PostgreSQL に変えたいときは、userRepository の実装を差し替えるだけ。Usecase も Handler も一切変更しなくてよい!
🧑🎓 それはアガる〜。てか Go の interface が implements いらないから、余計にハマるってこと?
🧑🏫 その通りだ! Go の interface がクリーンアーキテクチャと特に相性が良い理由は4つある!
- 暗黙的実装で循環依存が起きない — repository が domain パッケージを import するだけで済む
- 小さい interface がイディオム — 1〜3メソッドの interface が推奨される。DIP がシンプルに実現できる
- 明示的な依存注入が自然 — コンストラクタ関数で interface を受け取るパターンが標準的。DI コンテナ不要
- テスタビリティ — interface を満たすモックを簡単に作れる
🧑🎓 Go、めっちゃ設計のこと考えてるじゃん。見直したわ〜
goroutine — 並行処理を気軽に始める
🧑🎓 並行処理ってなんか難しそうなイメージあるんだけど、Go だと簡単なの?
🧑🏫 料理で考えてみろ! パスタを茹でている間にソースを作る、サラダを盛り付ける — これが並行処理だ! Go では go キーワードを関数の前に置くだけで goroutine(並行する調理タスク)が起動する!
func sayHello(name string) {
fmt.Println("Hello,", name)
}
func main() {
go sayHello("Alice") // goroutine として起動
go sayHello("Bob")
time.Sleep(time.Second) // goroutine の完了を待つ(仮の方法)
}
🧑🎓 え、go って書くだけ? めっちゃ簡単じゃん。でも OS のスレッドとは違うの?
🧑🏫 まったく違うぞ! ここが Go のカッコいいところなんだ!
| OS スレッド | goroutine | |
|---|---|---|
| 初期スタック | 約 1MB | 約 2KB |
| スタックサイズ | 固定 | 動的に成長 |
| 管理 | OS カーネル | Go ランタイム |
| 生成コスト | 高い | 非常に低い |
goroutine は Go ランタイムの GMP スケジューラ によって管理される。M個の goroutine を N個の OS スレッドにマッピングする仕組み(M:N スケジューリング)だ。数万の goroutine を同時に動かしても問題ない!
🧑🎓 数万!? やばくない? スレッドだとそんなの無理でしょ
🧑🏫 その通り! OS スレッドで数万は現実的じゃないが、goroutine なら余裕だぜ!
🧑🎓 てかさ、さっきの time.Sleep で待つの雑すぎない? 終わったかどうかわかんないじゃん
🧑🏫 鋭い! sync.WaitGroup を使うんだ! 全品揃ったら配膳する、というイメージだぜ!
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // この料理は完成した、と報告
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 作る料理を1品追加
go worker(i, &wg)
}
wg.Wait() // 全品揃うまで待つ
}
🧑🎓 なるほどね〜、Add して Done して Wait。わかりやすい。名前がそのまんまなのウケる
channel — goroutine 同士の会話
🧑🎓 goroutine の結果を受け取りたいときってどうすんの? WaitGroup だと「終わった」しかわかんないじゃん
🧑🏫 いいところに気づいたな! channel を使うんだ! 料理人同士の声かけだと思えばいい。「ソースできたよ!」「了解、パスタに合わせるね」というやり取りが channel だ!
func compute(ch chan int) {
result := 42
ch <- result // 「42できたよ!」と送る
}
func main() {
ch := make(chan int) // 声をかけ合うための経路を作る
go compute(ch)
result := <-ch // 「受け取った!」
fmt.Println(result) // 42
}
🧑🎓 あー、矢印の向きで送る・受け取るが決まるのね。直感的でいいかも
🧑🏫 そうだ! ch <- が送信、<-ch が受信だ! で、channel にはバッファなしとバッファありがあるんだ!
🧑🎓 バッファ? 何それ
🧑🏫 バッファなし channel は「手渡し」だ — 相手が受け取るまで送り手は待つ。バッファあり channel は「郵便受け」— 受け手がいなくてもバッファに空きがあれば送れるんだ!
ch := make(chan int, 3) // バッファサイズ3の郵便受け
ch <- 1 // 即座に送れる
ch <- 2 // まだ空きがある
ch <- 3 // まだ空きがある
// ch <- 4 // ここでブロックされる(郵便受けが一杯)
🧑🎓 手渡し vs 郵便受け、めっちゃわかりやすいじゃん。でも複数の channel を同時に待ちたいときはどうすんの?
🧑🏫 select 文を使う! 複数の channel のうち、準備ができたものを処理できるんだ!
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
}
🧑🎓 へー、switch 文みたいな見た目だけど channel 用ってことか。タイムアウトも書けるのいいね
🧑🏫 その通り! select + time.After のパターンは実務で超頻出だ! 覚えとけ!
エラーハンドリング — 例外がない世界
🧑🎓 Go には try-catch がないって聞いたんだけど、まじ? じゃあエラーどうすんの?
🧑🏫 本当だ! Go ではエラーはただの値であり、戻り値として返すんだ!
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
🧑🎓 えー、if err != nil を毎回書くの? だるくない? try-catch の方が楽じゃん
🧑🏫 一見冗長に見えるが、try-catch には「どこで例外が飛ぶか分かりにくい」という問題があるんだ! 10行のコードを try で囲んだとき、どの行が例外を投げうるかは読み手が把握しなければならない。Go では if err != nil が「ここでエラーが起きうる」という明示的なマーカーになる。エラーの発生箇所と処理が常に隣り合っているから、コードの流れを追いやすいんだ!
🧑🎓 あー、たしかに。try の中で5個くらい関数呼んでると、どれが例外出すかわかんないもんね
🧑🏫 そう! 冗長さと引き換えに可読性を得ている。これが Go の設計思想だ!
🧑🎓 なるほどね〜、納得。じゃあエラーの種類を判定したいときはどうすんの?
🧑🏫 Go 1.13+ では errors.Is / errors.As / %w でエラーチェーンを扱えるぞ!
// エラーをラップして文脈を追加
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}
errors.Is は「このエラーチェーンの中に特定のエラーが含まれるか」を判定する!
if errors.Is(err, os.ErrNotExist) {
fmt.Println("ファイルが存在しない")
}
🧑🎓 errors.As ってのもあるんでしょ? あれはどういうとき使うの?
🧑🏫 エラーの「種類」で分岐しつつ、そのエラーの詳細情報にアクセスしたいときに使うんだ!
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// エラーを返す側
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be non-negative"}
}
return nil
}
// エラーを受け取る側
err := validateAge(-1)
var ve *ValidationError
if errors.As(err, &ve) {
// ValidationError の詳細フィールドにアクセスできる
fmt.Println("invalid field:", ve.Field) // "age"
fmt.Println("reason:", ve.Message) // "must be non-negative"
}
🧑🎓 ラップされたエラーでも errors.As 使えるの? %w で包んでても大丈夫?
🧑🏫 使える! %w でラップされていても、チェーンを辿って型を見つけてくれるんだ!
// ラップされたエラー
wrapped := fmt.Errorf("input validation failed: %w",
&ValidationError{Field: "email", Message: "invalid format"})
var ve *ValidationError
if errors.As(wrapped, &ve) {
fmt.Println(ve.Field) // "email" — ラップを解いて取り出せる
}
🧑🎓 まじ? ラップ解いて中身取れるとかめっちゃ賢いじゃん
🧑🏫 だろ! 使い分けだけ覚えとけ!
使い分けの目安 — errors.Is は「どのエラーか(値の一致)」、errors.As は「どの種類のエラーか(型の一致)+詳細情報へのアクセス」。
Go ならではの "知っておくべき" まとめ
defer — 後始末の保証
defer は関数の終了時に実行される。ファイルのクローズやロックの解放に使う。
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // この関数を抜けるときに必ず実行される
// f を使った処理...
return nil
}
複数の defer は LIFO(後入れ先出し) で実行される。
defer fmt.Println("1st")
defer fmt.Println("2nd")
defer fmt.Println("3rd")
// 出力: 3rd → 2nd → 1st
panic と recover
panic はプログラムを異常終了させる。recover は defer の中でのみ panic を捕捉できる。
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // b=0 なら panic
}
panic / recover は Go における例外処理 ではない。通常のエラーハンドリングには error を使い、panic は「プログラムを続行できない致命的な状況」に限定すること。
ゼロ値 — 宣言しただけで使える
Go の変数は宣言時に自動的にゼロ値で初期化される。
| 型 | ゼロ値 |
|---|---|
int, float64
|
0 |
string |
"" |
bool |
false |
pointer, slice, map, channel, func, interface
|
nil |
struct |
各フィールドのゼロ値 |
nil の扱いで注意すべき非対称性がある。
// nil slice は append OK
var s []int
s = append(s, 1) // 問題なし
// nil map への書き込みは panic
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
map は make で初期化してから使うこと。
m := make(map[string]int)
m["key"] = 1 // OK
未使用変数・import はコンパイルエラー
Go はコードの清潔さに厳格だ。使っていない変数や import があるとコンパイルが通らない。
import "fmt" // fmt を使っていなければコンパイルエラー
func main() {
x := 10 // x を使っていなければコンパイルエラー
}
開発中に一時的に無視したい場合は _(ブランク識別子)を使う。
_ = x // 「使った」ことにする
init() 関数 — パッケージ初期化
init() はパッケージが読み込まれたときに自動実行される特殊な関数である。main() より前に呼ばれる。
var config Config
func init() {
// パッケージ読み込み時に設定をロード
config = loadConfig()
}
init() はテストしにくく、実行順序の把握も難しい。多用は避け、明示的な初期化関数(NewXxx() など)を優先するのが現代の Go のプラクティスである。
🧑🏫 🧑🎓 以上が Go の基本概念の「なぜ」である!Go の設計は「シンプルさ」と「明示性」を徹底している!最初は冗長に感じる部分もあるが、コードベースが大きくなるほど、その設計判断の意味が分かってくるはずだ!!!🔥
P.S.
ギャル生徒が優秀すぎる