GoのイテレータにRubyっぽいコレクション操作を提供するパッケージを作った

GitHub - aereal/iter: iter provides utility functions about standard iter.

使い方

pkg.go.devを見てもらえるとよい。

Chunkを取り上げると、第一引数を n 個ごとの要素に分割したイテレータを返す。

たとえば一度に最大500個までの引数を受け取るAPIへリクエストを送る処理を実装する場合、送りたい引数リストを500件ずつに分割して送りたい。 そういう時に Chunk を使うとよい。

for chunk := range seq.Chunk(args, 500) {
  _ = sendRequest(ctx, chunk)
}

他に、2つの iter.Seq を引数にとり同じ添字に位置する要素をペアにした iter.Seq2 を返す Zip などもある。

モチベーション

Rubyなどにあるような高級なコレクション操作が言語を問わず使えると嬉しい。「ある条件を満たす限り先頭から要素をとりつづけて~」と説明するより「takeWhileしたい」と言って通じるほうが断然話が早いのは間違いない。

高級なコレクション操作をGo向けに提供する試みに対してこれまで個人的に否定的な態度をとってきたが、2つの変化が追い風となり便利さが勝ったと判断したので作ることにした。

理由のひとつがジェネリクスの導入。

ジェネリクス導入以前のGoは型システムが貧しく、コレクション操作を一般化しようとすると、コード生成を用いて利用者が使う具象型ごとに適応させるかさもなくば any (interface{}) を使ってキャストに頼るかしかなかった。

素朴なコレクション操作のためにひたすらにコード生成を強いるのは利用者の負担が大きいし、そのようなライブラリ・ツールを実装することを考えると割に合わない気がした。

キャストする場合、当然コレクションと見なせない型であれば実行を停止せざるをえないが、panicしてシグネチャからエラーを取り除くにせよ、キャスト失敗をエラーとして伝えるシグネチャにするにせよ、利便性や安全性などの観点でそれぞれ懸念がある。

ジェネリクスはこれらの問題を(ほぼ)解決してくれる。

もうひとつの理由がイテレータの整備、より具体的にはrange-over-funcの登場である。

対象とするコレクションがどんな性質か・コレクションを加工としてどんなデータを得たいかによって効率的なプログラムの書き方が変わってくる。

Goのコレクション操作ライブラリの利用者は、for文を使って何度も書いてきた定型的な操作を任せたいのであって、コレクションそのもののサイズやキャパシティ管理だとか排他制御だとかまでを手放したいわけではない。

range-over-funcの導入で拡充されたイテレータは、これまで言語仕様で特定の型だけを特別扱いして規程されていた反復処理の実装を利用者が制御できるよう拡張しつつ、イテレータプロトコルに則って先に挙げたデメリットを解消ないし抑えて一般化しやすくするもので、これが最後の後押しとなった。

イテレータプロトコルに則れば途中で反復を止めることも可能だから、無限リストやストリームのようなコレクションも適切に扱える。

実際、 Zip はpull型のイテレータを使っているので、渡したイテレータが勝手に終端しない無限リストのような振る舞いをしても要素を反復できる。

むすび

Go自身の進化によりコレクション操作のライブラリが提供・利用しやすくなったので恩恵に最大限与るために作ったよ、というご紹介だった。

なんでも入れるつもりはなくて、たとえばmapのような操作は十分に単純なので入れるつもりはない。

また、イテレータからスライスであるとかマップであるとか具象へ変換するような処理やいわゆる畳み込みに類されるものの導入も消極的。

端的に言うと range の右側に書く組み合わせ可能なグッズだけ集めることに価値を見出しているかんじ。

Goの見た目をRubyとかScalaっぽくするジョークグッズを作るつもりはなく、あくまで普段からよく使うが初見でテストなしに遭遇すると境界条件が気になるような処理が集まっていて嬉しい……そういう実用的なライブラリを目指す。

どうぞご利用ください。

gotest2rdf: 失敗したGoのテストをreviewdogで報告するためのユーティリティを作った

# gotest2rdfの紹介

github.com

