Goの外部パッケージに独自の変更を加える

外部パッケージを使っていて、ちょっとした修正を試したい時は以下のような方法があります。

1. 外部パッケージをForkしてgo.modで置き換える

最も基本的なやり方なので特に理由がなければこちらの方法にするのが良いでしょう。
対象のパッケージのForkに変更を加え、以下のようにしてgo.modで置き換えます。

go mod edit -replace github.com/EXTERNAL_/PACKAGE@v1.0.0=github.com/daisuzu/PACKAGE@development
go mod tidy

なお、Forkにコミットを追加する場合はその度にgo mod tidyで擬似バージョンを更新していく必要があります。

2. 外部パッケージのコピーをリポジトリに追加してgo.modで置き換える

Forkを作りたくなかったり、試行錯誤したい場合などはリポジトリの中に対象の外部パッケージをコピーする方がやりやすいかもしれません。
その場合も同様にgo.modで置き換えます。

go mod edit -replace github.com/EXTERNAL_/PACKAGE@v1.0.0=./PATH_TO_COPY

3. 変更したファイルをリポジトリに追加してoverlayで書き換える

変更が数行程度だと上記の方法が面倒だと感じることがあるかもしれません。
その場合は変更したファイルのみをリポジトリに追加し、goコマンドのoverlayフラグで書き換えることも可能です。

以下のようなoverlay.jsonを用意してgo build -overlay=overlay.jsonのように指定します。

{
  "Replace": {
    "/GO_MOD_CACHE/PATH_TO_EXTERNAL_PACKAGE/TARGET.go": "/PATH_TO_COPY/TARGET.go"
  }
}

いずれの方法もうまくいったら本家に還元しましょう。

Vimの極意

この記事はVim Advent Calendar 2022の1日目の記事です。

今年でVimをメインエディタにして15年になります。
最近どうすれば思考する速度でテキストを編集できるようになる*1のか考えたりすることがあったので、この機会に軽くまとめてみます。

簡単な操作であれば「○○をしたい」と思った瞬間にそうなっていることもありますが、実際はそうならないことがの方が多いです。
それが何故なのかというと、複雑な編集をする際には自分のやりたいことをVimの操作に変換する必要があり、そこに時間がかかっているからだと考えました。

そこで思いついたのが、やろうとしていること自体をVimのコマンド群として捉えられるようになればさらに高速にテキストを編集できるのではないか、ということです。

具体例をあげてみると、以下のようなGoのコードでカーソルがfuncのfにある時に、戻り値の型をClientInterfaceから*Clientに変更したいと思ったら、

func NewClient() ClientInterface {
    return newDefaultClient()
}

戻り値の位置にカーソルを移動して、カーソル下の単語を*Clientに書き換えよう、と考えてVimを操作するのではなく、
最初から$bcw*Clientと考えてVimを操作します。

つまり、「あらゆる編集操作をVimのコマンドで表現できるようになる」ことがVimの極意ということになります。

実際に文章に書いて読んでみるとかなり難しそうな気がしてきましたが、
これを会得する方法としては以下の7つが考えられます。

  1. 極意のことを意識しながらVimを使い続ける
  2. 定期的にVimのヘルプを読み返してコマンドとしての語彙を増やす
  3. 指に負担を感じたら少し立ち止まってより良い方法がないか調べる
  4. VimGolfをやる
  5. 覚えにくい処理をユーザー定義コマンドやマッピングにする
    • vimrcを育てる
  6. 4で汎用性が高いものをプラグインにする
    • 既に似たようなプラグインがあればそれを使っても良い
  7. 5で有用なものをVimの本体に組み込む
    • Vim本体の実装に対する知識が必要

ある程度身に付いてくれば先のサンプルコードで戻り値の型がわからなくてもすぐに
/newD<CR><C-]>Wyiw<C-T>j%bcw<C-R>0<ESC>が出てくるようになることでしょう。

ぜひ試してみてください。

*1:実践Vim 第21章の続き

goplsに独自Analyzerを組み込む

internal/lsp/source/options.godefaultAnalyzers()が返すmapに自作のAnalyzerを追加してgo installすれば使えるようになる*1

用途としてはチームのコーディング規約をtextDocument/diagnosticでチェックしたり、チェックに引っかかったコードの修正や一部だけ実装したコードの続きを生成するtextDocument/codeActionを実行したりなど。

今まではCIで独自Analyzerとreviewdog + action-suggesterを用いてチェックやコード修正を行っていたものを、goplsに組み込むことによってより早いコーディングのタイミングで行えるようになった。

もちろん補完やコードジャンプ、コード生成(textDocument/codeLens)などをカスタマイズしてさらに便利にすることもできるけど、本体の変更に追従するのが大変になりそうなので今のところはやっていない。
Analyzerの追加だけであればコンフリクトのことはほとんど考えなくて良いので定期的にupstreamを取り込むように設定するくらいでメンテナンスに関しては特に問題なさそう。
それにgoplsのCLI版で使えない機能を増やしすぎてしまうと、GoLandなどLSPに対応していないツールと差が開きすぎてしまうのも理由の一つ*2

*1:gopls/v0.9.4の場合

*2:今のチームは半分近くのメンバーがGoLandを使っているので

QNAPをTS-233に移行した

今までTS-231を使っていたけど、たまに落ちることがあったり去年ついにサポートが切れてしまったのでTS-233に移行した。

手順はTS-231の電源を落として、HDDを入れ替えて、TS-233を起動するだけ。

起動後は固定IPにしていたのがDHCPになっていた*1のでQfinderで探し、一応IP設定を戻しておいた。
それ以外はデータもアカウントもそのまま引き継がれていたので特に何もしなくて良かった。*2

