Slack Block Kitの制約とデザインパターンガイド

はじめに

前回の記事「GoでSlack通知を実装する方法」では、Slack通知の基本的な実装方法とBlock Kitの初歩的な使い方について解説しました。

今回は、Slack Block Kitをより深く掘り下げて、その特性、制約、そして効果的なデザインパターンについて詳しく説明します。特に、Block Kitの「見た目があまり変えられない」という特性と、その制約の中でいかに美しく機能的なメッセージを作成するかに焦点を当てます。

Slack Block Kitの特性と制約

1. デザインの統一性と制約

Slack Block Kitの最も大きな特徴は、デザインの自由度が意図的に制限されていることです。これにはいくつかの理由があります。

  • 一貫性の確保: すべてのアプリからのメッセージが統一された見た目になる
  • 可読性の向上: Slackの標準UIパターンに従うことで、ユーザーが迷わない
  • アクセシビリティ: スクリーンリーダーやキーボードナビゲーションに配慮

2. 制約の具体例

以下のような点でカスタマイズが制限されています。

  • 色の変更: テキストやブロックの背景色は基本的に変更不可
  • フォントサイズ: 固定のフォントサイズ体系
  • 余白・レイアウト: ブロック間の余白やレイアウトは Slack側で制御
  • アニメーション: 動的な効果は実装不可

これらの制約があるからこそ、コンテンツの構成とブロックの組み合わせが重要になります。

Block Kitの主要ブロックタイプ解説

実際のサンプルコードを基に、各ブロックタイプの特性と使用例を詳しく見てみましょう。

1. Header ブロック

1
2
3
4
5
6
7
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Slack Block Kitデザインシステム見本"
}
}

特徴:

  • メッセージの最上部に配置される大きなタイトル
  • plain_textのみ対応(Markdownは使用不可)
  • 絵文字を使用することで視覚的なアクセントを追加可能

使用場面:

  • 通知のタイトル

2. Section ブロック

1
2
3
4
5
6
7
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*1. 基本的なセクション*\nこれは基本的なセクションブロックです。*太字*や_斜体_、~取り消し線~、`コード`などのMarkdown書式が使えます。"
}
}

特徴:

  • 最も汎用的で使用頻度が高いブロック
  • Markdownによる豊富なテキスト装飾
  • accessoryフィールドで画像やボタンを右側に配置可能

応用例 - 画像付きセクション:

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "画像を右側に配置できます"
},
"accessory": {
"type": "image",
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png",
"alt_text": "かわいい犬の画像"
}
}

3. Context ブロック

1
2
3
4
5
6
7
8
9
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "👆 Contextブロックは小さいテキストで補足情報を表示するのに最適です"
}
]
}

特徴:

  • 小さなフォントサイズで表示
  • 補足情報や注釈に最適
  • 複数の要素を横並びで配置可能

使用場面:

  • タイムスタンプ
  • 作成者情報
  • 追加の説明文

4. Divider ブロック

1
2
3
{
"type": "divider"
}

特徴:

  • シンプルな水平線
  • セクション間の視覚的な区切り
  • パラメータ不要で最もシンプルなブロック

5. Actions ブロック

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Primary ボタン",
"emoji": true
},
"style": "primary",
"value": "primary_button",
"url": "https://example.com/primary"
}
]
}

特徴:

  • ボタンやその他のインタラクティブ要素を配置
  • 最大5つの要素まで横並び配置可能
  • 3つのボタンスタイル:default(境界線のみ)、primary(青色)、danger(赤色)

制約の中での効果的なデザインパターン

1. 疑似的な枠線の実装

Block Kitには明確な「枠線」がありませんが、以下のようなテクニックで視覚的なグループ化を実現できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{		
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*2. 擬似的な枠線付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```項目:値```"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```ステータス:完了```"
}
}

工夫のポイント:

  • Markdownを使用した疑似的な枠線

2. 状態表示のパターン

色が変更できない制約の中で、絵文字とテキストを組み合わせて状態を表現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/goals|目標の基本を学ぶ>*\n :alarm_clock: 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/personal-goals|個人目標設定の方法を学ぶ>*\n :warning: 2024年9月24日(期限超過)"
}
}

使用される表現方法:

  • :alarm_clock: - 通常の期限
  • :warning: - 期限超過や注意
  • :white_check_mark: - 完了
  • :x: - エラーやキャンセル

3. 複数ボタンのレイアウトパターン

