Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Plan 9のCプリプロセッサを読んだ

Plan 9には cpp(1) というANSIに準拠したCプリプロセッサが用意されていますが、Plan 9標準のCコンパイラコンパイラ自身が簡略化したプリプロセッサを持っているので基本的には使いません。ただし、Cコンパイラが持っているプリプロセッサは設計思想の影響もあり大幅に簡略化されているので、ANSI準拠のプリプロセッサが必要な場合は cpp を使います。8c(1)の場合は -p オプションが渡されれば cpp が使われるようになりますし、APE(ANSI/POSIX Environment)用のCコンパイラpcc(1)は特に何もしなくても cpp が使われます。*1

この cpp#include_next というGCC拡張ディレクティブを追加したくて関連するコードを読みました。

必要な理由

なんで #include_next を追加する必要があったのかというと、いくつかUnix由来のソースコードPlan 9へ移植していた時に、#include_nextが使われているものがありました。この拡張はシステム標準のヘッダファイルから一部を書き換えたい場合に使うことを想定しています。例えば uint32_t 型がシステムによって提供されていない場合、

#include_next <stdint.h>

typedef ulong uint32_t;

という内容を stdint.h というファイル名で #include のサーチパスに入れておくと、ソースコードからは単純に#include <stdint.h>とすればuint32_tが使える状態で読み込まれるという便利なものです。

Plan 9cpp は、実行されない場所に書かれたディレクティブであっても解析はするので、

#if 0
#include_next <stdint.h>
#endif

上記のコードでも #include_next がパースされて、結果として不明なディレクティブなのでエラーになってしまいます。このエラーを簡単に避ける方法がありませんでした。

データ構造

#include のサーチパスは includelist[] によって表現されます。

typedef struct  includelist {
    char    deleted; // 1なら参照しない
    char    always;  // 0の場合は "file.h" のみ対象
    char    *file;   // ディレクトリ名(例: /sys/include)
} Includelist;

#define    NINCLUDE 64
Includelist includelist[NINCLUDE];

デフォルトでは /$objtype/include/sys/include がサーチパスに入っています。また、$include 環境変数がセットされていれば、その内容も含まれます。特にオプションを渡さない場合、以下のような配列になります。

[0] file=/$objtype/include always=1
[1] file=/sys/include always=1
[2] file=$include(1) always=1
[3] file=$include(2) always=1
...
[63] file=. always=0

cpp-I オプションを渡した場合、includelist の末尾に追加されていきます。例えば cpp -I/a/include -I/b/include の場合は以下のようになります。

[0] file=/$objtype/include always=1
[1] file=/sys/include always=1
[2] file=$include(1) always=1
[3] file=$include(2) always=1
...
[61] file=/b/include always=1
[62] file=/a/include always=1
[63] file=. always=0

もう一つ、Source *cursource も重要なデータで、現在処理中のファイルのスタックを表します。

typedef struct source {
    char    *filename;  /* name of file of the source */
    int line;       /* current line number */
    int lineinc;    /* adjustment for \\n lines */
    uchar   *inb;       /* input buffer */
    uchar   *inp;       /* input pointer */
    uchar   *inl;       /* end of input */
    int     ins;        /* input buffer size */
    int fd;     /* input source */
    int ifdepth;    /* conditional nesting in include */
    struct  source *next;   /* stack for #include */
} Source;

Source  *cursource;

これはリンクリストになっていて、現在処理中のファイルが先頭です。

検索

検索する場合は、例えば #include <stdio.h> なら、includelist を後ろから検索していきます*2。この時、deletedが1の場合は常に無視し、alwaysが0の場合は #include <xx> の対象となりません(#include "xx" なら対象)。そうして、stdio.h が見つかったら探索を止めて cursource を更新します*3

#includeがネストした場合は、もう一度 includelist を後ろから検索してファイルを探します。見つかったら cursource のリストが増えて、処理し終われば取り除かれて cursource が以前処理していたファイルに戻ります。

*1:pcc はデフォルトのオプションやサーチパスが異なるだけで、コンパイル自体は 8c で行います

*2:この辺りのコードは /sys/src/cmd/cpp/include.c に書かれています

*3:これは /sys/src/cmd/cpp/lex.csetsourceunsetsource が行います

Perlの環境作った

plenv

個人的にはenv使わないことが多いけど、必要になったので入れた。

$ brew install plenv

