その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

golangのstructに対してコンストラクタを自動生成するCLIツール "gonstructor" を書いた

github.com

表題の通りです.出オチのような名前です.

使い方やどういう挙動をするかという話題についてはREADME.mdのSynopsisをご覧いただければと思います.Javalombokをご存知の方はlombok@Valueのような挙動をする,と言えばイメージしやすいでしょう.
基本的にはgo:generateと組み合わせて使うことになるだろうと思います.


モチベーションとしてはREADME.mdのMotivationにも書きましたが,ザックリ言うと以下のような感じです:

  • Structのフィールドをprivateかつimmutableに取り扱いたいことが多い
  • フィールド (ないしはstruct) をimmutableに保つことについて,goでは基本的に「気をつける」しかないが,その際にはフィールドはprivateであるほうが嬉しい
    • フィールドを破壊的に変更できる存在が絞られるのでコントロールしやすくなる
  • フィールドをprivateにすると直接structを作るのが難しくなる
    • 得てしていわゆるコンストラクタのようなものを作りがち
  • structの定義に基づいてコンストラクタを自動で生成したい
    • ついでにフィールドに対するgetterも生えていると便利なことが多いのでそれも自動生成したい

publicなフィールドを行儀悪くグイグイ書き換えるようなことはしないだろ〜,とは思いますが,しかし仕組みとしてできるということはできるという意味なので,そういうのはなるべく仕組み側でやりにくくしてしまうのが良いだろうと考えている次第です.

どうぞご利用ください.


それはそうとしてGitHub Actions便利ですね.

gowrtr - goコード生成支援ライブラリ

gowrtr (go writerと発音します) というgoのコード生成支援ライブラリ (ジェネレータ群) を書きました.

github.com

Synopsisに書いたように,

package main

import (
	"fmt"

	"github.com/moznion/gowrtr/generator"
)

func main() {
	generator := generator.NewRoot(
		generator.NewComment(" THIS CODE WAS AUTO GENERATED"),
		generator.NewPackage("main"),
		generator.NewNewline(),
	).AddStatements(
		generator.NewFunc(
			nil,
			generator.NewFuncSignature("main"),
		).AddStatements(
			generator.NewRawStatement(`fmt.Println("hello, world!")`),
		),
	).
		EnableGofmt("-s").
		EnableGoimports()

	generated, err := generator.Generate(0)
	if err != nil {
		panic(err)
	}
	fmt.Println(generated)
}

のようなコード (ジェネレータ) を書いてやると

// THIS CODE WAS AUTO GENERATED
package main

import "fmt"

func main() {
        fmt.Println("hello, world!")
}

というようなコードが生成されるというようなライブラリです.
この例だと生成結果があまりにシンプルなので,逆に記述量が増えてアレな感じになっていますが,ある程度生成結果が大きくなるようなものだと便利に使えるはず……です (後述のImmutabilityの話題もご覧ください).詳細につきましてはGoDocをご覧ください (Exampleも示してあります).


特徴としては

  • 生成したコードにコードフォーマッタ (gofmt, goimports) を適用することができる
  • ライブラリの各メソッドはimmutableに振る舞う

というものが挙げられます.

前者は生成結果にgofmtをかけることによって「生成コードのフォーマットが統一される」というメリットと「Syntaxチェックができる」というメリットを享受できることに加え,goimportsを適用することによって「コード生成のためのジェネレータを書いているときに『何をimportするか』を考えなくても (記述しなくても) 良くなる」というメリットを得ることができます.

後者については各ジェネレータの各メソッドが暗黙的に内部状態を変更しないので,利用者はジェネレータのスナップショットを任意のタイミングで取得することができます.これによりジェネレータの再利用が可能になると同時に,そこからジェネレータを派生させていくようなコードが書きやすくなるというメリットがあります.