![reviewdogがテストの失敗をGitHubの行コメントで報告している様子](https://raw.githubusercontent.com/aereal/gotest2rdf/main/docs/image.png)

gotest2rdfというツールを作りました。go testの出力を受け取り、Reviewdog Diagnostic Formatに変換します。

これをreviewdogに渡すことで失敗したテストやスキップしたテストをPull Requestに行コメントで報告してくれます。

もちろんreviewdogのreporterを指定すればChecksとして報告もできますし、GitHub以外のサービスも対応できます。

ビルドジョブの失敗通知でテストが失敗したことがわかったけど、じゃあどこが落ちとるねんということを知るためにいちいちジョブの出力を見にいくのは地味に手間なので、元々linterなどで活用していたreviewdogを使えないかと考えたことがきっかけです。

# reviewdogとは

GitHub - reviewdog/reviewdog: 🐶 Automated code review tool integrated with any code analysis tools regardless of programming language

> Automated code review tool integrated with any code analysis tools regardless of programming language.

……ということでlinterなどの解析結果を報告してくれるツールです。

おおまかな仕組みとしては、ファイル・行・列・メッセージからなる解析結果を渡すと、コードをホスティングしているサービスのAPIを呼び出していいかんじに見せてくれます。

GitHubのPull RequestやGitLabのMerge Request画面で「この変更ではこうした解析が得られましたよ」という情報を見せることで、開発者にコード改善の示唆を与えてくれます。

典型的には、いわゆるlinterと呼ばれるよりよい書き方へ統一するよう支援してくれるツールの報告に使われることが多いです。

しかし用途はlinterに限られずコードを解析した結果をコードの変更に紐付けて表示するためならなんでも使えます。自動テストもコードの品質を担保する役割も担うことから広義のソフトウェア解析といえるでしょう。

何より既に述べたように、どのテストがどのように失敗したのか、できるだけ画面遷移を減らして知れるとよいですし、不可解な失敗に遭遇した時にレビュアーとのあいだでコミュニケーションする良い機会にもなります。

# Reviewdog Diagnostic Format (RDF) について

ReviewdogはVimのerrorformatなどいくつかサポートしていますが、独自に定義した[Reviewdog Diagnostic Format](https://github.com/reviewdog/reviewdog/tree/master/proto/rdf)という形式もサポートしています。

詳しい仕様やモチベーションはREADMEに譲りますが、機械的に解析しやすく特定の言語やツールに依存しないポータブルかつリーズナブルなフォーマットが世の中に存在しなかったので定義した、ということです。

Protocol BuffersおよびJSON Schemaが公開されているので、準拠も楽です。

# go testの-jsonオプション

go testには `-json` というオプションがあり、これは名前から想像できるようにテスト結果をJSON形式で出力してくれます。
正確にはいわゆるJSON Linesに近いもので、JSON表現を改行区切りにしたものです。

go testのプレーンテキスト出力はパースしやすいとは言えませんし、そもそも仕様が定かなのか・安定しているのかは不明ですし、 `-json` オプションがあるのにわざわざプレーンテキストをパースする意味もありません。

# gotest2rdfの実装について

ここまででgo test -jsonで出力されたJSON LinesをReviewdog Diagnostic Formatに変換してあげれば、失敗したテストをreviewdog経由で報告することができそうだということがわかりました。

しかし単純な変換ができるわけではありません。

go testはテストスイートごとに成功 (pass), 失敗 (fail), スキップ (skip) のいずれかが記録されるのみで、失敗やスキップと理由が直接紐付けられているわけではありません。

よく使う `t.Errorf` などの関数は実は `t.Logf(...); t.Fail()` とほぼ同じです。実際、go testのJSON出力でactionがfailのmessageはテストスイートの名前などが含まれるのみで、 `t.Errorf` に渡した文字列などは含まれません。

渡した文字列はfailより前にoutputというactionのイベントとして記録されます。

`t.Logf()` などの出力はoutputというイベントとして記録されることと `t.Errorf` が `t.Logf(); t.Fail()` と同等ということを踏まえると理解はできるのですが、これが便利かというとそうではありません。

大抵の場合は `t.Errorf` に渡した文字列でアサーションが失敗した理由について詳しく報告しているので、この出力も解析結果として含まれていてほしいはずです。

なのでgotest2rdfではある程度outputイベントをバッファリングしておき、failやskipなどに遭遇したらその時点までに見つかったoutputイベントの出力をfailやskipの補足情報だとみなしてRDFのmessageに含めます。

バッファリングではデフォルトで3行です。これはとても長い出力を無闇にメモリ上に確保することによるパフォーマンスの劣化を気にした……というわけではなく、出力の混同をできるだけ抑えるためです。

以下のようなコードを考えます:

```go
import "testing"

func TestBlah(t *testing.T) {
t.Log("supplemental info")
if 4%2 != 0 {
t.Error("the number theory system maybe corrupted")
}
}
```

5行目のアサーションが失敗した場合、書き手が意図するアサーション失敗の情報はthe number theory system maybe corruptedだけのはずです。

しかし4行目のsupplemental infoも単なるoutputというイベントであり区別は難しいです。

なのでヒューリスティックにバッファリングしておき、それまでに登場した出力をすべて関連あるものとみなすという手段をとっています。

ファイルの内容を読み取って構文解析すればもう少し精度は上げられるかもしれませんが、けっきょく仕様上、failとoutputの関連付けがなされない以上ヒューリスティックに頼るほかありませんし、関係ないかもしれない出力が多少混じっても実用上はそんなに困らないので実装のリーズナブルさをとりました。

# むすび

とても便利なのでぜひご利用ください。

git pushするだけでGo製アプリケーションをリリースするGitHub Actionsのワークフローを整えた

Go製アプリケーションのリリース自動化

Goで書いたアプリケーションをリリースする際にやらないといけないことはいろいろある。

まず当然のこととしてコンパイルして成果物を作り、それをリリースする。 せっかくクロスビルドが容易なGoを使うので配布する成果物も可能な限り幅広い環境に対応させたい。

また、リリースに含まれる変更の概要をまとめたいわゆるChangelogのようなものも簡単に作れるとなおよい。

GitHubでホストするリポジトリのリリースについて主に考えたいので、リリースのホスティングGitHubのReleasesになる。 各Releaseの説明をChangelogとし、変更がもたらされたPull Requestのリンクも添えたい。

リリースするということは新しいバージョンを決めて、Gitのタグを打つ必要もある。

ひとつひとつはよくある作業だし特段難しいことはないが、いちいち手でやっていられないのでCIに任せたい。 掲題の通りGitHub Actionsでこれら一連の作業を自動化させるという話。

GitHub Actionsによるリリース自動化それ自体は目新しいトピックではない。しかしこの記事で紹介するワークフローはgit pushする以外に開発者がとるべきアクションはない。 具体的には、世の中の先行事例はGitのタグ発行は開発者の手作業で、それを契機にパイプラインを開始する……というものばかりだがそれも不要になる。

goreleaserにビルドまわりを任せる

最近はgoreleaserというツールがよく使われているのでビルドまわりはこれに任せる。

goreleaserには:

  • アプリケーションをビルドする
  • ビルドした成果物をGitHub Releasesにアップロードする

……という2点を任せる。

アップロード対象のリリースは実行したリポジトリに存在する最新のタグから同定される。 言い換えるとタグを打つのはgoreleaserの責務外となる。

semantic-releaseにバージョニングを任せる

[semantic-releaser][]というツールがある。JavaScript (Node) で実装されたCLIツールでConventional Commitsに従ってコミットログを書いておくとよしなに次のリリースバージョンを決めてくれる。

Conventional CommitsはSemantic Versioningを参照し、たとえば feat: blah blah ... とか書くと新機能の追加なのでminorの更新を含意する、などのルールを定義している。 Conventional Commitsに従うとコミットログから次のバージョンを決めるルールがツールを越えて相互運用できる。

他にもプラグインGitHub Releasesを作ったりGitタグを打ったり、あるいはNPMにアップロードするなどができる。

JavaScriptで書かれていることからもわかるように元々NPMパッケージのリリースを主眼に置いてエコシステムが整えられたツールだが、前述のようにリリースバージョンの決定以外はプラグインとして実装されているので、NPMパッケージのリリース以外にも使える。 必要なのはConventional Commitsに従ってコミットすることだけ。

つまり semantic-releaseを使ってGitタグの発行を含むバージョンアップ作業を自動化し、発行された新しいタグに成果物を作成・紐付ける作業をgoreleaserで自動化する というのがワークフローのあらましになる。

ワークフロー例

実際のワークフローを例に説明する。

pkgboundaries/ci.yml at main · aereal/pkgboundaries

上記ファイルから抜粋・簡略化したYAMLが以下。

  determine_release:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    outputs:
      will_release: ${{ steps.determine_release.outputs.new_release_published }}
    steps:
      - uses: actions/checkout@v3
      - id: determine_release
        uses: cycjimmy/semantic-release-action@v3.0.0
        with:
          dry_run: true
        env:
          GITHUB_TOKEN: ${{ github.token }}
  release:
    runs-on: ubuntu-latest
    needs:
      # - test
      - determine_release
    if: ${{ needs.determine_release.outputs.will_release }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: cycjimmy/semantic-release-action@v3.0.0
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - uses: actions/setup-go@v3
        with:
          go-version: '1.18.x'
      - uses: actions/cache@v3
        with:
          path: ~/go/pkg/mod
          key: go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            go-
      - uses: goreleaser/goreleaser-action@v2.9.1
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ github.token }}

determine_releaseとreleaseという2つのジョブに分けている。

determine_releaseは次のリリース予定を調べるジョブで「実行時点で新しいバージョンが発行されそうか」を示す真偽値 will_release を出力する。 cycjimmy/semantic-release-actionというsemantic-releaseを実行するActionがあり、その outputs.new_release_published という真偽値を参照している。 determine_releaseでは dry_run: true を指定し実際のリリースは行わず、バージョンアップ予定だけを調べる。

releaseジョブは実際にリリースを行う。 まず needs にdetermine_releaseを指定する。これは単に依存関係を宣言することに加えて needs コンテキスト経由でdetermine_releaseジョブの出力にアクセスする目的もある。 if: ${{ needs.determine_release.outputs.will_release }} で「 needs.determine_release.outputs.will_release がtrueだったらreleaseジョブを実行する」という意味になる。 新しいバージョンアップを要する変更がコミットされていなければリリース作業は行われない。

参考: - Workflow syntax for GitHub Actions - GitHub Docs - Contexts - GitHub Docs

続いてsemantic-releaseを実行し、GitHub Releasesを作る。 以下にsemantic-releaseの設定を引用する:

{
  "branches": [
    "main"
  ],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/github"
  ]
}

