1-1. Hello World - Go の世界に入る
所要時間: 30-50分(がっつりなら2セッション分) コミット内容:
~/learn/go/day01/hello/一式(main.go と go.mod)
このレッスンのゴール(できるようになること)
-
go mod initで新規 Go プロジェクトを作れる -
package main/func main()の意味を説明できる -
go runとgo buildの違いを使い分けられる -
fmt.Printlnとfmt.Printfを使い分けられる - 大文字始まり/小文字始まりの可視性ルールが言える
なぜ学ぶか - 実務での悩みと結びつける
「docker や kubectl のソースを読みたい時、Go が読めないと詰む」「Node でビルドしてランタイム配って…が面倒」「API サーバーで goroutine を使った並行処理を書きたい」 - これらが本日のスタート地点。Hello World は単なる出力ではなく、Go の世界観を1ファイルに凝縮した最小モデル だ。package main、import、関数定義、可視性、ビルドコマンド - ここで触る要素はこの先の全章で再登場する。
前章とのつながり
これが最初のレッスン。前提知識として JS/TS の関数構文・モジュールシステムを知っている ことだけを期待する。それ以外の Go 特有の文法は本章でゼロから入る。
これができると何が嬉しいか
- GitHub の Go プロジェクトをクローンして
go runで動かせる → OSS の挙動を読みながら確認できる - 「Go で何か作ってみて」と言われた時に手が動く → 0分でプロジェクト初期化、5分で動くバイナリ
- Docker イメージに置くシングルバイナリ が手に入る → デプロイの土台が出来る
大前提: なぜ今 Go を学ぶか
JS/TS でフロントエンドをやっていたあなたが、バックエンドで一番最初に Go に触れる意味を整理する。
- Docker、Kubernetes、Terraform、Hugo、Prometheus … 現代インフラの主要 OSS はほぼ Go 製。サーバーサイドを「使う側」から「読み書きする側」に踏み込むなら、Go の文法は避けて通れない
- コンパイル型・静的型付け: JS のように「実行してみないと型エラーが分からない」が無い。ビルドが通ればまずまず動く、という安心感は TypeScript 経験者には馴染みやすい
- 並行処理 (goroutine / channel): API サーバーで「100リクエスト同時にDBに問い合わせ」みたいな処理が、Node.js より圧倒的に書きやすい
- デプロイが楽: ビルドすると1枚のバイナリが出来る。Docker イメージのサイズも小さい。Python のように仮想環境やランタイムを別途配る必要がない
Go の生まれと哲学
Google の Rob Pike / Ken Thompson / Robert Griesemer が 2007年に設計開始、2009年に公開。
設計思想
- Less is more: 言語仕様を意図的に小さく保つ。「機能Aと機能Bがあるけどどっち使う?」みたいな迷いを排除
- 明示的 (explicit): 隠れた挙動を嫌う。例外も throw/catch じゃなく「エラーは戻り値」
- コンパイルが速い: C++ のビルドが遅すぎたという反省から、秒速ビルドを最初から狙った
JS/TS 界の流儀(短い記述・暗黙の型変換・チェーン文化)とは正反対の思想。最初は窮屈に感じるが、それが Go。
セッション①: Go をインストールして最初のプログラム(25-30分)
0. 録画スタート&作業ディレクトリ
mkdir -p ~/log ~/learn/go/day01
cd ~/learn/go/day01
script ~/log/go_day01.log1. Go のインストール確認
# バージョン確認
go version
# 例: go version go1.23.4 darwin/arm64
goコマンドの本質Go は言語名であると同時に、ツールチェイン全部入りの
goという単一コマンドを提供する。
go run: ソースを直接実行(裏でビルドして実行、成果物は捨てる)go build: バイナリを作るgo mod: 依存管理go test: テスト実行go fmt: フォーマットgo vet: 静的解析JS/TS で言うと:
node+npm+tsc+eslint+prettier+jestがgo一発に統合されているイメージ。最初に覚えるコマンドはgo1つでいい。
Go が入っていない場合
# Homebrew で入れる brew install go # 確認 go versionmacOS の場合
~/go/binがPATHに通っていないとビルドしたバイナリが見つからない。Homebrew インストール時に~/.zshrcへの追記を案内してくれるので従う。
2. Hello World を書く前に: モジュールを初期化する
# プロジェクト用ディレクトリ
mkdir -p ~/learn/go/day01/hello
cd ~/learn/go/day01/hello
# モジュール初期化
go mod init example.com/hello実行後、カレントに go.mod というファイルが出来る。中身は3行ほど。
module example.com/hello
go 1.23
go mod initとは何かプロジェクトを Go モジュール として宣言するコマンド。
go.modという設定ファイルを生成する。
go.modの役割
- このプロジェクトの「名前 (module path)」を決める
- 使う Go のバージョンを宣言する
- 後で依存ライブラリを追加すると、ここに記録される
JS/TS で言うと:
npm init -yでpackage.jsonを作るのと完全に同じ役割。go.mod=package.json。
モジュール名の付け方
example.com/helloのように 「ドメイン名 + プロジェクト名」のような形 で書く慣習がある。理由: 後で公開する時に、そのリポジトリの URL がそのままモジュール名になるから。
- 公開しない学習用:
example.com/helloでもmyappでも何でもいい- GitHub にあげる予定:
github.com/<ユーザー名>/<リポジトリ名>にする(例:github.com/kuroiwa/myapp)落とし穴: 後でモジュール名を変えると
import文を全部書き直すハメになる。最初に決めておく。
go mod initを忘れるとどうなるかモジュール外で
go runすると最近の Go では大体エラーになる:go: go.mod file not found in current directory or any parent directory「ここどのプロジェクト?」が分からないと Go は動かない。新規プロジェクトは必ず
go mod initから。
3. main.go を書く
hello.go という名前で以下を保存(エディタは VS Code / Cursor / vim 何でも)。
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}これで保存して、
go run hello.go
# Hello, Go!出ました。
package mainの意味Go ではすべての
.goファイルが何かのパッケージに属する。1行目で「このファイルは main パッケージの一部」と宣言している。特別なパッケージ
main
mainという名前のパッケージは「実行可能プログラム」として扱われる- その中に
func main()を1つだけ書く → これがエントリポイント- 他のパッケージ名(
utils,dbなど)にすると「ライブラリ」になり、func main()を書けないJS/TS で言うと:
main.tsのmain()関数が自動で呼ばれるようなもの。package mainは「これは実行ファイルだよ」というマーカー。
import "fmt"の意味
fmtは format の略で、標準ライブラリの中で「フォーマットされた I/O」を担当するパッケージ。PrintlnPrintfSprintfなどが入っている。使い所
fmt.Println: 改行つきの出力。デバッグ・学習用に最頻出fmt.Printf:%s%d%vなどのフォーマット指定子で整形fmt.Sprintf: 結果を文字列として受け取る(ファイル名生成など)JS で言うと:
console.logがfmt.Println相当。ただし fmt は文字列整形まで担う。
func main() { ... }の構造
func: 関数を定義するキーワード(JS のfunction相当)main: 関数名(): 引数(main は引数なし){ ... }: 関数本体Go では 波カッコ
{が関数名と同じ行に必須。改行して{を次の行に置くとコンパイルエラーになる。// NG: コンパイルエラー func main() { fmt.Println("hi") }JS のように好きに改行できると思ってると最初の落とし穴。フォーマットは
go fmtが全部直してくれるので、慣れる前に保存時go fmtを癖にする。
セミコロンとカッコの作法
- 行末セミコロン
;不要(書いてもいいが書かないのが普通)- if / for の条件式にカッコ
()不要(書くと逆にエラー)- 波カッコ
{}は省略不可(1文だけの if でも省略できない)JS から来ると「カッコは書く、セミコロンは付ける」が体に染みついているので、最初は違和感を覚える。
4. go run で実行
go run hello.go
go runの本質ソースコードを受け取って、一時ディレクトリでビルド → そのバイナリを実行 → 終わったらバイナリ破棄 をワンライナーで行うコマンド。
使い所
- 動作確認・デバッグ中の手早い実行
- スクリプト的に1回だけ動かしたい時
落とし穴: 起動は速く感じるが、毎回内部でビルドしているので繰り返し実行するなら
go buildでバイナリを作ってそれを叩いた方が速い。
複数ファイルの実行
ファイルが増えたら
go run hello.go util.goのように並べる、または同じディレクトリの*.goをまとめて指定する:go run .
.はカレントディレクトリの main パッケージを実行という意味。実用的にはこれが一番楽。
5. go build でバイナリを作る
go build
# → hello という実行ファイルができる(モジュール名末尾が hello だから)
./hello
# Hello, Go!
# 別名で作りたい
go build -o myapp
./myapp
go buildの本質ソースをコンパイルして 実行可能なバイナリファイル(マシン語) を作る。一度作れば Go ランタイムが入っていない PC でも動く(シングルバイナリ)。
使い所
- 配布: 同僚や本番サーバーに「これ実行して」と渡す
- Docker イメージ作成: ビルドしたバイナリだけを入れた軽量イメージが作れる
- 性能計測: 一時ビルドの
go runより起動が速いクロスコンパイル:
GOOS=linux GOARCH=amd64 go buildでMac上からLinux用バイナリも作れる。Goのキラー機能の1つ。
バイナリのサイズに最初驚く
シンプルな Hello World でも生成バイナリは 1.5MB~2MB くらいある。これは Go ランタイムやガベージコレクタを内包しているから。
小さくしたいとき:
go build -ldflags="-s -w" -o myappデバッグ情報を削るので20-30%小さくなる。さらに
upxで圧縮も可能(が、本番では普通やらない)。
セッション②: JS/TS との違いを整理(25-30分)
6. JS/TS 経験者向け対応表
// Go
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
result := add(1, 2)
fmt.Println(result)
}// TypeScript
function add(a: number, b: number): number {
return a + b;
}
const result = add(1, 2);
console.log(result);似ているところ / 違うところ
観点 TypeScript Go 実行 tscで JS にトランスパイル →node実行go buildで ネイティブバイナリ に型 静的(実行時には消える) 静的(実行時も型がある) 型注釈の位置 変数・引数の 後ろ ( a: number)変数・引数の 後ろ ( a int) - 似てる関数定義 function add(a: number): numberfunc add(a int) int文字列補間 `Hello, ${name}`fmt.Sprintf("Hello, %s", name)null/undefined 2種類ある nilのみ(次回詳しく)エラー処理 try/catch戻り値で返す( result, err := ...)クラス あり 無い(struct + メソッド) パッケージ管理 npm/pnpmgo modフォーマッタ Prettier(任意) gofmt(標準・強制)一番のカルチャーショック: クラスが無い、例外が無い、ジェネリクスは長いこと無かった(1.18 で導入)、三項演算子も無い。意図的に機能が削られている。
7. 命名規則: 大文字 = 公開、小文字 = 非公開
package mypkg
// 公開: 他のパッケージから使える
func PublicFunction() {
// ...
}
// 非公開: このパッケージ内でのみ使える
func privateFunction() {
// ...
}
// 公開フィールド
type User struct {
Name string // 公開
age int // 非公開
}Go の「アクセス制御」は識別子の最初の文字で決まる
JS/TS の
publicprivateexportのようなキーワードは 無い。代わりに:
- 大文字始まり (
User,PrintAll) = public (exported)- 小文字始まり (
user,printAll) = package-private (unexported)同じパッケージ内なら大文字小文字に関係なくアクセスできる。他パッケージから見ると小文字始まりは存在しないも同然。
使い所
- 内部用ヘルパー関数は小文字で始める → うっかり外部に漏らさない
- API として公開したい型・関数は大文字で始める
落とし穴: TypeScript のように
exportを書き忘れたから動かないんじゃなくて、変数名の頭文字を間違えただけで「未定義」エラー になる。デバッグ時に「あれ、関数があるのに見えない」となったら大体これ。
8. go fmt でフォーマット統一
# 現在のディレクトリのファイルをすべてフォーマット
go fmt ./...
# 表示だけして書き換えない
gofmt -l hello.goGo の公式フォーマッタは「議論の余地なし」の文化
Go コミュニティでは インデント・スペース・改行の流儀で議論しない。理由:
gofmtが全部決めてしまうから。
- インデント: タブ
- 1行の最大長: 制限なし(推奨はあるが強制ではない)
- import 並び順: 自動整列
使い所
- 保存時に自動 fmt(VS Code/Cursor の Go 拡張で「format on save」をONに)
- CI で
gofmt -d .を走らせて差分があれば失敗にする落とし穴: 個人の好みで「ここはスペース入れたい」と思っても、
gofmtが容赦なく消す。抵抗せず受け入れるのがGoスタイル。
9. 自作チャレンジ
// hello.go を以下のように書き換えて、go run . で動かしてみる
package main
import "fmt"
func main() {
name := "Takato"
fmt.Printf("Hello, %s! Welcome to Go.\n", name)
// 計算もしてみる
sum := 1 + 2 + 3
fmt.Printf("1+2+3 = %d\n", sum)
}実行:
go run .
# Hello, Takato! Welcome to Go.
# 1+2+3 = 6
Printfのフォーマット指定子(よく使う)
指定子 用途 %s文字列 %d整数 %f浮動小数( %.2fで小数点2桁)%v何でも表示(迷ったらこれ) %+v構造体をフィールド名付きで表示(デバッグ用) %T型を表示(型確認デバッグに便利) \n改行
練習課題
# 1. 新規プロジェクトを作る
mkdir -p ~/learn/go/day01/greeting
cd ~/learn/go/day01/greeting
go mod init example.com/greeting
# 2. main.go を作って、自分の名前と今日の日付を表示する
# (ヒント: import "time" して time.Now() を使う)
# 3. go run . で動作確認
# 4. go build -o greet でバイナリを作る
# 5. ./greet で実行
# 6. go.mod の中身を cat で確認
cat go.mod動いたら次に進める
go.modが出来ているgo run .で何か出力されるgo buildで出来たバイナリを./バイナリ名で実行できるここまでで「Go プロジェクトの骨格」を体に入れた状態になる。
締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見(このノートに追記)
- 一番「TS と違うな」と思ったところ:
現時点ではそこまで差は感じない
ただ、ビルドするとランタイム環境がなくても動くのは便利だなと思った
攻撃者の側にたつと攻撃用のプログラムを知らない人の環境でも実行しやすそう
- ハマったエラー(あれば):
今のところ特にないが、`Printf` のフォーマット指定子がだるい。。
- go fmt で勝手に直された箇所:
なし
- 明日やりたいこと:
がんばっておぼえていきたい
メンターコメント (2026-05-16)
- TS との差「現時点では感じない」: Day 1 では正解の感覚。構文は本当に似てる。違いが効くのは Day 5-6 の スライス/マップの参照渡しの罠、Day 4 の 多値返却とエラー処理、Level 2 の interface と並行処理。「あ、こりゃ別物だ」と腑に落ちる瞬間が必ず来るのでお楽しみに
- 「攻撃側に立つと配布しやすい」: 自分でそこまで言語化できてるのは強い。Week 7 のセキュリティ章で「Go製マルウェアの増加」の話と繋がる。Day 1 の気付きが Week 7 で点が線になるはず
- 「Printf のフォーマット指定子がだるい」: 同感。実は
fmt.Printlnで十分なケースは多い。Printf は「複数値を整形したい時の最終手段」。本格運用ではlog/slogの Info/Debug など名前付きキー値で書く方が主流(Level 3 で扱う)。だるさは正しい感覚- 「go fmt で直された箇所: なし」← 事実誤認: 実際は3箇所直ってる。
git diff HEAD~ HEAD -- day01/greeting/main.goで振り返り推奨
name:= "Takato"→name := "Takato"(:=前のスペース)now :=time.Now()→now := time.Now()(:=後のスペース)fmt.Print(name , now)→fmt.Print(name, now)(,前の不要スペース削除) gofmt は「自分が直した」じゃなく「Go側が黙って直す」なので、気付かないのは普通。エディタの保存時自動整形 を仕込めば一生意識しなくていい- 「明日やりたいこと: がんばって覚えていきたい」: 抽象目標は ADHD の天敵。実行されない。具体化推奨:
- 例: 「Linux Day 2 を 25分」
- 例: 「Go Day 2 を始めて、変数・型・定数の3つだけ触る」
- 例: 「
gofmtをエディタ保存時に走らせる設定を入れる」 3 つ並べたら 1 つだけやれば勝ち、というルールで
自己評価チェックリスト
手を動かせた(実装)
-
go versionで Go が入っていることを確認した -
go mod initでgo.modを作った -
package mainとfunc main()を書いて Hello World を出した -
go run .で実行した -
go buildでバイナリを作って./バイナリ名で実行した
説明できる(メタ認知)
- 大文字始まり/小文字始まりの違いを人に説明できる
-
fmt.Printlnとfmt.Printfの違いを説明できる -
go runとgo buildの使い分けが言える -
package mainが「実行ファイル」のマーカーだと説明できる
やらかし回避(アンチパターン)
-
go mod init忘れエラーを再現できる -
{を改行して書いてコンパイルエラーを見た - エディタの保存時
gofmt設定を確認した
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| バージョン確認 | go version |
| プロジェクト初期化 | go mod init <モジュール名> |
| 実行(カレントの main) | go run . |
| 実行(特定ファイル) | go run hello.go |
| バイナリ作成 | go build |
| バイナリ作成(名前指定) | go build -o myapp |
| フォーマット | go fmt ./... |
| 静的解析 | go vet ./... |
| 依存追加 | go get <パス> |
| 標準出力 | fmt.Println(...) |
| 整形出力 | fmt.Printf("%s %d\n", s, n) |
「実務OK」基準
このレッスンで身に付くべき感覚:
- 新規 Go プロジェクトの開始手順が手に染みている:
mkdir → go mod init → main.goの流れに迷わない go runとgo buildの使い分けが言える: 動作確認はgo run、配布はgo buildfmt.Printlnとfmt.Printfを使い分けられる: ベタ出力か整形出力か- 大文字/小文字の意味が分かる: 公開・非公開はキーワードじゃなく頭文字
go.modがpackage.jsonの親戚だと言える
ここまで来たら「Go の入口の入口」をくぐった状態。次は変数と型に進む。
次のレッスン
1-2. 変数と型 へ。
var と := の違い、基本型、定数 const、iota、そして JS の null/undefined との違いを掘る。