さて,Javaにはsquare/javapoetというライブラリがあって,これはJavaコードの生成支援を行うライブラリなのですが,gowrtrはこのライブラリに着想を得て作られました.
まだできたてのライブラリなのですが,一部の自前環境に適用したところうまくワークしているのである程度動くのではないでしょうかというステータスです.が,もしかしたら足りない機能や記法のサポートがあるかもしれません.ご意見・ご要望をお待ちしております.

ぜひご利用くださいませ.

go-errgen書いた

goのstructにエラー定義を書いておけば良い感じで「エラーを返却する関数」をコード生成するツールであるgo-errgenを書きました.

github.com

Synopsisに書いてあるとおり,

package mypkg

//go:generate errgen -type=myErrors
type myErrors struct {
	FooErr error `errmsg:"this is FOO error"`
	BarErr error `errmsg:"this is BAR error [%d, %s]" vars:"hoge int, fuga string"`
}

みたいな感じでstructにエラー定義を書いて,go:generate を設定してから go generate を実行すると

package mypkg

import "errors"
import "fmt"

func FooErr() error {
	return errors.New("[ERR-1] this is FOO error")
}

func BarErr(hoge int, fuga string) error {
	return fmt.Errorf("[ERR-2] this is BAR error [%d, %s]", hoge, fuga)
}

func MyErrorsList() []string {
	return []string{
		`[ERR-1] this is FOO error`,
		`[ERR-2] this is BAR error [%d, %s]`,
	}
}

という感じのコードが my_errors_errmsg_gen.go として生成されるというツールです.

これはエラーメッセージに「通し番号が付いたprefix」を付与したerrorを返却する関数をコード生成します.もし errmsg に加えて vars パラメータが定義されている場合はその値が関数の引数パラメータとして利用され,かつ fmt.Errorf() によってsprintf互換のプレースホルダにbindされます.


主なモチベーションとしては,

  • 集権的にエラーを定義して管理したい (エラー定義が散在するとつらい)
  • エラーの特定を行う際にエラーコードを利用したい

というのがあり,特に後者はいろいろなチームから多く利用されるコンポーネントであれば必須に近い機能でしょう.で,集権的にエラーを管理するにしても,手でエラーコードを記述するようにしたところでうっかりミスってしまう可能性もありますし (例えばエラーコードを重複させてしまうとか),そこんところは機械的にやりたいな〜という気持ちからerrgenを作ったという感じです.

errgen -type=myErrors -prefix=My-Prefix のように -prefix を付与すると [My-Prefix-1] のようにprefixを自由に設定することもできます.


この手の仕組みはプロジェクトごとに自作しがちだったんですが,毎度毎度書くというのも面倒だったのでこの度汎用的に使えるようツール化したという次第です.

ご利用くださいませ.

golangのstruct custom tagをうまいことparseして値を引っこ抜きたいってとき

type Foo struct {
	Bar string `buz:"qux" iyan:"bakan"`
}

における `buz:"qux" iyan:"bakan"` を良い感じで buz => "qux", iyan => "bakan" のように取得したいというケースでは以下のようにするとよい:

tagKeyValue := reflect.StructTag(`buz:"qux" iyan:"bakan"`)
buz := tagKeyValue.Get("buz") // => "qux"
iyan := tagKeyValue.Get("iyan") // => "bakan"

という具合でreflect.StructTagを使うと良い感じでできます.よかったよかった.

……クソ,なんのためにこんなものを書いてしまったんだ!!!! 良いお年を!!!!

github.com

req_mirror書いた

req_mirrorというサーバアプリケーションを書いた.受け取ったHTTP requestをJSONにserializeして,responseに詰めて返してくれるというやつ.つまりHTTP requestをおうむ返しにしてくれる雰囲気です.鏡的な挙動なのでreq_mirrorという名前にした次第.

github.com

Exampleのところにも書いているのだけれど,要はこういう動き方をします (localhost:22222でreq_mirror serverが立ち上がっている前提).