from pkgboundaries/.releaserc.json at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries

特別な設定は必要としない。デフォルトでNPMパッケージを公開する @semantic-release/npm が有効になっているのでそれ以外の有用なプラグインだけを明示しているだけに留まる。

最後にgoreleaserを実行する。 goreleaser-actionというインストールから実行までをよしなにやってくれるActionがあるのでこれを使う。 goreleaser/goreleaser-action: GitHub Action for GoReleaser

goreleaserはYAMLでいろいろ挙動をカスタマイズできるのだが、今回重要なのは release.mode になる。

release:
  mode: keep-existing

from pkgboundaries/.goreleaser.yml at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries

goreleaserもリリースノートの作成やGitHub Releasesの作成ができるが、今回のワークフローではそれらはsemantic-releaseに任せている。 mode: keep-existing を選ぶとタグに対応するGitHub Releasesが既に存在したらタイトルや本文を置き換えず成果物だけアップロードするという挙動になる。

こうすることでsemantic-releaseとうまく共存できる。

今後

リリース時に人間による確認を挟みたいという場合には、Environmentsが使えるかもしれない。

参考: Using environments for deployment - GitHub Docs

EnvironmentsはDeploymentsに関連する概念で、その名の通りデプロイ対象の環境を表す。 Environmentsはそれぞれデプロイ時に必須となるcommit statusやレビュアーを指定できる。 これら機能はBranch protection rulesと似ていて、たとえばproduction環境では所定のチームやユーザのレビューなしにデプロイできない、といった設定ができる。