Actionsブロックでは、最大5つまでのボタンを配置できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "確認",
"emoji": true
},
"style": "primary",
"value": "confirm"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "後で",
"emoji": true
},
"value": "later"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "キャンセル",
"emoji": true
},
"style": "danger",
"value": "cancel"
}
]
}

デザインの考慮点:

  • Primary(青色)は最重要アクション用
  • Default(境界線のみ)は通常アクション用
  • Danger(赤色)は削除や危険なアクション用

まとめ

Slack Block Kitは確かに「見た目があまり変えられない」という制約がありますが、これらの制約を理解し、適切にブロックを組み合わせることで、美しく機能的なメッセージを作成できます。

重要なのは以下の点です。

  1. 制約を受け入れる: デザインの自由度は制限されているが、その分一貫性と可読性が保たれる
  2. コンテンツ構成に集中: 色やレイアウトではなく、情報の構造化と優先順位付けに注力
  3. パターンの活用: 疑似的な枠線や絵文字による状態表現など、制約内でのテクニックを習得

Block Kitの詳細なリファレンスはSlack Block Kit Builderで実際に構築しながら確認できますので、ぜひご活用ください。

この記事で紹介したテクニックのサンプルもぜひご利用ください!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Slack Block Kitデザインシステム見本",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "こんにちは、鈴木 太郎さん\nこちらはデザインパターンの総合的な見本です。"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*1. 基本的なセクション*\nこれは基本的なセクションブロックです。 *太字* や _斜体_ 、 ~取り消し線~ 、 `コード` などのMarkdown書式が使えます。"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "👆 Contextブロックは小さいテキストで補足情報を表示するのに最適です"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*2. 擬似的な枠線付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```項目:値```"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```ステータス:完了```"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*3. リンク付きのテキスト*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/1|リンク付きタスク名>*\n 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• リンクなしタスク名\n 2024年9月25日"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*4. 画像付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "画像を右側に配置できます"
},
"accessory": {
"type": "image",
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png",
"alt_text": "かわいい犬の画像"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*5. ボタンとアクション*"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Primary ボタン",
"emoji": true
},
"style": "primary",
"value": "primary_button",
"url": "https://example.com/primary"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Default ボタン(アウトライン)",
"emoji": true
},
"value": "default_button",
"url": "https://example.com/default"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Danger ボタン",
"emoji": true
},
"style": "danger",
"value": "danger_button",
"url": "https://example.com/danger"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*6. 通知カード*"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "┌──────────────────────────────────────────┐"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " *【オンボーディング】入社後1ヶ月間のTODO*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " *社員*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " 佐藤 花子、田中 一郎、山田 太郎"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "└──────────────────────────────────────────┘"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "コースを確認",
"emoji": true
},
"style": "primary",
"value": "check_course",
"url": "https://example.com/onboarding/course"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*7. タスクリスト(期限付き)*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/goals|目標の基本を学ぶ>*\n :alarm_clock: 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/management|代表的なマネジメントの型を知る>*\n :alarm_clock: 2024年9月25日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/personal-goals|個人目標設定の方法を学ぶ>*\n :warning: 2024年9月24日(期限超過)"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*8. 複数ボタン配置*"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "確認",
"emoji": true
},
"style": "primary",
"value": "confirm"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "後で",
"emoji": true
},
"value": "later"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "キャンセル",
"emoji": true
},
"style": "danger",
"value": "cancel"
}
]
},
{
"type": "divider"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "このメッセージはデザインシステムの参考用に自動生成されました"
}
]
}
]
}

GoでSlack通知を実装する方法

はじめに

Slackは現代のチーム開発において欠かせないコミュニケーションツールです。システムからの通知やアラート、定期的なレポートなど、様々な情報をSlackに送信することで、チーム全体での情報共有を効率化できます。

この記事では、Goを使ってSlackに通知を送信する方法を、基本的なテキストメッセージから高度なBlock Kitを使ったリッチなメッセージまで、実際のコード例とともに解説します。

使用するライブラリ

今回は、github.com/slack-go/slackというGoの公式Slackクライアントライブラリを使用します。このライブラリは活発に開発されており、Slack APIの最新機能もサポートしています。

セットアップ

1. Slackアプリの作成とトークンの取得

