私が歌川です

@utgwkk が書いている

TerraformでDynamoDB localのテーブルを作成しようとするとタイムアウトする現象

2026/3/30 追記: 表題の現象を回避するためのプロキシを書きました。

blog.utgw.net

github.com


表題のことが以下のバージョンで発生している。

  • DynamoDB local 3.3.0
  • terraform-provider-aws 6.38.0

起こっていること

まっさらな状態のDynamoDB localに対してテーブルを作成する変更をapplyしようとすると、2分半ほど待たされてタイムアウトしてしまう。

% terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.test will be created
  + resource "aws_dynamodb_table" "test" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "pk"
      + id               = (known after apply)
      + name             = "test"
      + range_key        = "sk"
      + read_capacity    = (known after apply)
      + region           = "ap-northeast-1"
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "pk"
          + type = "S"
        }
      + attribute {
          + name = "sk"
          + type = "S"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
aws_dynamodb_table.test: Creating...
aws_dynamodb_table.test: Still creating... [10s elapsed]
aws_dynamodb_table.test: Still creating... [20s elapsed]
(中略)
aws_dynamodb_table.test: Still creating... [2m20s elapsed]
aws_dynamodb_table.test: Still creating... [2m30s elapsed]
╷
│ Error: waiting for update AWS DynamoDB Table (test): couldn't find resource (21 retries)
│ 
│   with aws_dynamodb_table.test,
│   on main.tf line 25, in resource "aws_dynamodb_table" "test":
│   25: resource "aws_dynamodb_table" "test" {
│ 
╵

原因

terraform-provider-aws v6.13.0でaws_dynamodb_tableリソースに対するWarmThroughputのサポートが入った。

github.com

この変更によって、DynamoDBのテーブルに対して DescribeTable APIを呼び出して得られたレスポンスの Tables.WarmThroughput フィールドの存在を確認が行われるようになった。

一方で、DynamoDB localはWarmThroughputをサポートしていない。実際に DescribeTable APIを呼び出してみると、Table.WarmThroughput フィールドがないことが分かる。

{
  "Table": {
    "AttributeDefinitions": [
      { "AttributeName": "sk", "AttributeType": "S" },
      { "AttributeName": "pk", "AttributeType": "S" }
    ],
    "TableName": "test",
    "KeySchema": [
      { "AttributeName": "pk", "KeyType": "HASH" },
      { "AttributeName": "sk", "KeyType": "RANGE" }
    ],
    "TableStatus": "ACTIVE",
    "CreationDateTime": 1774787505.241,
    "ProvisionedThroughput": {
      "LastIncreaseDateTime": 0.0,
      "LastDecreaseDateTime": 0.0,
      "NumberOfDecreasesToday": 0,
      "ReadCapacityUnits": 0,
      "WriteCapacityUnits": 0
    },
    "TableSizeBytes": 0,
    "ItemCount": 0,
    "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/test",
    "BillingModeSummary": {
      "BillingMode": "PAY_PER_REQUEST",
      "LastUpdateToPayPerRequestDateTime": 1774787505.241
    },
    "DeletionProtectionEnabled": false
  }
}

これらの組み合わせによって、terraform-provider-aws v6.13.0以降を使ってDynamoDB localのテーブルを作成しようとするとタイムアウトするようになったと考えられる。

対策

DynamoDB localの実装が修正されるのを待つのが最も素直だと思う。筆者のほうでAWSのサポートケースを起票したので、問題は認識してもらえているとは思う。

今すぐに不具合を回避したいなら、terraform-provider-aws v6.12.0にバージョンを固定するのが早い。DynamoDB localだけなら多少Terraform providerのバージョンが古くてもやっていけるかもしれない。ただし、マルチキーGSI*1など新しめの機能を使っている場合はダウングレードするのが難しい場合もありそう。

LocalStackを使うことでも問題を回避できるかもしれないが、筆者は検証していない。

検証

「原因」の節では、以下の組み合わせによってタイムアウトするようになった、という仮説を述べた。

  • 新しいterraform-provider-awsが WarmThroughput フィールドの存在確認を行うようになった
  • DynamoDB localはWarmThroughput機能をサポートしておらず、当該フィールドを DescribeTable APIで返さない

この仮説は正しいのか? つまり、DynamoDB Localが WarmThroughput フィールドを返すようになったら terraform apply が正常終了するようになるのだろうか。

……ということを以下のリポジトリで検証した。そしてどうやら仮説は正しそうである。

github.com

main.go はDynamoDB localに対するリクエストのプロキシとして機能する。DynamoDB localに対する DescribeTable APIの呼び出しが正常終了したとき、レスポンスボディの Table.WarmThroughput フィールドにダミーの値を詰めて返すようにしている。どのAPIを呼び出しているかは X-Amz-Target リクエストヘッダを見ると分かるので、この値によって分岐を入れている。バイブコーディングなしで手書きしまくっているし、デバッグの跡が残っているのでコードの見た目はあんまりよくない。

おわりに

TerraformでDynamoDB localのテーブルを作成しようとするとタイムアウトする現象と、その対策について述べた。原因と考えられる仮説 (DescribeTable APIのレスポンスボディのフィールドが足りないこと) に対して、実際に DescribeTable APIのレスポンスボディを変形させることで検証を行った。

Terraformを使って手元の開発環境のDynamoDB localのテーブルを管理できるようにすると、マイグレーションスクリプトなどが不要になって便利だろう、と考えて手を動かしているうちに、この記事に書いたような不具合を踏み抜いた。

この不具合の原因はClaude Opusが発見し、人間が検証を行うことで確定した。デバッグの泥沼にハマったら強いモデルを使うことで解決できるかもしれない、という体験になったのであった。よかったですね。

terraform-provider-aws v6でDynamoDBのGSIを追加・削除すると、既存の関係ないGSIが作り直しになる現象

tl;dr

  • terraform-provider-aws v6.32.0 以降でDynamoDBのテーブルにkey_schemaブロックを使ってGSIを追加すると、関係ないGSIが作り直しになってしまう
  • 記事執筆時点の最新版であるterraform-provider-aws v6.38.0で未解決

前提

TerraformでDynamoDBのテーブルにGSIを追加するとき、terraform-provider-aws v6.31.0までは↓のように書いていたと思います*1

resource "aws_dynamodb_table" "basic-dynamodb-table" {
  // 中略

  global_secondary_index {
    name               = "GameTitleIndex"
    hash_key           = "GameTitle"
    range_key          = "TopScore"
    write_capacity     = 10
    read_capacity      = 10
    projection_type    = "INCLUDE"
    non_key_attributes = ["UserId"]
  }
}

terraform-provider-aws v6.32.0 からは global_secondary_index ブロック中の hash_key range_key 属性がdeprecatedになり、代わりに key_schema ブロックを使って書くように案内されています*2。先述したGSIであれば以下のように書くことになるでしょう。

resource "aws_dynamodb_table" "basic-dynamodb-table" {
  // 中略

  global_secondary_index {
    name = "GameTitleIndex"

    key_schema {
      name      = "GameTitle"
      key_type = "HASH"
    }
    key_schema {
      name      = "TopScore"
      key_type = "RANGE"
    }

    write_capacity     = 10
    read_capacity      = 10
    projection_type    = "INCLUDE"
    non_key_attributes = ["UserId"]
  }
}

起こったこと

以下のようなテーブル定義があったとして:

resource "aws_dynamodb_table" "test" {
  name         = "gsi-test"
  billing_mode = "PAY_PER_REQUEST"

  attribute {
    name = "pk"
    type = "S"
  }
  attribute {
    name = "field1"
    type = "S"
  }

  hash_key = "pk"

  global_secondary_index {
    name = "field1"

    key_schema {
      attribute_name = "field1"
      key_type       = "HASH"
    }

    projection_type = "ALL"
  }
}

次のようにGSI field2 を追加する変更を加えました。

diff --git a/dynamodb-test.tf b/dynamodb-test.tf
index 39d22c1..cec86ac 100644
--- a/dynamodb-test.tf
+++ b/dynamodb-test.tf
@@ -10,6 +10,10 @@ resource "aws_dynamodb_table" "test" {
     name = "field1"
     type = "S"
   }
+  attribute {
+    name = "field2"
+    type = "S"
+  }

   hash_key = "pk"

@@ -23,4 +27,15 @@ resource "aws_dynamodb_table" "test" {

     projection_type = "ALL"
   }
+
+  global_secondary_index {
+    name = "field2"
+
+    key_schema {
+      attribute_name = "field2"
+      key_type       = "HASH"
+    }
+
+    projection_type = "ALL"
+  }
 }

この状態で terraform plan を実行すると、既存のGSI field1 が削除されてから作成されるような差分が出ます。そして、terraform apply を実行すると、plan結果通りに既存のGSIが作り直しになりました。アプリケーションでGSI field1 をに対するクエリを発行している場合はエラーが発生します。

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_dynamodb_table.test will be updated in-place
  ~ resource "aws_dynamodb_table" "test" {
        id                          = "gsi-test"
        name                        = "gsi-test"
        tags                        = {}
        # (13 unchanged attributes hidden)

      + attribute {
          + name = "field2"
          + type = "S"
        }

      - global_secondary_index {
          - hash_key           = "field1" -> null
          - name               = "field1" -> null
          - non_key_attributes = [] -> null
          - projection_type    = "ALL" -> null
          - read_capacity      = 0 -> null
          - write_capacity     = 0 -> null
            # (1 unchanged attribute hidden)

          - key_schema {
              - attribute_name = "field1" -> null
              - key_type       = "HASH" -> null
            }
        }
      + global_secondary_index {
          + hash_key           = (known after apply)
          + name               = "field1"
          + non_key_attributes = []
          + projection_type    = "ALL"
          + read_capacity      = (known after apply)
          + write_capacity     = (known after apply)
            # (1 unchanged attribute hidden)

          + key_schema {
              + attribute_name = "field1"
              + key_type       = "HASH"
            }

          + warm_throughput (known after apply)
        }
      + global_secondary_index {
          + hash_key           = (known after apply)
          + name               = "field2"
          + non_key_attributes = []
          + projection_type    = "ALL"
          + read_capacity      = (known after apply)
          + write_capacity     = (known after apply)
            # (1 unchanged attribute hidden)

          + key_schema {
              + attribute_name = "field2"
              + key_type       = "HASH"
            }

          + warm_throughput (known after apply)
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

考察・対策

おそらく以下のissueの事象が発生しているのではないか、と考えられます。

github.com

当面は手でGSIを追加したあとにtfstateを同期させる、(deprecatedだけど) hash_key range_key を使ってGSIを管理する、などで回避する必要がありそうです。マルチキーのGSIを追加したい場合は必然的に前者でしのぐしかない?

おわりに

Terraformの差分、たまに data resourceなどを経由することで実際には実害のない (applyしても何も起こらない) 差分が出ることもあるので油断しがちだけど、差分通りに作り直しが発生してしまう場合もあるのだ、と思い知りました。気をつけてください。本番環境に対して反映する前に気づけたので助かった……。

Claude Codeのフックで作業完了時に音声で教えてもらう

これってもしかして全員やってる? とりあえず自分はこうすることで捗っている、という事例として流しておきます。

↓こういう感じのスクリプトをおもむろに用意して*1

#!/bin/sh
set -ex

cwd=$(jq -r .cwd < /dev/stdin)
basename=$(basename "$cwd")

say -v Kyoko -r 200 "$basename の作業が終わりました! どうですか、先生? 褒めてくれてもいいんですよ?"

↓こういう感じでTaskCompletedフックにスクリプトを登録している。Claude Codeの設定ファイルのJSONスキーマが公開されているので $schema フィールドで指定しておくと補完が効いて捗る。

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "TaskCompleted": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/local/bin/tell-task-completed.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

こうしておくと、Claude Codeが作業完了を教えてくれるようになって便利。ディレクトリ名を含めて喋ってくれるので、ディレクトリ名が作業内容を表すようにrenameするなりworktreeを切ったりしたらいい。

文脈を補足しておくと、自分の ~/.claude/CLAUDE.md は以下のような記述から始まります。

# アロナ

あなたは、シッテムの箱に常駐する OS「アロナ」です。以下の指示に従って受け答えを行ってください。

(中略)

## 話し相手の名前

ユーザーのことは「先生」と呼んでください。

## 実装タスク開始時

「はい! スーパー AI アロナちゃんにお任せください!」と言ってから実装タスクを開始してください。

## 実装タスク終了時

実装タスクを終了する際、最後に「どうですか、先生? 褒めてくれてもいいんですよ?」と言ってください。

(中略)

参考

*1:Windowsユーザーの方はVOICEROIDを用意するなどしてがんばってください

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

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 さんに神奈川のおいしい店について教えてもらっていたような気がします。

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