参考:

GitHubにおけるDeploymentsは単にイベント履歴とそれらに応じたWebhookでしかない。 最近追加されたGitHub Actionsとの統合では、ジョブに environment: production のように記述することである環境の利用を宣言できる。 このジョブ実行開始時にDeployments APIでdeploymentとdeployment_statusが作成される。完了するとsuccessもしくはfailureの記録がGitHub Actionsによって行われる。

実際のデプロイ処理はジョブのstepとして自由に定義できるので、この記事で紹介したワークフローをデプロイ処理として記述することもできる。

するとEnvironmentsの機能でDeploymentsの開始時にチーム・ユーザによる確認を挟んだ上でリリースする、といったことが実現できる。

これは規模の大きいチームで厳格な統制を図りたい時に便利かもしれない。

prpl: AWS SSMパラメータストアの値を環境変数に設定するツールを作った

github.com

作った。

prpl = parameters pull toolです。

使い方

go run github.com/aereal/prpl/cmd/prpl -path /app/staging env

こういう風に使う。 -path はパラメータストアのパラメータパス。このパス以下のパラメータをすべて取得し、環境変数として設定、コマンドを実行する。 SecretStringもdecryptされて設定される。

injecuetと組み合わせるために作った。もちろんこれに限らずSSMパラメータストアと連携させてコマンドを実行するのにも使える。

this.aereal.org

ちなみに横着してバイナリの配布はしていない。自分の用途では go run で問題ないので。気が向いたらバイナリ配布します。

環境変数命名規則

READMEにも書いてあるけど、 -path に指定した文字列を取り除いた残りを使う。英数字以外はすべてアンダースコアにして大文字になる。

なぜこうしているかというと、 /app/staging/creds/id/app/production/creds/id のようにパスの先頭に環境などを含めているケースを想定しているため。 環境変数を参照するアプリは環境を意識せずに CREDS_ID と参照し、prplを実行する際に -path を切り替えて実行することを想定している。

