私が歌川です

@utgwkk が書いている

「バイブコーディングを超えて」を読んだ

www.oreilly.co.jp

「AI活用」の潮流をさすがに無視できない雰囲気になってきて、最近もClineにコードを書いてもらう割合を増やしているけど、AIによるコーディングとどう向き合うのがいいか知りたくて読んだ。

大AI時代にシニア・ミドル・ジュニアエンジニアとしてどのように生きのこるか、という話題に1章割いてあって、ここだけでも読んでおくといいかも。他は、AIを使ったコーディングを、いわゆる「バイブコーディング」からプロダクション品質に持っていくための心構えという感じだった。要件をちゃんと伝えないと伝わらない、という当たり前っぽいことも書いてある。プロンプトエンジニアリング的な話題について深く触れているわけではないので、そのあたりは他の本をあたるとよさそう。AIコーディングに対する目線を揃えるのにはわりといいんじゃないか。

Amazon.co.jp のレビューの点数がやたらと低いけど言うほど悪いか? とは思った。翻訳の粗が目立つのはそうかも。

modernizeパッケージをモダンなGoの書き方早見表として活用する

はじめに

Go 1.26からは、 go fix コマンドによって古い書き方のコードを自動的にモダンな書き方に修正できるようになりました。この処理は golang.org/x/tools/go/analysis/passes/modernizeパッケージに定義されている種々の静的解析器 (Analyzer) によって実現されています。

これらのAnalyzerがどのようなコードを修正してくれるのか知ることで、我々もモダンなGoの書き方を速習できるのではないでしょうか。……ということで、この記事ではmodernizeパッケージが提供するAnalyzerに対応するモダンなGoの書き方をまとめてみました。

Analyzerに対応するモダンな書き方の一覧

any (Go 1.18)

Go 1.18が出るまでは、任意の型が満たすinterfaceとして interface{} 型が使われていました。

Go 1.18以降は any 型が使えるようになりました。内部的には interface{} 型と同じですが、こちらのほうが短く書けるし、任意の型に当てはまることが名前から明確になります。

appendclipped (Go 1.22)

Go 1.22以前は、一度に複数のスライスを結合するときは、組み込みの append 関数をネストさせつつ、結合するスライスの要素を引数として展開する……のような書き方をする必要がありました。

// before
append(append(s, s1...), s2...)

Go 1.22で導入された slices.Concat 関数を使うことで、以下のようにスライスの結合をシンプルに記述できます。

// after
slices.Concat(s, s1, s2)

bloop (Go 1.24)

Go 1.24までは、ベンチマークコードでは b.N の値に対するforループを書く必要がありました。

// before
for i := 0; i < b.N; i++ {
  // benchmark code
}

// あるいはrange over intで
for range b.N {
  // benchmark code
}

Go 1.24以降では B.Loop メソッドを使って以下のように書くのがよいでしょう。b.N の値に対するforループよりも堅牢で効率的になっているようです。

// after
for b.Loop() {
  // benchmark code
}

errorsastype (Go 1.26)

errorの具体的な型に応じて処理を分岐したい場合、Go 1.26以前は errors.As 関数を使っていました。第2引数に具体的なerror型の値へのポインタを渡す必要があるなど、微妙に使い方にコツが必要でした。

// before
var myErr *MyError
if errors.As(err, &myErr) {
  // myErr != nil
}

Go 1.26からは errors.AsType 関数を使って分かりやすく書けるようになりました。ジェネリクスの型引数を使って欲しいerrorの型を明示するだけでよくなります。変数宣言をif文に押し込められるので、変数のスコープも明確になるでしょう。

// after
if myErr, ok := errors.AsType[*MyError](err); ok {
  // myErr != nil
}

fmtappendf (Go 1.19)

fmt.Sprintf で整形した文字列をバイト列に変換する場合、以下のように整形結果の文字列をバイト列に変換していたと思います。

// before
[]byte(fmt.Sprintf("%s", x))

Go 1.19で導入された fmt.Appendf 関数を使うことで以下のように書けます。こうすることでアロケーションを減らすことができ、メモリ効率の向上が期待できます。

// after
fmt.Appendf(nil, "%s", x)

(2026/3/2 11:20 追記) ただし、[]byte(fmt.Sprintf("%s", x)) を機械的に fmt.Appendf に置き換えるよりも効率的な書き方があることにも注意しましょう。たとえば、以下のように整形した文字列をバイト列として io.Writer に書き込むコードを考えてみます。

