この記事はGo Advent Calendar 2025 シリーズ2 19日目の記事です。
初めに
お久しぶりです、私です。Twil3akineと申すものです。
今回はGoに入門しようと思います。
GoはRustの対抗馬とよく聞くので、入門してみようかなと思った次第でございます。
なんか、Goは標準ライブラリだけでHTTPサーバが立てられると聞いたので、それはもうやってみるしかないじゃないかと思い立ったが吉日でございます。
ということで、今回は「立ち上げてから訪問された回数を表示するサーバ」を作成しようと思います。
時間がない人向け
先に今回作成したものを載せておきます。
立ち上げてから訪問された回数を表示するサーバ
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
// 訪問回数を保持できるハンドラー
type countHandler struct {
count int
mx sync.Mutex // 排他制御するため
}
// アクセスすると、訪問回数を1増やして画面に出力する
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mx.Lock() // 排他制御を開始
defer h.mx.Unlock() // 必ず排他制御を解除して関数を終了する
h.count++
fmt.Fprintf(w, "count is %d.\n", h.count)
}
func main() {
// ハンドラーのインスタンス化
h := countHandler{count: 0}
http.Handle("/", &h)
log.Fatal(http.ListenAndServe(":8080", nil))
}
環境
- Windows11 Home
- go1.24.6 windows/amd64
基礎文法
今回初めてGoを触るので、まず基礎的な文法から学びます。
感想ですが、C言語ともRustとも違うなんか個人的にはすごい異質に感じました。
package main
// --- ライブラリのインポート ---
import (
"fmt"
"os"
)
// --- 構造体の定義 ---
type Person struct {
Name string
Age int
}
// --- メソッドの定義 ---
// (p Person) の部分を「レシーバ」と呼ぶ
func (p Person) SayHello() {
fmt.Printf("Hello, my name is %s.\n", p.Name)
}
// ポインタレシーバ
// 構造体の中身を更新する場合は *(ポインタ) をつける
func (p *Person) Birthday() {
p.Age++ // 自動的にデリファレンスされるので、(*p).Age++ と書かなくて良い
}
// メイン関数の定義
func main() {
// --- 変数宣言 ---
var num int // 数値型
var str string // 文字列型
// var flg bool // 真偽値
var arr []int // 数値のスライス(動的配列みたいなもの?)
num = 42
str = "String"
arr = []int{1, 2, 3}
// --- 型推論 ---
anything := 108
fmt.Println(num, str, arr, anything)
// --- 四則演算 ---
x := 42
y := 6
fmt.Println(x + y) // 足し算
fmt.Println(x - y) // 引き算
fmt.Println(x * y) // 掛け算
fmt.Println(x / y) // 割り算
fmt.Println(x % y) // 余り
// --- 条件分岐 ---
isTrue := true
if isTrue {
fmt.Println("True")
} else {
fmt.Println("False")
}
// --- 繰り返し ---
for idx, value := range []int{1, 2, 3} {
fmt.Printf("idx: %d, value: %d\n", idx, value)
}
// --- エラーハンドリング ---
f, err := os.Open("foo.txt")
if err != nil {
fmt.Println("エラーが発生しました:", err)
return
}
// defer: 関数終了時に必ず実行する処理
defer f.Close()
fmt.Println("ファイルを開けました")
// --- 構造体とポインタ ---
// インスタンス化
person := Person{Name: "Gopher", Age: 10}
// メソッド呼び出し
person.SayHello()
// ポインタを使ったメソッド
person.Birthday()
fmt.Println("New Age:", person.Age)
}
本題
最初にも書いた通り、今回は標準ライブラリnet/httpを用いてHTTPサーバを立てます。
net/httpを眺める
Serversという項目に目を奪われてみる
net/httpを眺めていくと、Serversと書かれた項目があります。
ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. Handle and HandleFunc add handlers to DefaultServeMux:
// コメントアウトは私が勝手に推測で書いてます // 多分なんか挙動を決めるハンドルの定義? http.Handle("/foo", fooHandler) // 多分ここもハンドルの定義? http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { // "Hello, $URL"を多分画面に出力する? fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) }) // ポート8080でリッスンしてログを残す? log.Fatal(http.ListenAndServe(":8080", nil))
なんとなく、簡単なHTTPサーバの立て方がわかったような気がします。
- おそらくアクセスに対する挙動を決める
Handle - アクセスの感知、サービスの提供を行う
ListenAndServe
この二つが必要なんだと思います。
今回は「訪問回数を保持して、訪問されたら+1して画面に出力」という挙動を実装できたら、あとはちょちょいのちょいですね。
ということで、そこを実装するために次は Handle, HandleFunc を見に行ってみましょう。
Handle, HandleFunc に触れてみる
Handle
func Handle(pattern string, handler Handler)Handle registers the handler for the given pattern in DefaultServeMux. The documentation for ServeMux explains how patterns are matched.
HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))HandleFunc registers the handler function for the given pattern in DefaultServeMux. The documentation for ServeMux explains how patterns are matched.
ここから、読み取れるのは
-
HandleはHadlerという構造体を引数に取る -
HandleFuncは関数を引数に取る -
Handlerとは何ぞや
ということです。
構造体の中身にもよりますが、今回は訪問回数を保持したいので、もしかしたら Handle を使うべきなのかも?と思いながら Handler も見に行きましょう。
Handler も見てみる
type Handler interface { ServeHTTP(ResponseWriter, *Request) }A Handler responds to an HTTP request.
[Handler.ServeHTTP] should write reply headers and data to the ResponseWriter and then return.
... (略)
とりあえず、アクセス時の挙動内容を ServeHTTP に書けば良さそうですね。
実装してみる
ここまでの情報で1回実装してみます。
しました。
package main
import (
"fmt"
"log"
"net/http"
)
// 訪問回数を保持できるハンドラー
type countHandler struct {
count int
}
// アクセスすると、訪問回数を1増やして画面に出力する
func (handler *countHandler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
handler.count++
fmt.Printf("count is %d.\n", handler.count)
}
func main() {
// ハンドラーのインスタンス化
handler := countHandler{count: 0}
http.Handle("/", &handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
実行します。
$ go run main.go
count is 1.
count is 2.
count is 3.
?????????
何が起こってるんですか。マジで。
というわけで、fmtで探すと、これがまたあるじゃないですか。
func Fprintln(w io.Writer, a ...any) (n int, err error)Fprintln formats using the default formats for its operands and writes to w. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and any write error encountered.
これの w に http.ResponseWriter を使えば良いのでは?
おそらく、
-
現状:
fmt.Printfは標準出力してしまうため、HTTPレスポンス(ブラウザ)には何も書き込まれず、空のレスポンスが返った。 -
修正:
io.Writerインターフェースを満たすhttp.ResponseWriterに書き込むためにFprint系を使う必要がある。
こういう感じだと思います。
Fprintf に変更する
package main
import (
"fmt"
"log"
"net/http"
)
// 訪問回数を保持できるハンドラー
type countHandler struct {
count int
}
// アクセスすると、訪問回数を1増やして画面に出力する
func (handler *countHandler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
handler.count++
- fmt.Printf("count is %d.\n", handler.count)
+ fmt.Fprintf(writer, "count is %d.\n", handler.count)
}
func main() {
// ハンドラーのインスタンス化
handler := countHandler{count: 0}
http.Handle("/", &handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
これで事nothingをgettingしました。
よりGoらしく
Geminiに聞いてみる
これで今回の目的は達成したのですが、せっかく入門したんだから、もっとGoらしいもの作りたいですよね。
ということで、Geminiに聞いてみました。
提示されたコードは動作しますが、「Goらしさ(Idiomatic Go)」と、Webサーバとして致命的に重要な「並行処理の安全性」という観点で、以下の2点を修正するとより良くなります。
- 競合状態(Race Condition)の回避:
http.ListenAndServeはリクエストごとにGoroutine(スレッドのようなもの)を立ち上げます。つまり、同時にアクセスが来るとhandler.count++が競合してカウントがずれます。Goではsync.Mutexを使って手動で守るのが定石です。- 命名規則: Goではレシーバ名や引数名は短くするのが好まれます(
handler→h,writer→w,req→r)
なるほど、なんか知らない単語が出てきましたが、一旦、命名規則からやりましょう。
命名規則を意識する
package main
import (
"fmt"
"log"
"net/http"
)
// 訪問回数を保持できるハンドラー
type countHandler struct {
count int
}
// アクセスすると、訪問回数を1増やして画面に出力する
- func (handler *countHandler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
- handler.count++
- fmt.Fprintf(writer, "count is %d.\n", handler.count)
+ func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ h.count++
+ fmt.Fprintf(w, "count is %d.\n", h.count)
}
func main() {
// ハンドラーのインスタンス化
- handler := countHandler{count: 0}
- http.Handle("/", &handler)
+ h := countHandler{count: 0}
+ http.Handle("/", &h)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Goは場合によっては一文字変数OKなんですねえ。
sync.Mutex について学ぶ
sync を読んでみると、Mutex について以下の記述がありました。
type Mutex struct { // contains filtered or unexported fields }A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex.
A Mutex must not be copied after first use.
In the terminology of the Go memory model, the n'th call to Mutex.Unlock “synchronizes before” the m'th call to Mutex.Lock for any n < m. A successful call to Mutex.TryLock is equivalent to a call to Lock. A failed call to TryLock does not establish any “synchronizes before” relation at all.
Mutex はなんか排他制御が可能になるみたいですね。
-
Mutex.Lock()でロックをかける -
Mutex.Unlock()でロックを解除する
完成
package main
import (
"fmt"
"log"
"net/http"
+ "sync"
)
// 訪問回数を保持できるハンドラー
type countHandler struct {
count int
+ mx sync.Mutex // 排他制御するため
}
// アクセスすると、訪問回数を1増やして画面に出力する
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ h.mx.Lock() // 排他制御を開始
+ defer h.mx.Unlock() // 必ず排他制御を解除して関数を終了する
h.count++
fmt.Fprintf(w, "count is %d.\n", h.count)
}
func main() {
// ハンドラーのインスタンス化
h := countHandler{count: 0}
http.Handle("/", &h)
log.Fatal(http.ListenAndServe(":8080", nil))
}
まとめ
- 標準パッケージでHTTPサーバ立てられるのすげえ!
- なんやかんな書き心地悪くなかった
- Rustと違ってキャメルケースなんだなぁって
- Goもちょくちょく触りたいと思える楽しさがあった

