CODE Polyglot / よく使う書き方 / Go

Go のよく使う書き方(7本)

開発でよく書くコードの型と小さなツールの、Go の版をまとめました。標準の機能だけで書いていて、毎朝、実際に動かして答えを確かめています。

01. スネークケースとキャメルケースを変換する

user_id と userId を行き来させます。データベースの列名と JSON のキー、定数と変数のように、名前の書き方をそろえるときに使います。HTTPServer のように大文字が続く名前で、よく見かける書き方はほとんどの言語で崩れます。 ほかの言語・正解の例・解説 →

case_convert.go1.27.2 で 14 / 14 件の出力が正解と一致

// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
package main

import (
	"fmt"
	"os"
	"strings"
)

func isUpper(c byte) bool { return c >= 'A' && c <= 'Z' }
func isLower(c byte) bool { return c >= 'a' && c <= 'z' }
func isDigit(c byte) bool { return c >= '0' && c <= '9' }

func main() {
	mode, s := os.Args[1], os.Args[2]
	// 先に半角の英数字と _ だけかを確かめる。そのあとは1バイト=1文字として扱える
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !(isUpper(c) || isLower(c) || isDigit(c) || c == '_') {
			fmt.Println("NG: 半角の英数字と _ だけにしてください")
			return
		}
	}
	if mode == "snake" {
		// Go の regexp(RE2)には先読み・後読みが無いので、1文字ずつ前後を見て切れ目を決める
		var b strings.Builder
		for i := 0; i < len(s); i++ {
			c := s[i]
			if i > 0 && isUpper(c) {
				prev := s[i-1]
				// 小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
				if isLower(prev) || isDigit(prev) || (isUpper(prev) && i+1 < len(s) && isLower(s[i+1])) {
					b.WriteByte('_')
				}
			}
			b.WriteByte(c)
		}
		fmt.Println(strings.ToLower(b.String()))
		return
	}
	// strings.Split は「__」のところで空の部分を作る。空の部分は飛ばす
	// (strings.Title は単語の区切りの決まりが違い、Go 1.18 から非推奨なので使わない)
	var b strings.Builder
	for _, p := range strings.Split(s, "_") {
		if p == "" {
			continue
		}
		if b.Len() == 0 {
			b.WriteString(strings.ToLower(p))
		} else {
			b.WriteString(strings.ToUpper(p[:1]) + strings.ToLower(p[1:]))
		}
	}
	fmt.Println(b.String())
}

02. HTML の特殊文字をエスケープする

利用者が入力した文字を HTML に埋め込む前に、< > & " ' を &lt; などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →

escape.go1.27.2 で 8 / 8 件の出力が正解と一致

// 受け取った文字列の & < > " ' を、HTML の書き方(&amp; &lt; &gt; &quot; &#39;)に変える
package main

import (
	"fmt"
	"os"
	"strings"
)

// html.EscapeString は " を &#34; と書く(この回の &quot; と違う)ので、自分で置き換える。
// strings.NewReplacer は元の文字列を1回だけ見て置き換えるので、作った &lt; の & がもう一度変わることがない
var escaper = strings.NewReplacer(
	"&", "&amp;",
	"<", "&lt;",
	">", "&gt;",
	`"`, "&quot;",
	"'", "&#39;",
)

func main() {
	fmt.Println(escaper.Replace(os.Args[1]))
}

03. 順番を保って重複を消す

同じ値の2回目以降を消し、初めて出た順に残します。タグの一覧や、選んだ項目の一覧でよく使います。集合(Set)に入れて戻す書き方では、言語によって順番が変わったり、実行するたびに変わったりします。 ほかの言語・正解の例・解説 →

unique.go1.27.2 で 9 / 9 件の出力が正解と一致

// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
package main

import (
	"fmt"
	"os"
)

func main() {
	items := os.Args[1:]
	// map を range で回す順はわざと毎回ばらばらにしてある。見たかどうかだけ map で覚え、出す順はスライスに残す
	seen := map[string]bool{}
	out := []string{}
	for _, s := range items {
		if !seen[s] {
			seen[s] = true
			out = append(out, s)
		}
	}
	for _, s := range out {
		if s == "" {
			s = "(空)"
		}
		fmt.Println(s)
	}
	fmt.Printf("%d件(%d件を消した)\n", len(out), len(items)-len(out))
}

04. 日付と時刻を ISO 8601 で読み書きする

API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。 ほかの言語・正解の例・解説 →

iso8601.go1.27.2 で 15 / 15 件の出力が正解と一致

// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
package main

import (
	"fmt"
	"os"
	"regexp"
	"time"
)

