1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go入門】ギャル🧑‍🎓と熱血教師 🧑‍🏫の対話で学ぶ Go🐭 の設計思想 — 他言語経験者のための「なぜ?」ガイド

1
Posted at

はじめに — この記事の読み方

この記事は、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 と推論される

明示的に型を書かなくても、コンパイラは namestring であることを知っている。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つある!

  1. 暗黙的実装で循環依存が起きない — repository が domain パッケージを import するだけで済む
  2. 小さい interface がイディオム — 1〜3メソッドの interface が推奨される。DIP がシンプルに実現できる
  3. 明示的な依存注入が自然 — コンストラクタ関数で interface を受け取るパターンが標準的。DI コンテナ不要
  4. テスタビリティ — 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
}

複数の deferLIFO(後入れ先出し) で実行される。

defer fmt.Println("1st")
defer fmt.Println("2nd")
defer fmt.Println("3rd")
// 出力: 3rd → 2nd → 1st

panic と recover

panic はプログラムを異常終了させる。recoverdefer の中でのみ 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.

ギャル生徒が優秀すぎる

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?