これに伴って perl-build も必要だけど、Homebrewで perl-build を入れたらplenv install実行時にSSL/TLS関連のエラーが出た。

599 Internal Exception, https://fastapi.metacpan.org/v1/release/_search, Can't verify SSL peers without knowing which Certificate Authorities to trust This problem can be fixed by either setting the PERL_LWP_SSL_CA_FILE envirionment variable or by installing the Mozilla::CA module. To disable verification of SSL peers set the PERL_LWP_SSL_VERIFY_HOSTNAME envirionment variable to 0. If you do this you can't be sure that you communicate with the expected peer.

Homebrewのものはバージョンが古いらしいので、自分で最新版入れれば解決する。

$ mkdir -p $(plenv root)/plugins/perl-build
$ cd $_
$ git clone https://github.com/tokuhirom/Perl-Build.git .

plenv 自体もHomebrewを使う必要なさそうだけど一旦はこのまま。

環境設定

$HOME を汚したくないので ~/pkg/perl を使うように設定。

export PLENV_ROOT=~/pkg/plenv
export PATH=$PLENV_ROOT/shims:$PATH

plenvcpanmlocal::libPerl標準の環境変数があって難しい。

cpanm

plenvcpanm を入れる。

$ plenv install-cpanm

環境設定

こちらも plenv と同様に ~/pkg/perl を使うように設定。

export PERL_LOCAL_LIB_ROOT=~/pkg/perl
export PERL5LIB=$PERL_LOCAL_LIB_ROOT/lib/perl5
export PERL_CPANM_HOME=~/Library/Caches/cpanm
export PATH=$PATH:$PERL_LOCAL_LIB_ROOT/bin

# 必要なら
#export PERL_CPANM_OPT="--local-lib=$PERL_LOCAL_LIB_ROOT"

MackerelでGitHubのイシュー数推移を記録してみた

この記事はMackerelアドベントカレンダー2018の18日目です。

Mackerelはサーバ管理・監視サービスですが、取得する数値はサーバに限ったものではなく、例えば体重など、数値なら比較的なんでも記録することができて、記録した値の推移を眺めることができます。個人的にGitHubを使っていて積極的に参加していきたいと思っているので、活動した数値を可視化するプラグインを作ってみました。

f:id:lufiabb:20181218165122p:plain
作ったグラフ

この記事では、担当したイシューの残っている数と閉じた数を扱っていますが、GitHub API v3で取得できる値ならなんでも良いと思います。

プラグインを作る前に

プラグインは、mackerel-agentから1分ごとに呼ばれるコマンドです。Goが一番馴染んでいるのでGoを使ってプラグインを書きますが、ただのコマンドなので何で書いても良いと思います。

Goで書く場合、現在、プラグイン用の公式パッケージは2種類あります。

go-mackerel-plugin-helper のREADMEに、

We recommend to use go-mackerel-plugin instead of go-mackerel-plugin-helper to create mackerel agent plugin.

とあるので、今は go-mackerel-plugin を使う方が良さそうです。

プラグインを実装する

go-mackerel-plugin を使う場合は以下のインターフェイスどちらかを実装する必要があります。MetricKeyPrefix()があればユーザが設定ファイルでプラグインの名前を変更できるようになるので、新しく作る場合はPluginWithPrefixを実装する方が良いと思います。

package mackerelplugin

type Plugin inteface {
    // メトリック名やラベル、単位などを返すメソッド。
    GraphDefinition() map[string]Graphs

    // サーバから取得したメトリクスを返すメソッド。
    // マップのキーはGraphDefinitionで返したメトリック名に対応する。
    FetchMetrics() (map[string]float64, error)
}

type PluginWithPrefix interface {
    Plugin

    // プラグインの名前を返す。
    // 同じプラグインを異なる環境で使いたい場合に設定する。
    // (例えばGitHub.comとGHEで分けるなど)
    MetricKeyPrefix() string
}

例えばGitHubのイシューをopenとclosedで分けて収集したい場合、プラグインは以下のようなメトリクスを返すように書きます。ここで、github-issuesMetricKeyPrefix()で返した値となり、1545103883 はメトリックを取得した時刻です。中央の数値は FetchMetrics()が返す値です。

custom.github-issues.open   20   1545103883
custom.github-issues.closed 40   1545103883