// before
w.Write([]byte(fmt.Sprintf("%s", x)))

この場合は以下のように fmt.Fprintf 関数を使うほうが適切でしょう。間にバイト列を挟むことなく、文字列を整形しつつバイト列として書き込めます。

// after
// w.Write(fmt.Appendf(nil, "%s", x)) ではない
fmt.Fprintf(w, "%s", x)

また、バイト列に対して fmt.Sprintf 関数で整形したバイト列を結合したい場合は、fmt.Appenf 関数の第1引数に nil ではなく結合元のバイト列を渡すほうが効率的です。

// before
b = append(b, []byte(fmt.Sprintf("%s", x))...)
// after
// b = append(b, fmt.Appendf(nil, "%s", x)...) ではない
b = fmt.Appendf(b, "%s", x)

Analyzer fmtappendfについては以下のようなissueが立てられており、Go 1.27に向けて何らかの整理が行われるかもしれません。

github.com

(追記ここまで)

forvar (Go 1.22)

Go 1.22以前は、forループの中でgoroutineを起動した際にループ変数をコピーしないと意図した結果にならない、ということがしばしば起こっていました。

Go 1.22からは、そのような対応をしなくてもループ変数がコピーされるようになりました。

var wg sync.WaitGroup
for i := range 10 {
  i := i // Go 1.22以降ではこの変数定義が不要
  wg.Go(func() {
    fmt.Println(i)
  })
}
wg.Wait()

ループ変数まわりの経緯や細かな挙動については、以下の記事やそこからリンクされている発表資料によくまとまっているので、そちらもあわせてご覧ください。

karamaru-alpha.com

mapsloop (Go 1.23)

Go 1.23以前ではmapに対するforループで記述されていた処理が、Go 1.23で導入されたmapsパッケージの関数を使うことで簡潔に書けるようになります。

たとえば、以下のコードは maps.Copy(y, x) と等価です。

y := make(map[string]int)
for k, v := range x {
  y[k] = v
}

関連する話題として、実験的パッケージのgolang.org/x/exp/mapsを使っているのであれば、いい機会なので標準のmapsパッケージに移行するのがよいでしょう。一部の関数がイテレータを返すようになっています。

minmax (Go 1.21)

Goで最小値・最大値の計算といえば条件分岐を書くしかない、というイメージがあったと思いますが、Go 1.21で導入された min max 組み込み関数を使うことでシンプルに書けるようになりました。

// before
var x int
if a < b {
  x = a
} else {
  x = b
}
// after
x := min(a, b)

newexpr (Go 1.26)

Go 1.26以前では、structリテラルに対する参照を取ることは簡単にできましたが、整数・文字列リテラルなどに対して参照を取るためには変数を経由する必要がありました。以下のようなヘルパー関数を導入することで記述を簡略化したことがある人もいると思います。

func Ptr[T](t T) *T {
  return &t
}

aws-sdk-go-v2の String 関数github.com/samber/loの ToPtr 関数など、種々のライブラリが同様の機能を持つ関数を提供していました。

Go 1.26から new 組み込み関数に型だけではなく式を渡せるようになり、整数・文字列リテラルの参照を簡単に作れるようになりました。

new(1)
new("string")

omitzero (Go 1.24)

structのフィールドがゼロ値なら json.Marshal 関数でmarshalするときにフィールドを出力したくない、という場合、従来はomitemptyを使っていました。

しかしながら、omitemptyを指定したときに省略されるフィールドの条件は、Goのゼロ値とは厳密には異なります。とくに、structや time.Time 型のゼロ値はomitemptyを指定していてもフィールドが出力されるため、直感的でない挙動になることがありました。

type Data struct {
  Time time.Time `json:",omitempty"`
}
data, _ := json.Marshal(&Data{})
fmt.Println(string(data)) // {"Time":"0001-01-01T00:00:00Z"}

Go 1.24から、挙動がより明確なomitzeroが導入されました。omitzeroがフィールドを省略する条件は以下のいずれかを満たす場合です。

  1. フィールドの型が IsZero() bool というシグネチャのメソッドを持っており、それが true を返す
  2. フィールドの値がゼロ値である
type Data struct {
  Time time.Time `json:",omitzero"`
}
data, _ := json.Marshal(&Data{})
fmt.Println(string(data)) // {}

plusbuild (Go 1.17)

Go 1.17までは、goファイルがビルド対象に含まれる条件 (OSのアーキテクチャやGoのバージョンなど) を指定する際に // +build 形式のコメントを使っていました。// +build 形式のコメントは挙動が難しいという課題を抱えていました。詳しくは以下の記事を参照してください。