ssmwrapとの違い

非常によく似たツールにssmwrapがあって、というかもともとssmwrapを使おうとしていたけれどバグがあり修正PRを送ったものの応答がなかったのと、上記のようなもっと合理的な命名規則を採用したく、では別のものを作ろうと思い立った。

ssmwrapと違いオプションは -debug-path だけで、コマンドを実行する機能しかない。 正直、ssmwrapのオプションは -paths とか -names とか -prefix とか -env とかかなりややこしくて所望する結果を得るのにどういうオプションを渡したら良いのかかなりとっつきにくかったので、しょっちゅうオプションを変えるわけではないとはいえ、UNIX的世界観のツールらしくもっとsimpleかつeasyにしようという思いもあった。

また、ファイルへの書き出し機能は、おそらくECSやDockerのenvironmentFileのために追加されたのだろうけれど、これって env(1) で良いよなという思いもあり削りたかった。

いかがでしたか?

最近ちまちましたツール作りが捗っていて楽しい。どうぞご利用ください。

OpenAPI定義に沿ってバリデーションをしてくれるGoのライブラリを書いた

GitHub - aereal/go-openapi3-validation-middleware: net/http middleware to validate HTTP requests/responses against OpenAPI 3 schema using kin-openapi.

kin-openapiというOpenAPI 3定義を読んでリクエスト・レスポンスのバリデーションをしてくれるGoのライブラリがあるんだけど微妙に使い勝手が悪い。 素朴に使おうとするとHTTPハンドラ内でバリデーションに関するコードを書く必要があって関心を分離させるという目的を果たすにはちょっと弱いし、得られたエラーを一貫して取り扱うにはエラーをHTTPレスポンスに加工して返すところまで一気通貫で取り扱いたい。

Goでnet/httpを使ってHTTPサーバを書く時は、RackやPlackのように、ミドルウェアあるいはサンク (thunk) を組み合わせてリクエスト/レスポンスの参照や加工を行えるので、この仕組みに乗りたい。

というわけでREADMEのsynopsisから引用:

import (
    "net/http"

    "github.com/aereal/go-openapi3-validation-middleware"
    "github.com/getkin/kin-openapi/routers"
)

func main() {
    var router routers.Router // must be built with certain way
    mw := openapi3middleware.WithValidation(openapi3middleware.MiddlewareOptions{Router: router})
    http.Handle("/", mw(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        // this handler is called if validation succeeds
    })))
}

見てわかるように、http.Handlerを扱えるライブラリならnet/http以外でも使える。 実際に自分はhttptreemuxで使った。

WithValidation はリクエストとレスポンスどちらも検証する。 用途に合わせてリクエストのみを検証する WithRequestValidation, レスポンスのみを検証する WithResponseValidation というエントリポイントをそれぞれ用意してある。

  • 自動テストやQAが十分であればレスポンスの検証は本番環境では不要という考え方もありうること
  • 実装の都合上、書き出されたレスポンスボディをすべてメモリ上に確保するためGoのio.Writerインターフェースの良さをスポイルしてしまっていること

……といった理由からレスポンスは検証せずリクエストのみ検証するというオプションを用意した。 ちなみに WithValidationWithRequestValidationWithResponseValidation を合成しただけ。ちょっとおしゃれで好き。

実装してのおもしろポイントといえばhttp.ResponseWriterを独自実装したところとか。

go-openapi3-validation-middleware/response_writer.go at main · aereal/go-openapi3-validation-middleware · GitHub

http.ResponseWriterはインターフェースなのでnet/httpがデフォルトで持っている実装以外を使うこともできる。

このライブラリではレスポンスボディとステータスコードを保持するための実装を書いた。 レスポンスを検証する際の最終的なレスポンスは、内側のHTTPハンドラが書き出すかもしれないし、このライブラリがエラー報告を書き出すかもしれない。 なのでbufferingResponseWriterのWrite()とWriteHeader()の書き込みインターフェースはそれらを呼び出した時点では引数を保持するだけにしてある。

レスポンスを書き換えるパターンのミドルウェアは初めて書いたので手札が増えてよかった。

go-sql-caller-annotation: GoでSQLに呼び出し元の情報をコメントとして埋め込む

github.com

というのを書いた。ご利用ください。

スロークエリを解析する時に「このクエリ、アプリのどこから呼んでいるんだろう?」と調べたいことがよくある。
だいたい遅いクエリって長くて複雑だったりするのでいちいち横に縦にスクロールして全容を把握してgrepするのも地味につらい。

ISUCONに向けてほしいねーって話になったので書いた。