$ curl -s -XPOST -F "foo=bar" -F "buz=qux" 127.0.0.1:22222/hoge/fuga | jq .
{
  "Method": "POST",
  "URL": {
    "Scheme": "",
    "Opaque": "",
    "User": null,
    "Host": "",
    "Path": "/hoge/fuga",
    "RawPath": "",
    "ForceQuery": false,
    "RawQuery": "",
    "Fragment": ""
  },
  "Proto": "HTTP/1.1",
  "Header": {
    "Accept": [
      "*/*"
    ],
    "Content-Length": [
      "236"
    ],
    "Content-Type": [
      "multipart/form-data; boundary=------------------------74755673fc9beca6"
    ],
    "Expect": [
      "100-continue"
    ],
    "User-Agent": [
      "curl/7.43.0"
    ]
  },
  "Body": "--------------------------74755673fc9beca6\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--------------------------74755673fc9beca6\r\nContent-Disposition: form-data; name=\"buz\"\r\n\r\nqux\r\n--------------------------74755673fc9beca6--\r\n",
  "TransferEncoding": null,
  "Host": "127.0.0.1:22222"
}

はじめて使うHTTP Clientの挙動を確かめながらコード書く場合とかに地味に便利.あとは自作のHTTP Clientのテストをする時とかに威力を発揮します (発揮している).

まあコード自体はめちゃめちゃシンプルというか特になにもやっていないんですが,あると便利だったので外に置いた次第.なおhttp.Requestをそのままjson.Marshalに投げ込んだら処理系に怒られたという心温まるエピソードが開発中にはありました.
以上です.

golangで書いたツールをCircleCI上でビルドしてその成果物をGitHub Releasesにリリースする

表題の通り.いくらかポイントがあったのでメモとして記す.

基本的にこの記事の内容を真似した.

medium.com

あらかじめ,CircleCIの側の設定でGITHUB_TOKENという環境変数を登録しておく.なおGitHubのPersonal access tokenにはrepoのpermissionを付与しておく.

とは言えこれでは動かない.理由はghr (ghrについてはこちら: 高速に自作パッケージをGithubにリリースするghrというツールをつくった | SOTA) がcontextに依存しているからで,contextはgo 1.7以降でないと利用できない.しかしながらCircleCIのgolang環境は1.6系が使える内の最新なので *1 このままではghrをgo getすることが出来ない.
というわけでCircleCI上でgo 1.7を使うようにしましょう,ということでそういうconfigをcircle.ymlに書く.基本的にこのgistの真似.
しかしこのgistにはタイポがあってそのままでは動かない.ので以下のようにする.

machine:
  environment:
    GODIST: "go1.7.3.linux-amd64.tar.gz"
  post:
    - mkdir -p downloads
    - test -e downloads/$GODIST || curl -o downloads/$GODIST https://storage.googleapis.com/golang/$GODIST
    - sudo rm -rf /usr/local/go
    - sudo tar -C /usr/local -xzf downloads/$GODIST

こうしておくとgo 1.7.3がビルド時に利用されるようになる.

そして以下のようなdeploymentセクションを書く.オリジナルの記事ではmasterにpushする度にGitHub Releasesにアップロードされてしまうので,git tagがpushされた時にだけリリース処理が走るようにする (deployment.release.tagの部分でそうしている).

deployment:
  release:
    tag: /[0-9]+\.[0-9]+\.[0-9]+/
    commands:
    - go get github.com/tcnksm/ghr
    - make clean
    - make VERSION=`git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'`
    - rm bin/.gitkeep
    - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'` bin/