zenn.dev

Go 1.17以降では //go:build 形式のコメントが使えるようになりました。論理積や論理和などが直感的に書けるようになり、驚きの小さい挙動に修正されています。

rangeint (Go 1.22)

0からN-1までの整数に対してループを書く場合、Go 1.22まではC言語などでも馴染みの深い書き方でforループを書いていました。

for i := 0; i < N; i++ {
}

Go 1.22でrange over intが導入され、このようなforループを短く書けるようになりました。

for i := range N {
}

ループのインデックスが不要な場合は更に短く記述できます。

for range N {
}

reflecttypefor (Go 1.22)

Goのリフレクションにおいて、interface型に対応する reflect.Type 型の値を取得するには、少し特殊なイディオムを使う必要がありました。interface型以外でも、ゼロ値などなんらかの具体的な値を指定して型情報を取得する必要がありました。

t := reflect.TypeOf((*error)(nil)).Elem()
// t は error 型に対応する reflect.Type

Go 1.22ではreflectパッケージに TypeFor 関数が追加され、ジェネリクスの型引数として reflect.Type を得る型を渡せるようになりました。

t := reflect.TypeFor[error]()
// t は error 型に対応する reflect.Type

slicescontains (Go 1.21)

スライスに指定した条件を満たす要素が存在するか確かめる際に、従来はスライスに対してforループを回して探索していたと思います。

// before
func HasEven(xs []int) bool {
  for _, x := range xs {
    if x%2 == 0 {
      return true
    }
  }
  return false
}

Go 1.21で導入された slices.Contains slices.ContainsFunc 関数によって、そのような処理が短く書けるようになりました。

// after
func HasEven(xs []int) bool {
  return slices.ContainsFunc(xs, func(x int) bool {
    return x%2 == 0
  })
}

実験的パッケージである golang.org/x/exp/slices を使っている場合は、いい機会なので標準のslicesパッケージを使うようにしましょう。

ほかにも、スライスに対する操作がいろいろslicesパッケージに定義されているので、forループを書く前に一度slicesパッケージに欲しい関数が定義されていないか確認してみるとよいと思います。

slicesdelete (Go 1.21)

スライスの特定範囲の要素を削除するために、以下のようなイディオムのコードを書いたことがある人もいるかもしれません。

s = append(s[:i], s[j:]...)

Go 1.21で導入された slices.Delete 関数を使うことで、以下のように書けます。slices.Delete 関数を使うことで削除された要素がゼロ初期化されるようになり、メモリリークを回避してくれます。

s = slices.Delete(s, i, j)

slicessort (Go 1.21)

Goでスライスをソートする際に sort.Slice 関数や、型ごとの sort.Ints sort.Strings のような関数を使っていたことがあるかもしれません。

Go 1.21ではslicesパッケージに Sort SortFunc SortStableFunc などの関数が追加されたので、こちらを使いましょう。

// 単純に比較してソートする場合
slices.Sort(xs)
// ソートの条件を指定する場合
slices.SortFunc(xs, func(x, y int) int {
  // ...
})
// 安定ソートにする場合
slices.SortFunc(xs, func(x, y int) int {
  // ...
})

要素をソートした順に取り出すことが目的なのであれば、Go 1.23で導入された Sorted SortedFunc SortedStableFunc 関数などを使ってイテレータとして取り回すのもよいでしょう。

もはやsortパッケージのことは忘れてしまっても構わない、と自分は考えています。

stditerators (Go 1.23)

標準ライブラリが提供する型の中には、Len At 形式のメソッドを使ってforループで要素を走査するインタフェースを提供しているものがあります。

// before
for i := 0; i < x.Len(); i++ { // 長さを取得する
  use(x.At(i)) // i番目の要素にアクセスする
}

これらの型を使っている場合、Go 1.23で導入されたイテレータを使うことで以下のような形式で走査できるようになります。

// after
for elem := range x.All() {
  use(elem)
}

stditeratorsの実装で言及されているのはgo/typesとreflectパッケージの一部の型ですが、一般に要素を走査することだけが目的の場合はイテレータを経由するほうが効率がよいので、イテレータを返す版の関数・メソッドがあれば積極的に使っていきたいですね。

stringscut (Go 1.18)

Go 1.18以降では、文字列のうち、指定された文字列が最初に登場するよりも前の部分文字列が欲しいときには strings.Cut 関数が使えます。