PerlDBIを使っていた時にはDBIx::Tracerを使ったりして実現できていたので、Goでもやりたいと思って書いた。

やっていることは、database/sql/driverのラッパ実装を作っている。仕事は実際のドライバ (go-sql-driver/mysqlとかpqとか) に任せていて再実装ではないからDBへのアクセス部分にまったく不安はないと言っていいと思う。

この実装方法はaws-xray-sdk-goから借用した。Apache License 2.0を継承したりコメントやNOTICE.txtにもきちんと書いたつもりだけれども、もしライセンス上の不備があれば教えてください:

aws-xray-sdk-go/sql_context.go at master · aws/aws-xray-sdk-go · GitHub

どうぞご利用ください。

aws-xray-sdk-goを使う時はhttp.Client.Getとか使ってはダメで必ずcontext.Contextを渡さないといけない

aws-xray-sdk-goというAWS X-Rayでトレースを記録する便利グッズがある。

これはoutgoing HTTP requestも記録できるのだけれども、ある時を機会にトレースの記録に失敗するようになって試行錯誤したけど今日、IQ200になってすべてを理解した。
結論はタイトルの通りで、以下は詳細。

具体的にはこういうエラー・警告が出ていた:

[00] 2019-05-16T15:50:34+09:00 [Error] Suppressing AWS X-Ray context missing panic: failed to begin subsegment named 'example.com': segment cannot be found.
[00] 2019-05-16T15:50:35+09:00 [Warn] failed to record HTTP transaction: segment cannot be found.

つまり親のsegmentが取れていないということ。

HTTP APIなので xray.Handler を使ってsegmentを作っているはずだけど……。
コードはREADMEにある以下のようなかんじ:

func main() {
  http.Handle("/", xray.Handler(xray.NewFixedSegmentNamer("myApp"), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello!"))
  })))
  http.ListenAndServe(":8000", nil)
}

aws-xray-sdk-goはcontextを使ってrecorderというオブジェクトを持ち回しおり、このrecorderというオブジェクトがsegmentを保持している。

トレースできなかったコードはこういうかんじ:

	resp, err := c.httpClient.Get(endpointURL.String())

このメソッドにcontextは渡しているけれど、HTTPリクエストを送る時に渡していないのが原因
どうしたらいいかというとhttp.Requestオブジェクトを作ってWithContext()を呼ぶとうまくいった。

	req, err := http.NewRequest(http.MethodGet, endpointURL.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("[BUG] failed to build request: %s", err)
	}
	resp, err := c.httpClient.Do(req.WithContext(ctx))

いや〜〜〜〜〜〜〜大変。

気付くのに時間がかかったポイントとしては、以前のリビジョンではうまくいっていたのでHTTPリクエストを送るコードより設定やライブラリのバージョンに関心が寄っていたこと、他にトレースを発行するコードがなかったので全体に問題があるのかHTTPリクエスト送信に問題があるのか切り分けがむずかしかったこと、があげられそう。
手でsubsegmentを作ってうまくいくか見れば、トレースが取れてxray.Clientの使い方がおかしいと気付けたかもしれない。

ちなみになぜ以前はうまくいっていたかというと、その当時呼んでいたメソッドはWithContextを呼んでおり、最新のアプリではhttp.Client.Getを呼ぶ実装になっていたため……。

go-http-replay: VCRみたいに実際のHTTPレスポンスを保存してテストで再利用できるライブラリ

を作りました。

github.com

外部HTTPリクエストをスタブするTest::WWW::StubというPerlのモジュールや、スタブするレスポンスをあらかじめ記録しておいた実際のレスポンスから再現するVCRというRubyのライブラリなどにインスパイアされました。

使い方はこういうかんじ:

import (
    "net/http"
    "testing"

    httpreplay "github.com/aereal/go-http-replay"
)

func Test_http_lib(t *testing.T) {
    httpClient := &http.Client{
        Transport: httpreplay.NewReplayOrFetchTransport("./testdata", http.DefaultClient),
    }
    // httpClient will behave like the client that created from NewReplayTransport but DO actual request if local cache is missing.
}

ローカルにキャッシュが無い初回は実際にリクエストしそれを保存する、次回以降は保存されたレスポンスを再現するので実際にリクエストしない、といった振る舞いをします。

net/httpのAPIしか使っていないdependencies freeな実装なところがアピールしたいポイントです。

どうぞご利用ください!!!

GoのデバッグはdelveとVisual Studio Codeが便利

delveとは