まず、Slack APIでアプリを作成し、Bot User OAuth Tokenを取得する必要があります。

  1. Slack APIのページにアクセス
  2. “Create New App” → “From scratch”でアプリを作成
  3. “OAuth & Permissions”から必要な権限を設定
    • users:read - ユーザー一覧の取得
    • chat:write - メッセージの送信
    • im:write - ダイレクトメッセージの送信
  4. Bot User OAuth Tokenをコピー

2. 環境変数の設定

取得したトークンを環境変数として設定します。

1
export SLACK_BOT_TOKEN="xoxb-your-token-here"

基本的な実装

Slackクライアントの初期化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"os"
"github.com/slack-go/slack"
)

func main() {
// Slack APIトークンを環境変数から取得
token := os.Getenv("SLACK_BOT_TOKEN")
if token == "" {
log.Fatal("SLACK_BOT_TOKEN is not set")
}

// Slackクライアントの初期化
api := slack.New(token)
}

ユーザー一覧の取得

通知を送信する前に、まずワークスペース内のユーザー一覧を取得してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getUsers(api *slack.Client) {
// ユーザー一覧を取得
users, err := api.GetUsers()
if err != nil {
log.Fatalf("ユーザー一覧の取得に失敗しました: %v", err)
}

// ユーザー情報を表示
log.Printf("ワークスペース内のユーザー数: %d\n", len(users))
for i, user := range users {
// 必要な情報だけを表示(すべての情報を表示するとログが長くなりすぎるため)
log.Printf("%d: ID=%s, Name=%s, RealName=%s, IsBot=%v\n",
i+1, user.ID, user.Name, user.RealName, user.IsBot)
}
}

シンプルなダイレクトメッセージの送信

ユーザーIDを指定して、シンプルなテキストメッセージを送信する関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sendDirectMessage(api *slack.Client, userID, message string) error {
// ユーザーとのDMチャンネルを開く
channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{
Users: []string{userID},
})
if err != nil {
return fmt.Errorf("DMチャンネルを開けませんでした: %v", err)
}

// DMを送信
_, _, err = api.PostMessage(
channel.ID,
slack.MsgOptionText(message, false),
)
if err != nil {
return fmt.Errorf("DMの送信に失敗しました: %v", err)
}

log.Printf("ユーザー %s にDMを送信しました", userID)
return nil
}

Block Kitを使った高度なメッセージ

Slack Block Kitを使用すると、画像、ボタン、フォーマットされたテキストなどを含む、より視覚的にリッチなメッセージを作成できます。

Block Kitメッセージの構築

以下は、様々なBlock Kit要素を含むサンプルメッセージの構築例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func buildSampleBlockKit() slack.MsgOption {
// Block Kitを使ったメッセージの構築
headerText := slack.NewTextBlockObject("mrkdwn", "*ブロックキットのサンプル*", false, false)
headerSection := slack.NewSectionBlock(headerText, nil, nil)

// テキストブロック
textBlock := slack.NewTextBlockObject("mrkdwn", "これは `Block Kit` を使ったメッセージです。\n*太字* や _斜体_ などのMarkdownも使えます!", false, false)
textSection := slack.NewSectionBlock(textBlock, nil, nil)

// 画像ブロック
accessory := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/beagle.png", "犬の画像")
imageText := slack.NewTextBlockObject("mrkdwn", "こちらはかわいい犬の画像です :dog:", false, false)
imageSection := slack.NewSectionBlock(imageText, nil, slack.NewAccessory(accessory))

// ボタン要素
btnText := slack.NewTextBlockObject("plain_text", "クリックしてください", false, false)
btn := slack.NewButtonBlockElement("click_button", "button_clicked", btnText)
btnAccessory := slack.NewAccessory(btn)

// ボタンセクション
btnSectionText := slack.NewTextBlockObject("mrkdwn", "アクションを実行するには:", false, false)
btnSection := slack.NewSectionBlock(btnSectionText, nil, btnAccessory)

// 区切り線
divider := slack.NewDividerBlock()

// フッターブロック
footerText := slack.NewTextBlockObject("mrkdwn", "Block Kitの詳細は <https://api.slack.com/block-kit|こちら> をご覧ください", false, false)
footerSection := slack.NewSectionBlock(footerText, nil, nil)

// すべてのブロックを一つのメッセージにまとめる
return slack.MsgOptionBlocks(
headerSection,
divider,
textSection,
imageSection,
divider,
btnSection,
divider,
footerSection,
)
}

Block Kitメッセージの送信