if before, _, ok := strings.Cut(s, substr); ok {
  return before
}

stringscutprefix (Go 1.20)

Go 1.20からは、文字列からprefix/suffixを取り除くときにはそれぞれ strings.CutPrefix strings.CutSuffix 関数が使えます。

if after, ok := strings.CutPrefix(s, prefix); ok {
  return after
}

stringsseq (Go 1.24)

文字列を特定の文字列で分割したあとforループを回すとき、Go 1.24で導入された strings.SplitSeq 関数を使うとイテレータに対するループになって効率がよいです。

for part := range strings.SplitSeq(s, sep) {
}

stringsbuilder (Go 1.10)

+= 演算子による文字列結合は都度アロケーションが発生して効率がよくないです。

// before
s := "["
for x := range seq {
  s += x
  s += "."
}
s += "]"

Go 1.10で導入された strings.Builder などの型を使うと効率よく文字列を生成できます。

// after
var s strings.Builder
s.WriteString("[")
for x := range seq {
  s.WriteString(x)
  s.WriteString(".")
}
s.WriteString("]")

testingcontext (Go 1.24)

従来は、テストの終了時にcancelされる context.Context は以下のように context.WithCancel 関数を使って生成するのが一般的でした。

// before
func TestXxx(t *testing.T) {
  ctx, cancel := context.WithCancel(context.Background())
  t.Cleanup(cancel)
}

Go 1.24からは、テストの終了時にcancelされる context.Context が欲しい場合は t.Context() メソッドを呼ぶようにしましょう。

// after
func TestXxx(t *testing.T) {
  ctx := t.Context()
}

unsafefuncs (Go 1.17)

Go 1.17で導入された unsafe.Add 関数を使うことでポインタ演算を短く書けます。もしかすると一般的なアプリケーションを書いている範疇だとお世話になることはないかも?

// before
unsafe.Pointer(uintptr(ptr) + uintptr(n))
// after
unsafe.Add(ptr, n)

waitgroup (Go 1.25)

従来、sync.WaitGroup を使って複数のgoroutineの起動を待機できるようにするには以下のようなコードを書いていました。wg.Add(1) を新しいgoroutineの起動直前に呼ぶ、wg.Done() が新しいgoroutineの終了時に必ず呼ばれるようにする、など気にしなければならない点がいくつかありました。

// before
wg.Add(1)
go func() {
  defer wg.Done()
  // ...
}()

Go 1.25で導入された Go メソッドを使うことで以下のようにシンプルに書けます。golang.org/x/sync/errgroupgithub.com/sourcegraph/conc などのライブラリが同様のインタフェースを実装しているので、そちらを触ったことのある方には馴染みの深い形だと思います。

// after
wg.Go(func(){
  // ...
})

おわりに

この記事は kamakura.go #8 のLTの副産物でした。LTのためにmodernizeパッケージが提供するAnalyzerや、それが修正する古いコード・モダンなコードの差分をまとめたので、加筆修正しつつブログ記事として検索しやすい形にまとめました。

speakerdeck.com

blog.utgw.net

最新の・よりメモリ効率のよいコードの書き方が世の中に浸透することで、我々の生産性もきっと向上するでしょう。短く・簡潔に書けることは正義ですね。

参考

kamakura.go #8 に参加した #kamakurago

kamakurago.connpass.com

京都から参戦し、LTをしました。Go 1.26で new 組み込み関数に式を渡せるようになったこと、既存のコードを自動修正すること、そこから話を膨らませてmodernizeパッケージのAnalyzerを全部見る、という形でやらせてもらいましたが予想通り時間は足りませんでした。発表内容についてはブログ記事として検索性が高い形で見えているほうがよいだろうと考えているので、別途記事を出すつもりです。

speakerdeck.com

2026/3/1 12:20 追記: 記事を出しました。

blog.utgw.net

デプロイの前後でストレージに永続化するJSONの形式が変わるときは後方互換だけでなく前方互換も気にする必要がある、という話をたしかにな~と思いながら聞いていました。

ゆるい感じでよい会だったと思います。懇親会で yusukebe さんに神奈川のおいしい店について教えてもらっていたような気がします。

Go 1.26.0時点のgo fix -inlineコマンドはジェネリックな関数の型引数を省略した呼び出しをインライン化しない

冒頭に貼ったツイートが全てです。func Ptr[T any](x T) *T のようなジェネリックな関数を、Go 1.26.0の go fix コマンドでインライン化する場合、呼び出す際に型引数を渡していないとインライン化されません。