// time.Parse は秒の小数が書式に無くても読み、タイムゾーンが無いのと形の違いを同じエラーにする。
// 返す言葉を分けるため、形は先に正規表現で決める
var iso = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})?$`)

func main() {
	s := os.Args[1]
	m := iso.FindStringSubmatch(s)
	if m == nil {
		fmt.Println("NG: 日時の形ではありません")
		return
	}
	if m[2] == "" {
		// タイムゾーンが無くても、日時として正しいかは先に確かめる(2月30日は「形が違う」を返す)
		if _, err := time.Parse("2006-01-02T15:04:05", s); err != nil {
			fmt.Println("NG: 日時の形ではありません")
			return
		}
		fmt.Println("NG: タイムゾーン(Z か +09:00 など)を付けてください")
		return
	}
	// RFC3339 は Z と ±HH:MM を読み、2月30日・25時はエラーにする。ただし時差の範囲は見ず「+09:60」も通すので、自分で確かめる
	t, err := time.Parse(time.RFC3339, s)
	if tz := m[2]; err != nil || tz != "Z" && (tz[1:3] > "23" || tz[4:6] > "59") {
		fmt.Println("NG: 日時の形ではありません")
		return
	}
	// time.Local を使わず、UTC と「+9時間だけのタイムゾーン」を作る(tzdata が無くても動く)
	jst := time.FixedZone("JST", 9*60*60)
	// Format は秒の小数を書式に入れなければ切り捨てて書く
	fmt.Println("UTC " + t.UTC().Format("2006-01-02T15:04:05Z"))
	fmt.Println("日本時間 " + t.In(jst).Format("2006年01月02日 15:04:05"))
}

05. アクセスログを集計する

Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。 ほかの言語・正解の例・解説 →

access_log.go1.27.2 で 8 / 8 件の出力が正解と一致

// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
package main

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"sort"
	"strconv"
)

// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
var logLine = regexp.MustCompile(`^\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)$`)

type row struct {
	sec    string
	secNum float64
	method string
	path   string
	status string
}

func main() {
	var rows []row
	bad := 0
	// bufio.Scanner は行の終わりの \n と、その手前の \r を取って返す。最後に改行が無い行も読む
	sc := bufio.NewScanner(os.Stdin)
	for sc.Scan() {
		line := sc.Text()
		if line == "" {
			continue
		}
		m := logLine.FindStringSubmatch(line)
		if m == nil {
			bad++
			continue
		}
		// 正規表現で「数字.数字」と確かめてあるので、ParseFloat は失敗しない
		n, _ := strconv.ParseFloat(m[5], 64)
		rows = append(rows, row{sec: m[5], secNum: n, method: m[1], path: m[2], status: m[3]})
	}
	// Scan が false になったのが読み終わりか失敗かは Err で見分ける(1行が 64KB を超えると失敗する)
	if err := sc.Err(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("件数 %d\n", len(rows))
	if bad > 0 {
		fmt.Printf("読めない行 %d\n", bad)
	}

	counts := map[string]int{}
	for _, r := range rows {
		counts[r.status]++
	}
	// map を range で回す順番は毎回わざと変えられている。キーを取り出して並べてから出す
	statuses := make([]string, 0, len(counts))
	for s := range counts {
		statuses = append(statuses, s)
	}
	sort.Strings(statuses)
	fmt.Println("ステータス")
	for _, s := range statuses {
		fmt.Printf("%s %d\n", s, counts[s])
	}

	fmt.Println("遅い順")
	// sort.Slice は安定でない(同じ秒の行の順が入れ替わることがある)。先に出た行を先に残すには SliceStable
	sort.SliceStable(rows, func(i, j int) bool { return rows[i].secNum > rows[j].secNum })
	for i, r := range rows {
		if i == 3 {
			break
		}
		fmt.Printf("%s %s %s\n", r.sec, r.method, r.path)
	}
}

06. URL 用の文字列(slug)を作る

記事のタイトルから「hello-world」のような URL の一部を作ります。ブログや商品ページのアドレスを、見て意味が分かる形にするときに使います。よく見かける書き方は、日本語のタイトルをそのまま URL に入れたり、空の slug を黙って返したりします。 ほかの言語・正解の例・解説 →

slug.go1.27.2 で 14 / 14 件の出力が正解と一致

// 文字列を URL に使える形(slug)に変える
package main

import (
	"fmt"
	"os"
	"regexp"
	"strings"
)

const maxLen = 32

// 残す文字を [a-z0-9] と書く。否定の文字クラスは RE2(Go の正規表現)にもある
var notSlug = regexp.MustCompile("[^a-z0-9]+")

func main() {
	// strings.ToLower は Unicode の決まりで変える(İ が「i」になる)。A〜Z だけを変える
	s := strings.Map(func(r rune) rune {
		if r >= 'A' && r <= 'Z' {
			return r + ('a' - 'A')
		}
		return r
	}, os.Args[1])
	s = strings.Trim(notSlug.ReplaceAllString(s, "-"), "-")
	// ここまでで半角の英数字と「-」だけになっているので、len(バイト数)=文字数
	if len(s) > maxLen {
		s = strings.TrimRight(s[:maxLen], "-")
	}
	if s == "" {
		fmt.Println("NG: URL に使える文字がありません")
		return
	}
	fmt.Println(s)
}

07. URL エンコードする

検索語や日本語の名前を、URL のクエリの値(`?q=...`)に入れられる形に変えます。よく見かける書き方は、空白を「+」にしたり、`!` や `~` を変えずに残したりして、受け取る側で別の文字列になります。 ほかの言語・正解の例・解説 →

url_encode.go1.27.2 で 15 / 15 件の出力が正解と一致

// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
package main

import (
	"fmt"
	"net/url"
	"os"
	"strings"
)

func main() {
	// QueryEscape が残す文字は RFC 3986 と同じだが、空白だけ「+」にする
	// 入力に入っていた「+」は %2B になっているので、あとから「+」を %20 に置き換えてよい
	s := strings.ReplaceAll(url.QueryEscape(os.Args[1]), "+", "%20")
	fmt.Println(s)
}

ほかの言語