構築したBlock Kitメッセージを送信する関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sendDirectMessageWithBlocks(api *slack.Client, userID string, blocks slack.MsgOption) error {
// ユーザーとのDMチャンネルを開く
channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{
Users: []string{userID},
})
if err != nil {
return fmt.Errorf("DMチャンネルを開けませんでした: %v", err)
}

// ブロックキットメッセージを送信
_, _, err = api.PostMessage(
channel.ID,
blocks,
)
if err != nil {
return fmt.Errorf("DMの送信に失敗しました: %v", err)
}

log.Printf("ユーザー %s にブロックキットDMを送信しました", userID)
return nil
}

完全なサンプルコード

以下が、これまでの機能をすべて含んだ完全なサンプルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
// Slack APIトークンを環境変数から取得
token := os.Getenv("SLACK_BOT_TOKEN")
if token == "" {
log.Fatal("SLACK_BOT_TOKEN is not set")
}

// Slackクライアントの初期化
api := slack.New(token)

// ユーザー一覧を取得して表示
getUsers(api)

// 特定のユーザーにDMを送信
userID := "U02K6JU8D" // 実際のユーザーIDに置き換えてください
err := sendDirectMessage(api, userID, "これはテストメッセージです!")
if err != nil {
log.Fatalf("DMの送信に失敗しました: %v", err)
}

// ブロックキットを使用したDMを送信
blocks := buildSampleBlockKit()
err = sendDirectMessageWithBlocks(api, userID, blocks)
if err != nil {
log.Fatalf("ブロックキットDMの送信に失敗しました: %v", err)
}
}

実行方法

  1. 依存関係をインストール:

    1
    go mod tidy
  2. 環境変数を設定:

    1
    export SLACK_BOT_TOKEN="xoxb-your-token"
  3. プログラムを実行:

    1
    go run main.go

注意事項

  1. レート制限: Slack APIにはレート制限があります。大量のメッセージを送信する場合は、適切な間隔を設けましょう。

  2. エラーハンドリング: 本番環境では、ネットワークエラーやAPI制限エラーに対する適切なリトライ機構を実装することを推奨します。

  3. ユーザーID: 実際の運用では、ユーザー名からユーザーIDを動的に取得する仕組みを構築することが一般的です。

  4. セキュリティ: Slack APIトークンは機密情報です。ソースコードに直接記述せず、必ず環境変数や設定ファイルから読み込むようにしてください。

まとめ

この記事では、Goを使ったSlack通知の実装方法を、基本的なテキストメッセージから高度なBlock Kitを使ったリッチなメッセージまで幅広く解説しました。

github.com/slack-go/slackライブラリを使用することで、Slack APIの豊富な機能を簡単に利用できます。システム監視、定期レポート、チーム内通知など、様々な用途でSlack通知を活用して、より効率的なチーム開発を実現してください。

Block Kitの詳細については、Slack Block Kit Builderで実際にブロックを構築しながら学習することをおすすめします。

Go言語でAsynqにレート制限を実装する

タスクキューは多くのWebアプリケーションで必要不可欠な機能ですが、大量のタスクを処理する際にはレート制限が重要になります。特に外部API呼び出しやメール送信などのタスクでは、適切なレート制限なしに処理すると、サービス制限に引っかかったり、相手先のサーバーに負荷をかけてしまう可能性があります。

今回は、Go言語の人気タスクキューライブラリであるAsynqにレート制限を実装する方法を、複数のアプローチとともに解説します。

プロジェクト構成

このサンプルプロジェクトは以下の構成になっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
asynq-late-limiter/
├── go.mod
├── tasks/
│ └── tasks.go # タスクの定義と処理ロジック
├── limiter/
│ ├── limiter.go # Limiterインターフェース
│ ├── rate_limiter.go # golang.org/x/time/rateを使った実装
│ ├── redis_rate_limiter.go # Redisベースの実装(シンプル版)
│ ├── redis_rate_limiterv2.go # Redisベースの実装(改良版)
├── producer/
│ └── main.go # タスクを生成するプロデューサー
└── worker/
└── main.go # タスクを処理するワーカー

1. インターフェース設計

まず、レート制限の基盤となるインターフェースを定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// limiter/limiter.go
package limiter

import (
"fmt"
"time"
)

// レート制限を確認し、エラーを返す
type Limiter interface {
Check() error
}