Go向けのデバッガで、ステップ実行とかブレイクした行のレキシカル変数が見えたりといった基本的な機能を提供しつつ、dlv debugコンパイルしつつ実行 (go run) したりgo(1)とほどよく統合されている。

後述するheadlessモードがあっていわゆるリモートデバッグが可能。

delveをmacOSにインストールする

公式のドキュメントではbrew install go-delve/delve/delve一発で済むように書かれているが実際はうまくいかなかった (´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)

==> Installing go-delve/delve/delve
==> Downloading https://github.com/derekparker/delve/archive/v1.0.0.tar.gz
==> Downloading from https://codeload.github.com/derekparker/delve/tar.gz/v1.0.0
######################################################################## 100.0%
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
==> Generating dlv-cert
==> openssl req -new -newkey rsa:2048 -x509 -days 3650 -nodes -config dlv-cert.cfg -extensions codesign_reqext -batch -out dlv-cert.cer -keyout dlv-cert.key
==> [SUDO] Installing dlv-cert as root
==> sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain dlv-cert.cer
Last 15 lines from /Users/aereal/Library/Logs/Homebrew/delve/02.sudo:
2018-02-28 11:11:35 +0900

sudo
security
add-trusted-cert
-d
-r
trustRoot
-k
/Library/Keychains/System.keychain
dlv-cert.cer


If reporting this issue please do so at (not Homebrew/brew or Homebrew/core):
https://github.com/go-delve/homebrew-delve/issues

These open issues may also help:
Upgrade to delve fails https://github.com/go-delve/homebrew-delve/issues/20

紹介されているissueを見てみると証明書を生成してインポートするスクリプトを手で実行したらうまくいったと書いてある。

実際に実行した手順はこういうかんじ:

✘>﹏<✘ < cd $(brew --cache)/
✘╹◡╹✘ < tar xzf delve-1.0.0.tar.gz
✘╹◡╹✘ < cd delve-1.0.0
✘╹◡╹✘ < bash ./scripts/gencert.sh
✘╹◡╹✘ < brew install go-delve/delve/delve

delveをインストールしたら sudo pkill taskgated を実行してtaskgatedを再起動する。
プロセス間通信を監視するデーモンらしく、bash ./scripts/gencert.shで生成・インポートした証明書を読み込みしなおす必要がある。

Visual Studio Codeとvscode-go

Visual Studio Codeは便利、GoやTypeScriptを書くとき最近はもっぱらこれ。

vscode-goというGoを書くときの各種便利サポートを追加してくれる拡張を入れる。

Debugging Go code using VS Code · Microsoft/vscode-go Wiki · GitHubが詳しい。

Visual Studio Codeでデバッグ

Visual Studio Codeでリポジトリを開きつつ Debug → Open Configurations でデバッガの設定 (JSON) を開く。

設定中の moderemote にする。remoteにすると別プロセスで既に起動しているデバッガーに接続する。

Debugging Go code using VS Codeにあるように、シェルで dlv --listen=:2345 --headless -- などと実行してデバッガーを起動する。

あとはVisual Studio Codeの Debug → Start Debuggingを押すとデバッガー開始できる。

gyazo.com

行頭を押すとブレイクポイントを設定できまるでIDEのよう〜。

ステップ実行もできるしレキシカル変数も見れて便利!!!!

Go の text/scanner で改行をトークナイズしたいときは

Go の text/scanner はレキサを書く面倒な仕事の大半をいいかんじにしてくれて本質的なコードに取り組みやすくとても便利。

しかしはてな記法Markdown のように、行全体が単なるテキストではなくいくつかのパターンからなる文法のレキサを書くときは、パターン中に明示的に改行を含めたい。

どういうことかというと:

line: inlines CR

みたいに書きたい。

しかし scanner.Scan が改行を含む空白文字をスキップしてしまうので改行をトークナイズする、ということができない。

結論からいうと Scanner.ScanScanner.Whitespace というフラグを見て空白文字かどうか判断するので、このフラグを変えて改行は空白文字ではないと教えてあげるとよい。

(よく読めば Scanner.Whitespace にコメントで説明が書いてあった)

実際のコード:

Support inline HTTP annotations by aereal · Pull Request #2 · aereal/go-text-hatena · GitHubn

markdown-toc: Go で Markdown の見出しをパースして目次を作る

github.com

Go でテキスト処理をする練習のために書いてみた。

mdtoc - The markdown ToC generator - ウェブログ - Hail2u.net が元ネタで再実装というかんじ。

小難しいことをやるとどうしても泥臭くなるけど、普段書いている高水準なコードはこれくらい泥臭くてともすれば効率が悪いんだろうな〜、ということを Go を書くたびに思っている気がする。

