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 に埋め込む前に、< > & " ' を < などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →
escape.go1.27.2 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
package main
import (
"fmt"
"os"
"strings"
)
// html.EscapeString は " を " と書く(この回の " と違う)ので、自分で置き換える。
// strings.NewReplacer は元の文字列を1回だけ見て置き換えるので、作った < の & がもう一度変わることがない
var escaper = strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
`"`, """,
"'", "'",
)
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)
}