// RateLimitErrorは、レート制限が超過されたときに発生するエラー
type RateLimitError struct {
RetryIn time.Duration
}

func (e *RateLimitError) Error() string {
return fmt.Sprintf("rate limited (retry in %v)", e.RetryIn)
}

このインターフェース設計により、異なるレート制限の実装を簡単に差し替えることができます。

2. レート制限の実装方式

2.1 golang.org/x/time/rateを使った実装

標準的なトークンバケットアルゴリズムを使った実装です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// limiter/rate_limiter.go
type RateLimiter struct {
limiter *rate.Limiter
limit int // 1秒間に処理できるリクエスト数
burst int // 瞬間的に許可される最大リクエスト数
retryIn time.Duration // レート制限時の再試行待機時間
}

func NewRateLimiter(limit int, burst int, retryIn time.Duration) *RateLimiter {
return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(limit), burst),
limit: limit,
burst: burst,
retryIn: retryIn,
}
}

func (rl *RateLimiter) Check() error {
if !rl.limiter.Allow() {
return &RateLimitError{
RetryIn: rl.retryIn,
}
}
return nil
}

メリット

  • 実装が簡単
  • メモリ効率が良い
  • 高速

デメリット

  • 単一プロセスでのみ動作
  • 分散環境では使用できない

2.2 Redisベースの実装(改良版)

複数のワーカープロセス間でレート制限を共有する場合に適しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// limiter/redis_rate_limiterv2.go
type RedisRateLimiterV2 struct {
client *redis.Client
key string
limit int
windowSize time.Duration
}

func (r *RedisRateLimiterV2) Allow(taskType string) (bool, int64, error) {
ctx := context.Background()
key := fmt.Sprintf("rate_limit:%s", taskType)
now := time.Now().Unix()

script := `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 古いエントリを削除
redis.call("ZREMRANGEBYSCORE", key, "-inf", now - window)

-- 現在のリクエスト数を取得
local count = redis.call("ZCOUNT", key, "-inf", "+inf")

if count >= limit then
-- 最も古いリクエストのタイムスタンプを取得
local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")
local retryIn = 0.0
if #oldest > 0 then
retryIn = math.max(0, oldest[2] + window - now)
end
return {0, retryIn}
end

-- 新しいリクエストを追加
redis.call("ZADD", key, now, tostring(now) .. "-" .. redis.call("INCR", "request_counter"))
redis.call("EXPIRE", key, window)

return {1, 0}
`

result, err := r.client.Eval(ctx, script, []string{key}, r.limit, int(r.windowSize.Seconds()), now).Result()
// ... エラーハンドリングと結果の解析
}

メリット

  • 分散環境での動作
  • 複数のワーカー間でレート制限を共有
  • 正確な待機時間の算出

デメリット

  • Redisへの依存
  • わずかなパフォーマンスオーバーヘッド

3. Asynqとの統合

タスクの定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tasks/tasks.go
func EmailNotificationTask(ctx context.Context, t *asynq.Task, limit limiter.Limiter) error {
// レート制限を確認
if err := limit.Check(); err != nil {
return err
}

// メール送信処理
var payload map[string]string
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return err
}
log.Println("Sending Email to:", payload["email"], "with subject:", payload["subject"])
return nil
}

ワーカーの設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// worker/main.go
func main() {
// 1秒間1リクエストに制限
emailRateLimiter := limiter.NewRedisRateLimiterV2("email", 1, time.Second)

srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: ":6379"},
asynq.Config{
Concurrency: 5,
Queues: map[string]int{
"default": 7,
"email": 3,
},
// レート制限エラーを失敗としてカウントしない
IsFailure: func(err error) bool {
return !IsRateLimitError(err)
},
// 再試行の間隔を指定
RetryDelayFunc: retryDelay,
DelayedTaskCheckInterval: 100 * time.Millisecond,
},
)

mux := asynq.NewServeMux()
mux.HandleFunc(tasks.TypeEmailTask, func(ctx context.Context, t *asynq.Task) error {
// TaskにRateLimiterを設定する
return tasks.EmailNotificationTask(ctx, t, emailRateLimiter)
})

if err := srv.Run(mux); err != nil {
log.Fatalf("サーバーを起動できませんでした: %v", err)
}
}

エラーハンドリングとリトライ戦略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// レート制限エラーの判定
func IsRateLimitError(err error) bool {
_, ok := err.(*limiter.RateLimitError)
return ok
}