go-mackerel-plugin で書く場合、メトリック名は以下の要素が.で連結されたものです。

  • custom (固定)
  • MetricKeyPrefix()の値
  • GraphDefinition()で返したマップのキー名
  • GraphDefinition()で返したマップのMetrics[].Name

そのため、上の例と同じメトリック定義を返す場合は以下のような実装になります。

import mp "github.com/mackerelio/go-mackerel-plugin"

func (g *GitHubPlugin) GraphDefinition() map[string]mp.Graphs {
    return map[string]mp.Graphs{
        "": {
            Metrics: []mp.Metrics{
                {Name: "open", Label: "Open", Stacked: true},
                {Name: "closed", Label: "Closed", Stacked: true},
            },
        },
    }
}

リポジトリごとにメトリクスを分けたい場合

上の例では、custom.github-issues.opencustom.github-issues.closed の2つしか値を返していませんが、GitHubは複数のリポジトリを持っているので、リポジトリ単位で分けられたらいいな、と思いました。イメージとしては以下のようなメトリックです。

custom.github-issues.repos.taskfs.open      20   1545103883
custom.github-issues.repos.taskfs.closed    40   1545103883
custom.github-issues.repos.plan9port.open   1    1545103883
custom.github-issues.repos.plan9port.closed 2    1545103883