Mackerel で QNAP も監視したいのでする

最近、MacBookSSD の空き容量がいつの間にか無くなっていて、スワップを作れなくなった結果、フリーズするということがあって反省したのでちゃんと管理しようという気持ちになった。

ひとまず家にある中で壊れたりトラブルが起きると一番困るのは QNAP なので、これを監視することにした。

Mackerel は Go で書かれた agent を入れるだけでよい割にアラートの閾値をいい感じに定義することができるので便利。

QNAP で mackerel-agent を動かす

僕の持っている QNAP は TS-220 というモデルで CPU は ARM v5 で OS は Linux ベースらしい。

mackerel-agent は最近の CentOSDebian しか動作することを保証されていない:

For now, mackerel-agent is guaranteed to run only on CentOS 5/6 and Debian 6/7.

https://github.com/mackerelio/mackerel-agent#readme

とはいえ Go で書かれているし Linux ベースなら取り付く島が無いなんてことはないだろうと思ってちょっと試したら動いた。

テストは書いていないしかなりいい加減だがこれで動いた。

  • QTS (QNAP の OS) に入っている uname-o (Operating System) をサポートしていない
    • どのような意図でもって実装されていないのかは不明だけれどもさして重要な情報ではないので決め打ちで "Linux" とする
  • QTS に入っている df-P オプションをサポートしていないし出力の形式が少し違う
    • 1024-block1k-block だった
    • それ以外は特に書式に大きな違いはなかったので正規表現を修正する

Docker でビルドする

mackerel-agent は OS X ではビルドに失敗するので Docker を使うことにした。

Dockerfile:

FROM golang:cross

RUN mkdir -p /go/src/github.com/mackerelio
ADD ./mackerel-agent /go/src/github.com/mackerelio/mackerel-agent
WORKDIR /go/src/github.com/mackerelio/mackerel-agent
RUN make deps
CMD bash -c "GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 make build && cp ./build/mackerel-agent /host/_mackerel-agent"

これで docker build -t mackerel-agent . && docker run -v "$(pwd)":/host mackerel-agent とすると _mackerel-agent が作られる。

Docker Hub Registry には golang が登録されているが、そのうちクロスコンパイルするための環境がセットアップされているタグを選ぶ。最新 (1.3) でよければ cross.

ビルドの際に指定している環境変数Optional environment variables を参照した。 また GOARM 変数については GoArm - go-wiki が詳しい。

ルート証明書が見つからないエラー

ビルドはできたものの実行時エラーが出る:

x509: failed to load system roots and no roots provided

意味がわからなかったが調べたところ x509 は SSL 通信のために使われているらしい。“roots” がよくわからなかったがルート証明書のことらしい。

ルート証明書が無いなら配置すればよいかと思ったもののどこに配置すればよいのかわからない。

Go において CertPool は参照すべき証明書を保持するオブジェクトの型で、SSL 通信などを扱うライブラリ (e.g. net/http) ではデフォルトではシステムの証明書を参照するようだった。

ではシステムの証明書の配置場所はどこかというと root_unix.go というファイルに定義されている。

var certFiles = []string{
        "/etc/ssl/certs/ca-certificates.crt",     // Debian/Ubuntu/Gentoo etc.
        "/etc/pki/tls/certs/ca-bundle.crt",       // Fedora/RHEL
        "/etc/ssl/ca-bundle.pem",                 // OpenSUSE
        "/etc/ssl/cert.pem",                      // OpenBSD
        "/usr/local/share/certs/ca-root-nss.crt", // FreeBSD/DragonFly
    }

QNAP では /etc/ssl/certs/myroots.crt に配置されていたので symlink を作った。

おわり

これで QNAP 上で mackerel-agent を動かすことができるようになったはず。

あとは init スクリプトを書いたりしてデーモン化しておけばよい。

ディスク容量だけではなく netin/netout や CPU 使用状況も見れるので並列にコピーを走らせて無茶を走らせているときなど、余裕があるかなど見れて便利。

だいたい普通の Linux という感じでかなり手軽に運用できるので、次は nasne あたりも監視したい。

Go-lang で書かれた Fast GitHub command line client: gh を試している

jingweno/gh · GitHub

Go で書かれた hub の port のひとつ、と表現するのが簡潔でわかりやすい。

インストールの方法はいろいろある

拡張・追加しているサブコマンドは hub のそれとあまり変わらない。

hub のおよそ2倍ほど速いので、alias git=gh してもそれほどストレスを感じない。

コードもそれほど規模も大きくなくあまり凝ったことをしていないので Go-lang のコード・リーディングにもまあまあよさそう。