// カスタムリトライ遅延
func retryDelay(n int, err error, task *asynq.Task) time.Duration {
var ratelimitErr *limiter.RateLimitError
if errors.As(err, &ratelimitErr) {
return ratelimitErr.RetryIn
}
return asynq.DefaultRetryDelayFunc(n, err, task)
}

4. 実行方法

1. 依存関係のインストール

1
go mod tidy

2. Redisの起動

1
2
3
4
5
# Dockerを使用する場合
docker run -d -p 6380:6379 redis:latest

# または直接実行
redis-server --port 6380

3. ワーカーの起動

1
go run worker/main.go

4. タスクの投入

別のターミナルで以下のコマンドを実行。

1
go run producer/main.go

5. 実装のポイント

レート制限の適切な設定

  • 外部API呼び出し: APIの制限に合わせて設定(例:100リクエスト/分)
  • メール送信: メール送信サービスの制限に合わせて設定(例:10通/秒)
  • データベース操作: データベースの負荷を考慮して設定

エラーハンドリング

1
2
3
4
5
6
7
8
9
10
11
srv := asynq.NewServer(
redisOpt,
asynq.Config{
// レート制限エラーは失敗としてカウントしない
IsFailure: func(err error) bool {
return !IsRateLimitError(err)
},
// レート制限エラーの場合は適切な待機時間でリトライ
RetryDelayFunc: retryDelay,
},
)

まとめ

このサンプルコードでは、Asynqにレート制限を実装する複数の方法を紹介しました。どの実装を選ぶかは、システムの要件によって決まります:

  • 単一プロセス環境: golang.org/x/time/rateベースの実装
  • 分散環境: Redisベースの実装

インターフェースベースの設計により、後から実装を切り替えることも容易です。適切なレート制限により、安定したタスク処理システムを構築できます。

参考リンク

Go 1.24.3アップデート後のビルドエラーと解決方法

はじめに

最近Go 1.24.3へアップデートしたところ、ローカル環境でビルドエラーが発生するようになりました。この記事では、発生した問題と解決方法について共有します。

発生した問題

Go 1.24.3にアップデートした後、以下のようなエラーメッセージが表示されるようになりました。

1
link: duplicated definition of symbol dlopen, from github.com/ebitengine/purego and github.com/ebitengine/purego (exit status 1)

原因

調査の結果、このエラーはMacのIntel CPU(darwin/amd64)環境で発生する既知の問題であることがわかりました。Go公式リポジトリでも同様の問題が報告されています。

Github Issue #73617: cmd/link: Go 1.24.3 and 1.23.9 regression - duplicated definition of symbol dlopen

解決方法

この問題はgithub.com/ebitengine/puregoの最新バージョン(v0.8.3)で修正されています。以下のコマンドでpuregoをアップデートすることで解決できます。

1
go get github.com/ebitengine/purego@v0.8.3

アップデート後、ビルドが正常に完了することを確認しました。

まとめ

Go言語のバージョンアップ後にはライブラリとの互換性の問題が発生することがあります。特にGoのマイナーバージョンアップデート(1.24.2→1.24.3など)でも互換性の問題が生じる可能性があるため、ビルドエラーが発生した場合は依存ライブラリの更新も検討するとよいでしょう。

参考リンク

Goのエラーハンドリングのベストプラクティス

はじめに

Goのエラーハンドリングは他の言語と少し異なるアプローチを取ります。例外機構を持たないGoでは、エラーは値として扱われ、関数の戻り値として明示的に返されます。このシンプルなアプローチは強力ですが、効果的に活用するにはいくつかのベストプラクティスを理解する必要があります。

この記事では、Goのエラーハンドリングの基本から、カスタムエラーの作成、エラーのラッピングとアンラッピング、そしてerrors.Is()errors.As()を使用した効果的なエラー判定の方法について解説します。

Goのエラーハンドリングのベストプラクティス

Goのエラーハンドリングのベストプラクティスを理解するために、いくつかの具体例を見ていきましょう。

1. 基本的なエラー定義と判定

Goでは、標準パッケージのerrorsを使って簡単にエラーを定義できます。以下のサンプルコードでは、よく使われるパターンを示しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"errors"
"fmt"
)

// カスタムエラー型の定義
var (
ErrNotFound = errors.New("not found")
ErrInvalid = errors.New("invalid input")
)