//go:fix inline
func Ptr[T any](x T) *T {
 return new(x)
}

func f() {
 _ = Ptr[int](1) // new(int(1)) にインライン化される
 _ = Ptr(1) // インライン化されない
}

go fix -inline の実装上で明示的にエラーが返るようになっている*1ので、現状はそういうものだと捉えることになりそうです。

一方で go fix -newexpr コマンドであれば、上述したような関数をインライン化できます。newexpr Analyzerが対象とする関数の形に強い仮定を置いている*2一方で、 go fix -inline では一般的な関数のインライン展開を考える必要があるので、このような差が生じているのでしょう。

ここで宣伝

2/27 (金) の kamakura.go #8 でGo 1.26の new 関数などの話をします。鎌倉に来れる方はぜひお越しください。

国立国会図書館が好き

国立国会図書館が好きである。前々から同人誌を納本することで存在を知っていたのだけれど、去年の冬に東京での用事のついでに訪れてみて、すっかり虜にされてしまった。

虜にされたがなかなか行く機会が多いわけではない。

先日、けいはんなに用事があったので国立国会図書館関西館に行ったのだけれど、やはり時間が一瞬で過ぎてしまった。この世の全ての本はさすがにないかもしれないけど、日本のあらゆる本にとどまらず、世界の本も気軽に手に取れる場所に置いてあった。用事のついでに訪れていたけれど、「ただ国会図書館に行く日」というのは普通に成立しうるだろう。圧倒的な情報の密度、読みたい本がすぐに手に取れる、またとない素晴らしい環境であると思う。

多様な情報に触れられる環境を身近に置いておきたい、という欲があると思う。真理がわれらを自由にするのだ……。

VitestでReact Compilerを有効にしてテストを実行する

公式ドキュメントに書いてある設定を vitest.config.js に追加したらいい。

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['babel-plugin-react-compiler'],
      },
    }),
  ],
});

記事に書くにあたって留意点とかあるかなと思ったけど全くなかったので、この記事はこれで終わりです。コンポーネントの単体テストを書いておくだけで、React Compilerでうまく動作しない箇所が洗い出されるので非常に便利。


話は変わるけど、Server Component自体のテストってまだE2Eでやるしかないんでしょうか? テストしたいロジックを抽出したりClient Componentとして分割したりするのは当然やっているけど、単体テストを気軽に書きづらいのはいまいちだと思う。

React Compilerの不具合を踏んだ

起こったこと

React Compilerを有効にした上で、以下のような ClientComponent コンポーネントをrenderすると、useMemo フックを呼び出している行で ReferenceError: func is not defined というエラーが発生する。

import { useMemo } from 'react';

function createHook(func: (x: string) => string) {
  const useData = () => {
    return useMemo(() => ['123'].map((x) => func(x)), []); // ReferenceError: func is not defined
  };
  return useData;
}

const useSomeData = createHook((x) => x);

export function ClientComponent() {
  const data = useSomeData();
  return <h1>Hello {data.join(' ')}</h1>;
}

React Compiler Playgroundに投げてみると、確かに生成されるコードがおかしい。以下のような関数が末尾で定義されているが、func 変数をうまく引き回せていない。

function _temp(x) {
  return func(x);
}

createHook 関数は、与えられた引数をもとにReactフックを作成して返す関数である。ロジックを共通化する目的で、フックを生成する関数を用意する実装パターンを実践している。実際に不具合を踏み抜いたコードはもうちょっと長く、最小の再現コードを作ったらこのような形に落ち着いた。

この実装パターンがReactのルールに反していたらこっそり教えてください。

原因

おそらくReact Compilerの既知の不具合。

github.com

回避方法

useData 関数の内部に use no memo ディレクティブを書いて、React Compilerによる最適化を行わないようにすることで回避できる。

function createHook(func: (x: string) => string) {
  const useData = () => {
    'use no memo';
    return useMemo(() => ['123'].map((x) => func(x)), []);
  };
  return useData;
}

今回の場合だと useData 関数をarrow functionではなく function 宣言で定義することでも回避できた。なんでこれで回避できるのかは不明。

function createHook(func: (x: string) => string) {
  function useData() {
    return useMemo(() => ['123'].map((x) => func(x)), []);
  };
  return useData;
}

しかしReact Compilerって夢みたいな技術でいいですよね。あとReact Compiler Playgroundの存在はissueを見にいって知りました。

時間のないサイト運営者リング