しかしGitHub上のリポジトリは増えたり減ったりするので、最初のGraphDefinition()では決まった名前を返すことができません。この場合、メトリック名に1箇所だけワイルドカード(# または *)を含めることができるので、リポジトリ名の部分をワイルドカードにすると対応できるようです。

リポジトリ名の部分にワイルドカードを使ったGraphDefinition()です。

import mp "github.com/mackerelio/go-mackerel-plugin"

func (g *GitHubPlugin) GraphDefinition() map[string]mp.Graphs {
    return map[string]mp.Graphs{
        "repos.#": {
            Metrics: []mp.Metrics{
                {Name: "open", Label: "Open", Stacked: true},
                {Name: "closed", Label: "Closed", Stacked: true},
            },
        },
    }
}

ただし、ホストメトリック#グラフ定義の投稿によるとワイルドカードは1箇所だけしか使えません。

またワイルドカード # は一つまでしか使えません。 メトリック名全体は ^custom(\.([-a-zA-Z0-9_]+|[*#]))+$ のようになります。

メトリックの値を収集する

これはGitHub APIを使って収集するだけなので簡単ですね。

func (g *GitHubPlugin) FetchMetrics() (map[string]float64, error) {
    metrics := make(map[string]float64)
    var opt github.IssueListOptions
    opt.State = "all"
    for {
        a, resp, err := g.c.Issues.List(g.ctx, true, &opt)
        if err != nil {
            return nil, err
        }
        for _, p := range a {
            metrics["repos."+*p.Repository.Name+"."+*p.State]++
        }
        if resp.NextPage == 0 {
            break
        }
        opt.Page = resp.NextPage
    }
    return metrics, nil
}

アクセストークンなどの管理

Mackerelプラグインでアクセストークンなどのシークレットを扱う場合、どうするのが正しいのかわかりませんでしたが、環境変数プラグインに渡すのが良さそうです。

s := os.Getenv("GITHUB_ACCESS_TOKEN")
token := &oauth2.Token{AccessToken: s}
ts := oauth2.StaticTokenSource(token)
c := github.NewClient(oauth2.NewClient(ctx, ts))

動作確認

一通り実装したらメトリックが取れているか確認しましょう。go-mackerel-plugin を使っているならそのまま実行すれば取得したメトリックを標準出力に書き出すので、これで確認することができます。ここで出力されない場合、GraphDefinition()のメトリック名とメトリック値の名前が食い違っていることが多いです。

$ go run path/to/plugin/main.go
github-issues.repos.zipcode.open    4   1545117743
github-issues.repos.taskfs.open     1   1545117743
github-issues.repos.pin.closed      1   1545117743

また、MACKEREL_AGENT_PLUGIN_META 環境変数に何かセットすると、グラフ定義をJSONで確認することができます。(以下の例は整形しています)

$ MACKEREL_AGENT_PLUGIN_META=1 go run path/to/plugin/main.go
# mackerel-agent-plugin
{
  "graphs": {
    "github-issues.repos.#": {
      "label": "GitHub Issues",
      "unit": "integer",
      "metrics": [
        {
          "name": "open",
          "label": "Open",
          "stacked": true
        },
        {
          "name": "closed",
          "label": "Closed",
          "stacked": true
        }
      ]
    }
  }
}

プラグインの組み込み

mackerel-agent.confプラグインの実行コマンドを追加してエージェントを再起動すればメトリックが収集されるようになります。下ではテストのためにgo runしていますが、通常はビルドしたコマンドを使いましょう。

[plugin.metrics.github]
command = "go run path/to/plugin/main.go"

他のサンプル

mackerel-agent-pluginsにいっぱいあるので参考になりました。

グラフの調整

上のプラグインでopen, closedのイシューをリポジトリ単位で取れるようになりましたが、このままだとopen/closedが全部積み重なって表示されるため少し読みづらいです。

f:id:lufiabb:20181218165439p:plain
オープン・クローズドが混ざったグラフ

終わったものと残っているものの推移を知りたいので、式を使ったグラフで対応しました。

  1. カスタムダッシュボードでグラフを追加
  2. グラフのタイプを 式グラフ に変更
  3. 式を書く
stack(
  group(
    alias(sum(host(3u5u9mHFmFS, custom.github-issues.repos.*.closed)), 'closed issues'),
    alias(sum(host(3u5u9mHFmFS, custom.github-issues.repos.*.open)), 'open issues')
  )
)

最終的にオープン・クローズドを分けてどれだけ消化したのかを見られるようになりました。上の式では全部のリポジトリをまとめて集計していますが、特定のリポジトリだけ取り出すことも簡単にできそうですね。

f:id:lufiabb:20181218165726p:plain
最終的なダッシュボード

式はカスタマイズしたグラフを表示するが分かりやすかったです。

悩んだところ

グラフ定義を変更したらエージェント再起動が必要?

正確には分かってませんが、開発中にグラフ定義をよく変更していました。このとき、エージェントを起動したままプラグインから返すグラフ定義を変更すると、変更した後に取得したメトリックの単位がfloatになっていたり、ワイルドカードを使ってもまとまらなかったりしました。

何かおかしいなと思ったらエージェントを再起動してみましょう。

グラフ定義を削除したい

上のように、間違ったグラフ定義が作られてしまった場合、不要な定義がいっぱい作られてしまうので、不要ならhttps://mackerel.io/my/graph-defs を開くと不要なグラフ定義を削除できるようです。

GoのPrintfでカスタムフォーマッタを使う

Goの fmt パッケージは、%s%3.4f のような書式を使えます。基本的にはCと同じですが、%#v%T などGo固有なものもあります。書式は基本的に

%[flag][width][.precision]{verb}

のように分かれていて、それぞれの数値は幅や0埋めなどを指定します。{verb}は必須ですが、それ以外は省略可能です。

func main() {
    fmt.Printf("%+07.*f\n", 3, 2.3)
    // Output: +02.300
}

f:id:lufiabb:20181215145145p:plain
書式の適用範囲

詳細な仕様は 公式ドキュメントの fmt パッケージに書かれています。

ポインタを整形したい

ただしポインタの場合、%d%xを使ってもポインタの値をフォーマットするだけです。ポインタが参照する先の値を扱うわけではありません。

func main() {
    i := 10
    p := &i
    fmt.Printf("%[1]p = %[1]d\n", p)
    // Output: 0xc000018088 = 824633819272
}

なのでポインタの場合に値をフォーマットしたい場合は *p のように参照しなければならないのですが、nil を参照するとパニックするので、分岐をしなければなりません。

func main() {
    i := 10
    p := &i
    if p == nil {
        fmt.Printf("<nil>\n")
    } else {
        fmt.Printf("%p = %d\n", p, *p)
    }
    // Output: 0xc00006e008 = 10
}

分岐を何度も書くのは良くないし、2つ以上の値を同時に出力しようとすると一気に複雑化するので、カスタムフォーマッタでうまく扱えないかと思いました。

fmt.Stringer を実装する

fmt.Println%v%sでフォーマットする場合、fmt.Stringer を実装しておくとそれが使われるようになります。また、%#v の場合は fmt.GoStringer があれば使われます。

type IntPtr struct {
    v *int
}

func (p IntPtr) String() string {
    if p.v == nil {
        return "<nil>"
    }
    return fmt.Sprintf("%d", *p.v)
}

func main() {
    p0 := IntPtr{nil}
    i := 10
    p1 := IntPtr{&i}
    fmt.Printf("%v %v\n", p0, p1)
    // Output: <nil> 10
}

ただし、この場合は %04x などの書式を扱うことができません。

fmt.Formatter を実装する

%d などでフォーマットする場合、型が fmt.Formatter を実装していれば、それが使われるようになります。fmt.Stringer と異なり、引数でfmt.Stateを受け取るので、これを使って整形ができます。また、fmt.Stateio.Writer を実装しているので、fmt.Fprintfの出力先として使えます。

type IntPtr struct {
    v *int
}

func (p IntPtr) Format(f fmt.State, c rune) {
    if p.v == nil {
        fmt.Fprintf(f, "<nil>")
        return
    }
    format := "%"
    if f.Flag('0') {
        format += "0"
    }
    if wid, ok := f.Width(); ok {
        format += fmt.Sprintf("%d", wid)
    }
    if prec, ok := f.Precision(); ok {
        format += fmt.Sprintf(".%d", prec)
    }
    format += string(c)
    fmt.Fprintf(f, format, *p.v)
}

func main() {
    i := 10
    p := IntPtr{&i}
    fmt.Printf("%[1]d %04[1]x\n", p)
    // Output: 10 000a
}

なぜIntPtrを構造体にしているか

Go言語仕様で、メソッドレシーバの基本型(base type)にポインタやインターフェイスを使えません。基本型というのは、メソッドのレシーバ型は T または *T が使えるけれども、両方における T のことです。これはMethod declarationsに書かれています。

Its type must be of the form T or *T (possibly using parentheses) where T is a type name. The type denoted by T is called the receiver base type; it must not be a pointer or interface type and it must be defined in the same package as the method.

なので、

type IntPtr *int

func (p IntPtr) String(f fmt.State, c rune) {
}

上のコードは、

invalid receiver type IntPtr (IntPtr is a pointer type)

というエラーでコンパイルできません。これはRe: named pointer type: invalid receiver typeによると、以下のような場合にどちらのGetを使えばいいのか明確ではないため、だそうです。

type I int

func (i I) Get() int {
    return int(i)
}

type P *I

func (p P) Get() int {
    return int(*p)
}

var v I
var x = (&v).Get()

http.RoundTripperでHTTPクライアントを拡張する

この記事はQiitaで公開されていました

GoでHTTPリクエストを行いたい場合、一般的には net/httphttp.Gethttp.Postを使うことになると思います。もしくは少し複雑なリクエストする場合、http.NewRequestを使うかもしれません。

上記どの方法を使うにしても、これらのリクエストはhttp.Client.Transportを通して、最終的にhttp.RoundTripper.RoundTripが呼び出されることになりますが、http.Client.Transporthttp.RoundTripper インターフェイスを満たしていれば良いので、ユーザが自由に置き換えることが可能です。http.RoundTripper の実装については、過去や今年のアドベントカレンダーに記事があるのでそちらを参照ください。

http.RoundTripper の具体的な話としては、例えば golang.org/x/oauth2 パッケージでは、アクセストークンの有効期限が切れた場合はリフレッシュトークンを使って自動更新する http.RoundTripper が組み込まれた http.Client を使います。こうした仕組みにより、ユーザ側のコードでOAuth2のヘッダを追加したりトークンを更新したりといった面倒な処理を書かなくてもうまく動くようになっています。

これと同様に、HTTPリクエストの事前処理または事後処理があるのなら、自作の http.RoundTripper を実装してみるといいんじゃないかなと思います。ただし自作する場合にとても重要なリソースの問題があって、http.Transportは内部でHTTP Keep-Aliveなどを行っているため、特に理由が無い場合は http.DefaultTransport を使い回すことが推奨されます。それでも、どうしても http.Transport を作らなければならない理由があるなら、全てのリクエストが終わった後で必ずhttp.Transport.CloseIdleConnectionsを呼びましょう。これを呼ばないとコネクションやゴルーチンなど色々なものがリークします。

リソース管理についてはGoのnet/httpクライアントで大量のリクエストを高速に行う方法にも少し書きました。

作ってみて便利だったもの

ここからは、今まで作った http.RoundTripper の実装で、個人的に便利だったものの紹介です。

リクエストとレスポンスを出力するもの

通過するリクエストとレスポンスのヘッダとボディを出力する http.RoundTripper です。デバッグする時だけ差し込めばいいのでとても楽でした。

type DumpTransport struct {
    Transport   http.RoundTripper
    Output      io.Writer
}

func (t *DumpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    w := t.Output
    if w == nil {
        w = os.Stdout
    }

    b, err := httputil.DumpRequest(req, true)
    if err != nil {
        return nil, err
    }
    if _, err := w.Write(b); err != nil {
        return nil, err
    }
    resp, err := t.Transport.RoundTrip(req)
    if err != nil {
        return nil, err
    }
    b, err = httputil.DumpResponse(resp, true)
    if err != nil {
        return nil, err
    }
    if _, err := w.Write(b); err != nil {
        return nil, err
    }
    return resp, nil
}

指数バックオフでリトライするもの

context で期限が来るまで指数バックオフする http.RoundTripper です。単純に増加するだけならそれほどでもないんですが、Retry-After ヘッダがあればそこまで待たなければならないなど地味に面倒だったりします。

type RetriableTransport struct {
    Transport http.RoundTripper
}

var retriableStatuses = map[int]struct{}{
    http.StatusTooManyRequests:     struct{}{},
    http.StatusInternalServerError: struct{}{},
    http.StatusServiceUnavailable:  struct{}{},
    http.StatusGatewayTimeout:      struct{}{},
}

func (p *RetriableTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    ctx := req.Context()
    var w backoff.Backoff
    for {
        resp, err := p.Transport.RoundTrip(req)
        if err != nil {
            if !isTemporary(err) {
                return nil, err
            }
            w.Wait(ctx)
            continue
        }
        if _, ok := retriableStatuses[resp.StatusCode]; !ok {
            return resp, nil
        }

        if d := ParseRetryAfter(resp, time.Now()); d > 0 {
            w.SetNext(d)
        }
        io.Copy(ioutil.Discard, resp.Body)
        resp.Body.Close()
        resp = nil
        if err := w.Wait(ctx); err != nil {
            return nil, err
        }
    }
}

type temporaryer interface {
    Temporary() bool
}

func isTemporary(err error) bool {
    e, ok := err.(temporaryer)
    return ok && e.Temporary()
}

一定期間内のリクエスト数を制限するもの

一定時間のリクエスト数、例えば1秒で最大100回までに制限する http.RoundTripper です。

type RateLimitTransport struct {
    Transport http.RoundTripper
    Interval  time.Duration
    Limit     int

    l    *rate.Limiter
    once sync.Once
}

func (t *RateLimitTransport) interval() time.Duration {
    return t.Interval / time.Duration(t.Limit)
}

func (t *RateLimitTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    t.once.Do(func() {
        n := rate.Every(t.interval())
        t.l = rate.NewLimiter(n, t.Limit)
    })
    ctx := req.Context()
    if err := t.l.Wait(ctx); err != nil {
        return nil, err
    }
    return t.Transport.RoundTrip(req)
}

rategolang.org/x/time/rate を使いました。

全てのリクエストが終わったことを保証するもの

通過したリクエスト全てが終わるまで待つ http.RoundTripper です。上で紹介したリトライやレートリミットと組み合わせると、待ち時間も含めて終わるまで待ちます。

type ClosableTransport struct {
    Transport http.RoundTripper

    wg     sync.WaitGroup
    closed int32
}

func (p *ClosableTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    p.wg.Add(1)
    defer p.wg.Done()
    if v := atomic.LoadInt32(&p.closed); v != 0 {
        return nil, errors.New("closed")
    }
    return p.Transport.RoundTrip(req)
}

func (p *ClosableTransport) Close() error {
    atomic.StoreInt32(&p.closed, 1)
    p.wg.Wait()
    return nil
}

おわりに

いかがだったでしょうか。ひとつひとつは簡単な実装ですが、例えば

  • 1分あたりのリクエスト数を300に制限する
  • リトライが必要なら指数バックオフでリトライする
  • リトライ分も含めて全て終わってからリソースを解放する
  • デバッグオプション付きの場合だけリクエストとレスポンスを出力する

という処理を愚直に実装すると、リトライやレートリミット解除待ちのリクエストをどう扱うのかなど地味に面倒なんですが、組み合わせで対応できるので個人的には便利だなと思っています。

ここで紹介したもの以外にもgithub.com/lufia/httpclientutilで公開しているので、興味があれば眺めてみてください。

また、似たようなパッケージもいくつかあります。サーバサイドのMiddlewareと区別して、http.RoundTripper実装のことをTripperwareと呼んでいるものもありますね。

http.RoundTripper 実装のヒントになれば幸いです。

GitHubへHTTPSアクセスする場合はトークンを使った方が良さそう

GitHubGitHub Enterpriseで、HTTPSを経由したgit push git pullをするとログインIDとパスワードの入力を促されますが、二段階認証を有効にしていると、正しいIDとパスワードでも認証がエラーになります。この場合、repo スコープを有効にしたトークンを用意して、パスワードの代わりにトークンを入力すれば良いです。

トークン入力を省略するため ~/.netrc の利用を案内する記事がありますけれども、さすがにトークンとはいえプレーンな形で保存するのは良くないので、最低でも暗号化しておきましょう。

macOSの場合は、いちど入力すると以降はKeychain Access.appに保存されるので~/.netrc は不要です。Keychain Access.appに保存されない場合は ~/.ssh/configUseKeychain yes を設定すると保存できるようになります。

そんなことを同僚氏に話したところ、なんか普通に2段階認証を設定してなくてもトークンを使うのが良いらしいということを聞いたので、今後はHTTPSトークンでアクセスしようと思いました。

HTTPSを使いたい理由

トークンを使わなくても、SSHでアクセスすれば問題なかったんですが、この時は珍しくサブモジュールを使いました。

サブモジュールを登録したリポジトリをJenkinsでビルドする場合、

checkout([
    $class: 'GitSCM',
    branches: scm.branches,
    extensions: scm.extensions + [
        [ $class: 'SubmoduleOption',
          disableSubmodules: false,
          parentCredentials: true,
          recursiveSubmodules: true,
          reference: '',
          trackingSubmodules: false
        ],
    ],
    userRemoteConfigs: scm.userRemoteConfigs
])

このように書けばサブモジュールも拾ってきてくれますが、JenkinsのGitHub Branch Source pluginGitHubAPIを利用してgit pullするので、parentCredentials: trueでメインリポジトリで使った資格情報を参照するとHTTPSトークンでアクセスできますが、SSHの場合はそれとは別に鍵の設定が必要になります。あまりCI環境への依存を増やしたくなかったので、サブモジュールのremoteHTTPSにしておく必要があってSSHが使えませんでした。

Go 2のgenerics/contract簡易まとめ

この記事はQiitaで公開されていました

2020年8月追記

この記事は2018年頃に出てきたGoのジェネリクスに関するドラフトを読んで、個人的に理解した内容をまとめたものです。2020年時点ですでに、contractの代わりにinterfaceが使われるなど、十分に内容が古くなってしまっているので最新仕様が知りたい人は新しい情報を探してください。

当時の内容

以下の内容は、ドラフトを読みながら書いているので、おそらく抜けている部分や理解不足なところはあると思います。特に、間違いがあれば修正しますので、この記事のコメント等で教えてください。原書は以下のリンクから。

導入

Goはこれまで、ジェネリクスも持っていないし継承もありませんでした。そのため、コレクションを扱う汎用的な方法は、interface{}などを使って多少ぎこちないけれどもうまくやるしかありませんでした。

例えばGo 1.11現在、標準の container/list はこんな感じです。

// 定義
package list

type Element struct {
    Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element

// 使い方
package main

l := list.New()
l.PushFront("test")
for e := l.Front(); e != nil; e = e.Next() {
    s := e.Value.(string)
    if s == "test" {
        ...
    }
}

元の型に戻すためには、扱う側で型アサーション(type assertion)しないといけません。リストとして動作はしていますが、もっと型の恩恵を受けたくなりますね。

また、標準の sort パッケージは、

func SearchFloat64s(a []float64, x float64) int
func SearchInts(a []int, x int) int
func SearchStrings(a []string, x string) int
type Float64Slice
type IntSlice
type StringSlice
func Float64s(a []float64)
func Ints(a []int)
func Strings(a []string)

など、よく使うプリミティブ型ごとに同じようなメソッドを提供してくれていますが、この辺りも、今後メンテナンスすることを考えるとつらいだろうなと思いますし、使う側も、例えば[]int32をソートしようと思った場合、

// これはできない
a := []int32{0, 1, 2}
sort.Ints(a) // cannot use s (type []int32) as type []int in argument to sort.Ints

// 詰め替えてあげる必要がある
a1 := make([]int, len(a))
for i := range a {
    a1[i] = int(a[i])
}
sort.Ints(a1)

のように、適切な配列へ詰め替えるか、sort.Interfaceを満たす型を自分で書くか、のどちらかが必要です。

ジェネリクスへの要望はずっと前からあったにも関わらず、言語の複雑さや実行速度などの影響から導入されてきませんでしたが、記事の最初で紹介したように、Go 2 Draft Designsジェネリクスのドラフトが上がってきました。Go 2と呼称していますが、実際はGo 1.15または1.16あたりを指しているようです。全く別の言語になるわけではありませんし、互換性も今まで通り維持されます。

追加される文法

ドラフトによると、ジェネリクスのために、型パラメータ型引数コントラクトが追加されます。

型パラメータと型引数

ジェネリックな型や関数は、名前の直後にカッコとtypeを使って実装します。型名の部分を型パラメータ (type parameter)と呼びます。

type List(type T) []T

func (l *List(T)) PushBack(x T)

type IntList = List(int) // typealias

func Keys(type K, V)(m map[K]V) []K

ジェネリックな型や関数を使う側は型名を渡します。型パラメータに渡す型は、型引数 (type argument)と呼びます。

var l List(string)
l.PushBack("hello")

// 引数から型引数がわかる場合は省略可能
keys := Keys(map[string]int{"A": 1, "B": 2})

他の言語、例えばJavaC++を経験した人は、なんで<T>[T]じゃないの、と思いますが、以下のようなケースで言語パーサが複雑になるからだそうです。

v := F<T>
n := f(a<b, c>d)

これらジェネリックな関数は、型パラメータの部分だけを適用することも可能です。上で挙げたsortパッケージの例をGo 2のジェネリクスで再定義すると、こんな感じでしょうか。

func Sort(type T)(a []T)
var Ints = Sort(int)
var Strings = Sort(string)

コントラクトの追加

型パラメータだけでは、例えばSort(type T)(a []T)の実装はできません。a[i]の値がa[j]と比べて大小どちらなのかを比較する方法が必要です。または実装によっては、sort.InterfaceのようにLess()メソッドを要求するかもしれません。しかし型パラメータだけでは必要な条件を表明することができません。

このため、ドラフトでは、contractを使って必要な条件を表明します。コントラクトにはinterfaceと同じように、メソッドシグネチャを書けますし、型名を書くこともできます。コントラクトの型パラメータは複数書くことができるので、区別のためメソッド名の前に型パラメータが必要です。

memo: ,で区切るとOR条件、別に分けるとAND条件

contract equaler(T) {
    T int, Equal(T) bool
}
contract comparer(T) {
    equaler(T)
    T Less(T) bool
}

contractの名前はよく小文字で表記されますが、型チェックで使うものなので、他のパッケージへエクスポートしている必要は 例を眺めた限りではおそらく ありません。(大文字開始の名前にすればエクスポート可能ですが)

こんな面倒なもの導入しなくても、ジェネリック関数の本文から必要な条件を抽出することもできるんじゃないのと思ってしまいますが、それだと内部実装を少し変更するだけでコントラクトも変わってしまうしエラーメッセージも不恰好になるため、コントラクトとして明記するように設計したようです。

型パラメータにコントラクトを追加

contractを参照する側は、型パラメータの後に続けてコントラクトを書きます。

func Sort(type T comparer(T))(a []T)

// contractのTは省略しても良い
func Sort(type T comparer)(a []T)

// 型パラメータが複数ある場合(この2つは同等)
func F(type T1, T2 comparer)(t1 T1, t2 T2)
func F(type T1, T2 comparer(T1, T2))(t1 T1, t2 T2)

コントラクト付きの型パラメータに、型引数を与えたコードをコンパイルすると、型引数がコントラクトを満たしているかのチェックがコンパイラで行われます。

interfaceとの違い

とりあえず気づいたところを2つ。他にもあるかもしれません。

  • interfaceは常にポインタと型を持つけどジェネリクスは値型のまま使える
  • interfaceは実行時にメソッド解決だけどジェネリクスコンパイル時に解決する?
  • 型の混在したコンテナのようなものはinterfaceでなければできない?

感想と告知

他の言語と様子が全然違いますが、Goのジェネリクスについて雰囲気はつかめたでしょうか。最初のバージョンと比べてだいぶ理解しやすくなったと思いますし、インターフェイスとの使い分けも分かりやすくなったんじゃないかなと感じました。