普段からそんなに読み書きはしていなかったのであまり性能の違いを感じられていないけど、ブラウザからアクセスした時の画面がもたつかなくなっていたのは嬉しいポイント。
あとは長期間安定して動いてくれることを祈るばかり。

*1:TS-231にはNICが2枚あって、Adapter1を固定IP、Adapter2をDHCPにしていたらAdapter2の方が引き継がれたっぽい

*2:https://www.qnap.com/ja-jp/nas-migration/?os=qts&source=ts-231&destination=ts-233 に書いてあったものの、やってみるまではちょっと不安だった

golang.org/x/exp/jsonrpc2を使ってgoplsに接続する

最近golang.org/x/expjsonrpc2が追加されていることを知ったので試してみた。 pkg.go.dev

基本的な使い方としてはDialConnectionを作成し、

  • レスポンスが返ってくるメソッドはCall + Await
  • レスポンスが返ってこないメソッドはNotify

を呼べば良い。

Content-Lengthなんかの処理はConnectionOptionsがデフォルトで設定するHeaderFramerがやってくれる。

以前に書いたgoplsに接続するコードに比べるとだいぶスッキリした。 daisuzu.hatenablog.com

import (
    "context"
    "net"

    "golang.org/x/exp/jsonrpc2"
)

type Client struct {
    conn *jsonrpc2.Connection
}

func Connect(ctx context.Context, addr string, initializeParams map[string]any) (*Client, error) {
    conn, err := jsonrpc2.Dial(ctx,
        jsonrpc2.NetDialer("tcp", addr, net.Dialer{}),
        jsonrpc2.ConnectionOptions{}, // goplsに繋ぐ時は全てデフォルト値でOK
    )
    if err != nil {
        return nil, err
    }

    var initializeResult map[string]any // このサンプルではレスポンスを受け取るだけで使わない
    if err := conn.Call(ctx, "initialize", initializeParams).Await(ctx, &initializeResult); err != nil {
        return nil, err
    }

    if err := conn.Notify(ctx, "initialized", map[string]any{}); err != nil {
        return nil, err
    }

    return &Client{conn: conn}, nil
}

func (c *Client) Shutdown(ctx context.Context) error {
    return c.conn.Notify(ctx, "shutdown", map[string]any{})
}

go-cmpでmap内の時間文字列を近似比較する

以下のような関数をテストする際、期待する値もtime.Now()で生成して概ね問題ない。

func f() map[string]interface{} {
    return map[string]interface{}{
        "time": time.Now().Format("2006-01-02 15:04:05"),
    }
}

ただし関数内に多くの処理がある場合など、ごく稀に時間がズレてFAILしてしまうことがある。
こういった関数を作らないことで回避することもできるが、cmp.FilterPathを使うと次のような比較処理を実装できる。

  1. mapの特定のキーに対して、
  2. 値をinterface{}からtime.Timeに変換し、
  3. 2つの値の差が1秒以下ならOKとする
cmp.FilterPath(func(p cmp.Path) bool {
    if mi, ok := p.Index(-1).(cmp.MapIndex); ok {
        // 1. mapのキーがtimeの場合はComparerを使う
        return mi.Key().String() == "time"
    }
    return false
}, cmp.Comparer(func(x, y interface{}) bool {
    // 2. time.Timeに変換する
    xt, ok := parseTime(x)
    if !ok {
        return false
    }
    yt, ok := parseTime(y)
    if !ok {
        return false
    }

    if xt.Before(yt) {
        // xt.Sub(yt)が負の値にならないように入れ替える
        xt, yt = yt, xt
    }

    // 3. Durationが1秒以下ならOKとする
    return xt.Sub(yt) <= time.Second
}))

使用例はこちらgo.dev

Vimのterminalでパイプを使う

この記事はVim Advent Calendar 2021の9日目の記事です。

Vimにterminal機能が追加されてずいぶん経ちましたが、普段はtmux上でVimを使っていたので実際のところ使用頻度はそんなに高くありませんでした。
たまに使った時は出力がVimの中に閉じているため、検索したり編集したりはtmuxのペイン分割と比べてやりやすいなと思うことがあったくらいです。

ただ、最近になって似たようなコマンドを何度も実行することが増えてきて、その度にシェルの履歴から探してきてはコマンドライン引数を変更して実行するのが煩わしくなってきました。
例えば以下のようなコマンドです。

go test \
    # 1. 詳細な出力が欲しい(-v)
    # 2. カバレッジが取りたい(-covermode, -coverprofile, -coverpkg)
    # 3. 実行するテストを指定したい(-run)
    # 4. goldenファイルを更新したい(-golden)
    # 5. テストの分析がしたい時は出力を自作コマンドに流し込む

1〜4までは:executeを使って組み立てたcmdを実行するようにすれば良いので簡単です。

:execute 'terminal ' . cmd

ところが5で出力を流すのに、パイプ(|)をどのように使えば良いのかわかりませんでした。
いくつか試してみたところ、bash(やzsh)の-cオプションに""で実行したいコマンドを渡してあげれば良いことがわかりました。

" grepが効かない(`|`以降もvimコマンドの引数として扱われる)
:terminal vim -h | grep vimrc

" エラー
:terminal bash -c 'vim -h | grep vimrc'

" OK
:terminal bash -c "vim -h | grep vimrc"

これを自作コマンドにしておくと簡単に実行できます。

当初は柔軟に実行できる形にしようと思っていましたが、組み合わせはある程度固定化されていたので3、4パターンほどコマンドとして定義しておき、必要に応じて修正するような使い方をしています。
シェルスクリプトMakefileにしておくのも手かと思いますが、管理の手間などを考えると自分にとってはvimrcに書いておくのが一番楽でした。

curlとjqを組み合わせても良さそうなので、必要になったら同じようにしてやってみようと思っています。