その他の細々した差としては,ghrのオプション部分で--replace `git describe --tags`として渡されている部分を--replace `git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'`というperlワンライナーを噛ませた形に書き換えている.git describe --tagsを実行すると{タグ名}-{ハッシュ}という結果が出てくる場合があるので,タグ名以降の内容を落とすためにそうしている.
あとは.gitkeepがアップロード元のディレクトリに含まれているとghrのアップロード時に失敗する (そんなものアップロードするな,みたいなエラーが出る) のであらかじめ消しておくという感じ.
なおビルド自体はmakeコマンドを叩くだけでglide installからクロスコンパイルまでやるという感じにしている (Makefileはこんな感じ
https://github.com/moznion/linenotcat/blob/4349058d9557d49e1f2fc9a23ebeb79e2279f81d/Makefile).

最終的なcircle.ymlとしては以下のような感じ.

machine:
  environment:
    GODIST: "go1.7.3.linux-amd64.tar.gz"
  post:
    - mkdir -p downloads
    - test -e downloads/$GODIST || curl -o downloads/$GODIST https://storage.googleapis.com/golang/$GODIST
    - sudo rm -rf /usr/local/go
    - sudo tar -C /usr/local -xzf downloads/$GODIST
deployment:
  release:
    tag: /[0-9]+\.[0-9]+\.[0-9]+/
    commands:
    - go get github.com/tcnksm/ghr
    - make clean
    - make VERSION=`git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'`
    - rm bin/.gitkeep
    - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'` bin/

これでtagをpushしたら自動的にGitHub Releasesにビルド成果物がリリースされるようになった.めでたしめでたし.それはそうとghr便利ですね.

[追記]

確かに!!!! ghrのバイナリダウンロードしてくれば良いですね.

linenotcatというツールを書いた

LINE Notifyが便利でよく使っています.LINE Notifyが何かとか何が便利なのかとかをご存じない方は

コマンドラインから LINE にメッセージを送れる LINE Notify « LINE Engineers' Blog

を読んでいただければと思いますが,あえてものすごくざっくり説明するとim.kayacのLINE版みたいなやつです.

んで,Slackというやつも便利で,こちらはチャットツールなわけですが,そのSlackにはslackcatというこれまた便利なコマンドラインツールがあり,これはコマンドを叩くだけで任意のSlackのchannelに対してメッセージやファイルの中身を送ることが出来ます.その名の通りcatコマンドの結果がSlackに流れるという感じ.

というわけでそのLINE Notify版を作ったという話です.コマンドライン経由でLINE Notifyにメッセージを送ることが出来ます.

github.com

LINE NOTify + catということでlinenotcatという名称です *1

基本的な使い方としてはslackcatとだいたい同じで,

$ echo 'YOUR_ACCESS_TOKEN' > $HOME/.linenotcat

という感じでAccess Tokenを登録するともう使えます.

$ echo 'Hello world!' | linenotcat

という風にすれば標準入力経由で投稿が出来て,

$ linenotcat /your/awesome/file.txt

という風にすればファイルの中身を投稿できます.

$ echo 'Hello world!' | linenotcat --tee
Hello world!

teeもできます.

$ tail -f /your/awesome/error.log | linenotcat --stream

stream modeなんてのもあり,上記の例の場合はtailが何か出力したらその内容を投稿するようになります (現状だと3秒間内容を溜め込んで投稿するようになっています).

$ linenotcat --message 'Hello world!'

なにかと便利だったのでそのままメッセージ送れるモードなんてのもあります.

$ linenotcat --image /path/to/your/awesome/image.png

LINE Notifyは実は画像をアップロードして送れるという機能もあるので画像も送れるようにしました

$ linenotcat --image /path/to/your/awesome/image.png --message "Yo!"

メッセージも添えられます.

$ linenotcat --config_file /path/to/your/config

config fileつまりAccess Tokenも差し替える事ができます.送り先を差し替えられたり出来ます.

$ linenotcat --status
{"status":200,"message":"ok","targetType":"USER","target":"moznion"}

今のTokenの情報を引いてくることも出来ます.


という感じです.まあこんなん無くてもshell scriptで大概解決するんですがバイナリポン置きで動くと楽だなという感じでこさえた次第です.
動くバイナリはGitHub Releasesにあるのでご利用下さい.

github.com

[追記]

homebrewに対応しました.

$ brew tap moznion/homebrew-linenotcat
$ brew install linenotcat

*1:そしてLINEは猫ではない