// データベース操作を模擬する関数
func findUser(id string) error {
// エラーをラップして返す
return fmt.Errorf("failed to find user: %w", ErrNotFound)
}

type NotFoundError struct {
ID string
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("user %s not found", e.ID)
}

func main() {
// 悪い例: == 演算子での比較
err := findUser("123")
if err == ErrNotFound { // これは動作しない!
fmt.Println("== で比較。ユーザーが見つかりませんでした")
}

// 良い例1: errors.Is() を使用
err = findUser("123")
if errors.Is(err, ErrNotFound) {
fmt.Println("errors.Is() で比較。ユーザーが見つかりませんでした")
}

err = &NotFoundError{ID: "123"}
var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
fmt.Printf("ユーザー %s が見つかりませんでした\n", notFoundErr.ID)
}

// エラーのラップと展開の例
err = findUser("123")
fmt.Printf("元のエラー: %v\n", err)

// ラップされたエラーを展開
unwrapped := errors.Unwrap(err)
fmt.Printf("展開されたエラー: %v\n", unwrapped)
}

実行結果

1
2
3
4
errors.Is() で比較。ユーザーが見つかりませんでした
ユーザー 123 が見つかりませんでした
元のエラー: failed to find user: not found
展開されたエラー: not found

2. エラー判定の種類と使い分け

上記のコードには、エラー判定のための重要な方法がいくつか含まれています。それぞれを詳しく解説します。

エラー比較の落とし穴: == 演算子

1
2
3
4
5
// 悪い例: == 演算子での比較
err := findUser("123")
if err == ErrNotFound { // これは動作しない!
fmt.Println("== で比較。ユーザーが見つかりませんでした")
}

このコードでは、findUser関数が返すエラーとErrNotFound==演算子で直接比較しています。しかし、このアプローチには大きな問題があります。findUser関数は単純にErrNotFoundを返すのではなく、fmt.Errorf("failed to find user: %w", ErrNotFound)を使ってエラーをラップしているため、==比較は失敗します。ラップされたエラーは元のエラーと等価ではないからです。

推奨方法1: errors.Is()

1
2
3
4
5
// 良い例1: errors.Is() を使用
err = findUser("123")
if errors.Is(err, ErrNotFound) {
fmt.Println("errors.Is() で比較。ユーザーが見つかりませんでした")
}

Go 1.13以降で導入されたerrors.Is()関数は、エラーチェーン内のどこかに特定のエラー値が含まれているかを確認します。これにより、ラップされたエラーでも正しく比較できるようになります。errors.Is()は、エラーが同一かどうかを確認する際の推奨方法です。

推奨方法2: errors.As()

1
2
3
4
5
err = &NotFoundError{ID: "123"}
var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
fmt.Printf("ユーザー %s が見つかりませんでした\n", notFoundErr.ID)
}

errors.As()関数は、エラーチェーン内のいずれかのエラーが特定の型に一致するかを確認し、一致する場合はその値をターゲット変数に設定します。これは、エラーの型に基づいて処理を分岐させたい場合や、エラー内の追加情報(この例ではID)にアクセスしたい場合に特に有用です。

3. エラーのラッピングとアンラッピング

1
2
3
4
5
6
7
// エラーのラップと展開の例
err = findUser("123")
fmt.Printf("元のエラー: %v\n", err)

// ラップされたエラーを展開
unwrapped := errors.Unwrap(err)
fmt.Printf("展開されたエラー: %v\n", unwrapped)

Go 1.13では、%w動詞を使用して元のエラーをラップする機能がfmt.Errorfに追加されました。これにより、より詳細なコンテキスト情報を提供しながら、元のエラー値を保持できます。errors.Unwrap()関数を使用すると、ラップされたエラーから元のエラーを取り出すことができます。

4. カスタムエラー型の作成

1
2
3
4
5
6
7
type NotFoundError struct {
ID string
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("user %s not found", e.ID)
}

Goでは、Error()メソッドを実装した任意の型をエラーとして使用できます。カスタムエラー型を作成することで、エラーに追加情報(この例ではID)を含めることができ、より詳細なエラーハンドリングが可能になります。

まとめ

Goのエラー判定では、==演算子による比較はラップされたエラーに対して正しく動作しません。
そのため、Go1.13以降で導入されたerrors.Iserrors.Asを活用することで、より安全かつ柔軟なエラーハンドリングが可能になります。
エラー処理の品質向上のため、ぜひこれらの手法を取り入れてみてください。

12330