Plan 9とGo言語のブログ

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

Go製バイナリを配布するためのGitHubワークフロー

前置き

以前、BuildInfoからバージョンを取得する方法を紹介しました。

blog.lufia.org

go installで正規の公開されたバージョンをインストールした場合は、以下の出力においてmodの行が示すように、sum.golang.orgチェックサム等が検証されてバイナリのメタデータに埋め込まれます。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    v0.0.2  h1:JWm92Aw8pSKJ4eHiQZIsE/4rgwk3h5CjEbJ/S30wiOU=
    build   -buildmode=exe
    build   -compiler=gc
    build   -trimpath=true
    build   DefaultGODEBUG=httplaxcontentlength=1,httpmuxgo121=1,panicnil=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
    build   CGO_ENABLED=0
    build   GOARCH=amd64
    build   GOOS=linux
    build   GOAMD64=v1

上記の出力から「dotsyncのバージョン0.0.2をgo1.22.2でビルドした」ことを読み取れますね。チェックサムhttps://sum.golang.org/lookup/<module-path>@<version> のようなURLにアクセスすると、正規のものかどうかの確認を行えます。完全なURLの仕様はGo Modules Reference/Checksum databaseをみてください。

このチェックサムが一度でも登録されてしまった後は、消したり変更したりできません。消せないのは困ると思うかもしれませんが、proxy.golang.org

I removed a bad release from my repository but it still appears in the mirror, what should I do?

への回答があるので、不備などではなく意図してデザインされていることが読み取れます。削除はできないものの、Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法のようにすれば意思を表明することは可能です。

正しくソースコードからビルドされたことを検証する

主に以下の条件を満たす*1場合、Go 1.21以降では生成するバイナリが完全に一致するので、同じパラメータを与えて手元でビルドしてみると検証できます。

  • Goコンパイラのバージョンが同じ
  • GOOS, GOARCH, GOAMD64 などターゲットが同じ
  • cgoを使わない
  • os/user, netなどで動的リンクをしない
  • ビルドするディレクトリ名が同じ、または-trimpathオプションを与える

以下の例はlegoコマンドをLinuxPlan 9でビルドしたものですが、同じハッシュ値になっている様子が分かると思います。

Linuxでビルド

$ go version
go version go1.22.2 linux/amd64

$ export GOTOOLCHAIN=go1.22.1
$ export GOOS=plan9
$ export GOARCH=amd64
$ export GOAMD64=v1
$ export CGO_ENABLED=0
$ go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

$ sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

Plan 9でビルド

% go version
go version go1.22.1 plan9/386

% GOTOOLCHAIN=go1.22.1
% GOOS=plan9
% GOARCH=amd64
% GOAMD64=v1
% CGO_ENABLED=0
% go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

% sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

再現可能なビルド

このように、第三者が特定のソースコードから生成されたものであると検証できるような概念は「再現可能なビルド」とか「再現性のあるビルド」と呼ばれるようです。

また、Go 1.21以降は、Goのコンパイラやライブラリも再現可能になっているようです。

コード署名とは違うのか

コード署名は、誰がビルドしたものなのかを検証できますが、特定のソースコードから生成されたものかどうかは保証しません。例えばビルドプロセスの途中で改ざんが行われた場合、コードの署名は正しく検証を通ってしまいます。

go buildの場合は正規のバージョンが入らない

ようやく本題です。この記事の冒頭で挙げたエントリでも書いたように、手元にソースコードを置いてgo buildした場合などでは、メインモジュールのバージョンやチェックサムが埋め込まれずに (devel) という文字列になります。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    (devel) 
    ...

(devel) の代わりに(おそらくv1.0.1-0.20240418xxxのような)疑似バージョンを埋め込む提案がcmd/go: stamp the pseudo-version in builds generated by go buildで承認されていますが、それでも正規のバージョンとは区別されていますし、少なくともGo 1.22の時点ではまだ実装されていません。

GoReleaserでビルドしたバイナリはメインモジュールのチェックサムを持たない

上記と同様に、2024年5月時点では、GoReleaserGoReleaser Actionでビルドしたバイナリは公開された正規なバージョンを持ちません。

実用上は致命的に困るものではないけれど、どうせなら検証可能になっていたほうが嬉しいですね。

GitHub Releasesでリリースしたらビルドして成果物に追加するワークフロー

というわけで、正規のバージョンが埋め込まれたバイナリをリリースするためのワークフローを作ってみました。以下のワークフローは、GitHub Releasesで新しいバージョンをPublishすると開始して、最終的にバイナリをリリースに添付します。

name: Release

on:
  release:
    types:
    - published
jobs:
  release:
    strategy:
      matrix:
        os:
        - linux
        - darwin
        - windows
        arch:
        - amd64
        - arm64
        include:
        - format: tgz
        - os: windows
          format: zip
        exclude:
        - os: darwin
          arch: amd64
        - os: windows
          arch: arm64
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version: stable
    - name: Build the package
      uses: lufia/workflows/.github/actions/go-install@v0.4.0
      with:
        package-path: github.com/lufia/dotsync
        version: ${{ github.ref_name }}
      env:
        GOOS: ${{ matrix.os }}
        GOARCH: ${{ matrix.arch }}
        CGO_ENABLED: 0
      id: build
    - name: Create the asset consists of the build artifacts
      uses: lufia/workflows/.github/actions/upload-asset@v0.4.0
      with:
        tag: ${{ github.ref_name }}
        path: >
          ${{ steps.build.outputs.target }}
          LICENSE
          README.md
        name: dotsync-${{ github.ref_name }}.${{ matrix.os }}-${{ matrix.arch }}
        format: ${{ matrix.format }}

  upload:
    needs: release
    permissions:
      contents: write
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/download-artifact@v4
      with:
        path: assets
        merge-multiple: true
    - name: Upload the assets to the release
      run: gh release upload -R "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" assets/*
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

どうでしょうか。記述量は多いですが、1/3くらいはビルド用のマトリクスを作っているところなので、第一印象ほど複雑ではないかなと思います。

ワークフローの途中で読んでいる複合アクションは以下の2つなので、興味があれば眺めてみてください。

*1:他にもあるかもしれないけど、これだけ揃えればだいたい同じになるはず

Steamクライアントが起動しなくなっていた

2024年2月ごろにSteamクライアントを更新してから、以下のログで停止して起動しなくなっていました。

$ flatpak run com.valvesoftware.Steam
...
Steam Runtime Launch Service: starting steam-runtime-launcher-service
Steam Runtime Launch Service: steam-runtime-launcher-service is running pid 34081
bus_name=com.steampowered.PressureVessel.LaunchAlongsideSteam

Steam is killed with no error message when steam runtime launch service is ranによると、落ちている原因はSteamにbackgroundパーミッションを与えていないからのようなので、以下のコマンドで追加すると起動できるようになります。

$ flatpak permission-set background background com.valvesoftware.Steam yes

ここでbackgroundが2つ並んでいるのは正しくて、最初がパーミッションストアのテーブル名、2つ目がパーミッションストアのオブジェクトの名前です。

以下のような結果になっていれば動作します。

$ flatpak permission-show com.valvesoftware.Steam
Table      Object     App                     Permissions                  Data
background background com.valvesoftware.Steam yes                          0x00

Titan Security Keyの新しいバージョンがPasskeysに対応していた

Titan Security Keyがパスキーに対応していたようですが、Google Storeの製品ページを見ても、それがパスキー対応したバージョンのTitan Security Keyなのか分からなくて混乱しました*1。ストア上では全部同じ名前でバージョン表記も無いし、国内販売が遅れた事例も過去にあるので「国内版はまだ古いバージョンだったりしないか」が気になります。

そこで、過去の記事などから形状と対応規格を調べてみました。過去バージョンは公式のGoogle Titan セキュリティ キーの安全および保証ガイドを参照しています。

先に結論を述べると、2024年1月時点で販売されているTitan Security Keyはパスキー対応と謳われているバージョンです。

2018年

  • Model: K9T (USB-A/NFC)
  • Model: K13T (Micro-USB/NFC/BLE)

Titan Security Keys: Now available on the Google Storeで登場したバージョンです。購入したパッケージに2つ同梱されていました。もともと2018年に登場していましたが、国内販売が遅れて2019年販売開始でした。過去記事で買ったのはこれ。

blog.lufia.org

具体的な形状はGoogleの安全な2段階認証を構築し不正アクセスを防ぐ物理キー「Titan セキュリティ キー」が日本で登場 - GIGAZINEの写真を見てもらえれば分かりますが、K13Tは卵形をしていて中央に楕円のボタンがあります。

2019年

  • Model: YT1 (USB-C)

USB-C Titan Security Keys - available tomorrow in the US でUSB-C版が追加になりました。こちらも形状はGoogleの2段階認証を構築する「Titan セキュリティ キー」にUSB Type-Cタイプが登場 - GIGAZINEの写真を見てもらえれば分かりますが、円形の鍵穴とTITANの刻印があります。

ところで、Titan Security Key - Wikipediaには

USB-C/NFC

と書かれているけど、YT1の安全および保証ガイドを見るとNFCは含まれていないので間違いかなと思っています。

2021年

  • Model: K40T (USB-C/NFC)

この辺りはよく知らないのですが、おそらくSimplifying Titan Security Key options for our usersの辺りでアップデートされたバージョンです。接続インターフェイスとしてのBLEが廃止となる代わりにNFCが追加になったようです。

K40Tの画像はGoogle「Titan セキュリティキー」ってどう使うの?NFCによるスマホ利用にも対応を見てください。円形の鍵穴と丸いボタンがあります。

これも、Titan Security Key - Wikipediaによると

supporting U2F and FIDO2

とありますが、Google USB-C/NFC Titan Security Key ReviewではFIDO U2F対応(FIDO2ではない)とあるので間違いかなと思っています。

2023年

  • Model: K51T (USB-A/NFC)
  • Model: K52T (USB-C/NFC)

The latest Titan Security Key is in the Google Storeで更新されたバージョンで、現在販売されているモデルです。公式情報にFIDO2対応とあるので本物でしょう。

形状は、ストアの画像はおそらく次のバージョンが出たら変わってしまうので、その場合は最大250種類のパスキーの保存が可能なGoogle Titan セキュリティ キーを使ってパスキー認証してみた - GIGAZINEの写真をみてください。楕円形の鍵穴と四角のボタンがあります。

*1:本当に困るので更新日くらいは書いてほしい

Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法

最近のGoには、関数やパッケージを非推奨と扱う方法があります。まとまっていると便利かなと思うので、種類ごとにまとめてみました。GoDocコメントを多用するので、GoDocを書き慣れていない場合は以下も参考にしてください。

blog.lufia.org

関数と型を非推奨にする

関数コメントに、// Deprecated: ではじまる段落を追加します。

// Parse parses a string of the form <status>=<status>.
//
// Deprecated: Use ParseStatusMap instead.
func Parse(src string) (map[Status]Status, error) {
    ...
}

型の場合も同様に。

// Error is the interface that wraps Error method. 
//
/// Deprecated: Use error instead.
type Error interface {
    ...
}

これを含んだバージョンをリリースすると、GoDocでは以下のように表示されます。

関数名と並んでDEPRECATEDと描画されている様子

ただし、関数や型の非推奨化を検出するLinterは存在します*1が、標準のツールでは警告等を出力しません。これはおそらく、非推奨だとしても互換性を維持するために関数は残り続ける習慣があるので、使い続けても支障はないためじゃないかなと思っています。

互換性を維持しつつ変更を加える方法は、以下の記事が参考になります。

変数と定数を非推奨にする

これも // Deprecated: コメントを追加します。

// ZP is the zero Point.
//
// Deprecated: Use a literal image.Point{} instead.
var ZP Point

定数も同様。

const (
    ...
    // Deprecated: Use TypeReg instead.
    TypeRegA = '\x00'
    ...
)

定数をまとめて非推奨にしたい場合はconstの前に書きます。

// Seek whence values.
//
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
    SEEK_SET int = 0 // seek relative to the origin of the file
    SEEK_CUR int = 1 // seek relative to the current offset
    SEEK_END int = 2 // seek relative to the end
)

2023年現在、変数または定数の場合は、GoDocの上では特別な表示をしていません。

構造体のフィールドを非推奨にする

型そのものではなく、一部のフィールドだけ非推奨としたい場合は、該当フィールドのコメントに // Deprecated: ではじまる段落を追加します。

type FileHeader struct {
    ...
    // ModifiedTime is an MS-DOS-encoded time.
    //
    // Deprecated: Use Modified instead.
    ModifiedTime uint16
    ...
}

行コメントの場合はこのように。

// PipeNode holds a pipeline with optional declaration
type PipeNode struct {
    ...
    Line     int             // The line number in the input. Deprecated: Kept for compatibility.
    ...
}

2023年現在、GoDocの上では特別な表示をしていません。

モジュールの特定バージョンを撤回する

壊れた状態でリリースしたしまったとか、関数名をtypoしていた等でバージョンを撤回したい場合があると思います。この場合は、go.modretractディレクティブを追加して、新しいバージョンを公開します。

retract v0.1.0 // Contains a misleading function name.

retractディレクティブでバージョンを示すと、モジュール上では撤回されたバージョンとして扱われます。GoDocでの表示はこのようになります。

撤回されたフラグが描画されている様子

また、撤回されたバージョンを参照したとき、goコマンドによって以下のような警告が出力されます。

go: warning: github.com/mackerelio/checkers@v0.1.0: retracted by module author: Contains a misleading function name.

retract ディレクティブは複数のバージョンをまとめて撤回したりもできます。詳細はドキュメントを参照してください。

モジュールそのものを非推奨にする

新しいメジャーバージョンを公開したので古いほうを非推奨としたい、またはメンテナンスを縮小するので非推奨としたい場合にはモジュールそのものを非推奨とすることもできます。

この場合は、go.modmoduleディレクティブに // Deprecated: を書きます。

// Deprecated: Use github.com/gomodule/redigo instead.
module github.com/garyburd/redigo

GoDocでの表示はこのようになります。

モジュールは非推奨と警告を出している様子

公式のドキュメントは以下です。

*1:staticcheck

GitHub ProjectsのTracksとTracked byを設定する

GitHub ProjectsにはTracksフィールドとTracked byフィールドがあります。

フィールド選択のところで確認できます

これらのフィールドは、新しいタスクリストでissueやプルリクエストを追加すると追跡の対象となります。

```[tasklist]
## Tasks
- [ ] #1234
- [ ] #1233
```

こうすると、タスクリストを記述した側のissueは#1234#1233を追跡している(Tracks)ことになり、反対に#1234#1233は親のissueに追跡された状態(Tracked by)となります。複数の親issueから1つの子issueを追跡することも可能です。この場合、2つのissueから参照された子issueは、2つのTracksを持ちます。

従来のタスクリストを使ってもUI上では追跡されているようにみえますが、どうやら従来のタスクリストはTracksTracked byの対象となっていません。

追跡されているようにみえるけど対象にならない

GitHub Projectsのタスクリストと追跡については公式ドキュメントがあります。

GitHub上のタスクリスト

2023年8月現在、GitHubのissueやプルリクエストでタスクリストを作成するには2種類の方法があります。

1つ目は従来からあるタスクリストです。以下のように記述します。

- [ ] task
- [ ] task

この方法は、日本語版のドキュメントではタスクリスト、英語版のドキュメントではtask listsと表現されています。

もうひとつ、昨年あたりに追加された方法があります。新しいタスクリストの書き方はこのようになります。

```[tasklist]
## section
- [ ] task
- [ ] task
```

こちらの方法は、日本語版だと従来と同じタスクリストと呼ばれていますが、英語版ではtasklistsと使い分けがされているようです。

新しいタスクリストがただのコードブロックになる場合

新しいタスクリストは、ドキュメントによるとまだプライベートベータです。Organization単位で有効化されるようなので、タスクリストがコードブロックになる場合は、該当のOrganizationはまだ無効なのかもしれません。この場合は、有効化したいOrganizationのAdminを持ったアカウントでWaitlistに登録しておくと、そのうち使えるようになります。

ただし、Waitlistに登録するフォームはOrganizationの指定が必須となっていたので、個人のアカウントはWaitlistに登録できません*1

*1:Feature Previewにも無かったので個人のリポジトリでは使えない気がする

systemd-homed環境でユーザーにログインできなくなる原因と対処

ここ数年はLinuxデスクトップで生活しているが、たまにユーザー環境へログインできなくなることがあった。

症状

これまで何の問題もなく動いていたが、再起動した後に突然ログインできなくなる。gdm を使っているけれども、パスワード入力のあと何のエラーもなくパスワード再入力画面に戻る。このとき、パスワードを間違えると「認証に失敗しました」のようなエラーになるのでパスワードは合っていると思われる。

root ではログインできるので、ログインした後に systemd-journald のログをみると以下のような行が記録される。

$ journalctl --since=today
systemd-homed[532]: lufia: changing state inactive → activating-for-acquire
systemd-homework[2917]: Provided password unlocks user record.
systemd-homework[2917]: Home directory /home/lufia exists, is not mounted but populated, refusing.
systemd-homed[532]: Activation failed: Device or resource busy

systemd-homed では一般的に /home/$USER は空のディレクトリで、ログインしたとき各種ストレージ方式ごとに必要なファイルをホームディレクトリとしてマウントする。例えば /home/$USER.home (LUKSストレージの場合)や /home/$USER.homedir (ディレクトリ方式の場合)を /home/$USER にマウントすることになる。

なのでログインする時点では /home/$USER は空であるはずが、エラーメッセージには /home/lufia が空ではないのでマウントに失敗したとあり、実際に /home/lufia 以下には空のディレクトリが作られていた。この謎のファイルは消しても一定時間で復活する状態だった。

fsck -f でディスクの検査をしても何もエラーは検出されない。

原因

以下の条件が揃えば同様の症状になる。

  • systemd-homedでユーザーを管理している
  • Dockerが動作している
  • /home/$USER 以下のファイルをボリュームマウントしたdockerコンテナが再起動時に残っている

Dockerはボリュームマウントしたとき、ディレクトリがなければ作成する。Dockerコンテナ実行時は /home/$USER にホームディレクトリがマウントされているので問題なく参照できるが、Linuxを再起動したときはまだユーザーがログインしていないので空のディレクトリになっている。そこでDockerは残っているコンテナを復活させようとして、/home/$USER 以下にファイルを作ってしまう。そうすると、実際にログインしたときは /home/$USER 以下に(Dockerが作成した)ファイルが存在するため、マウントできずエラーになってしまう。

対処方法

この状況になった場合は、root などでログインしたあと以下の手順を実行する。

  • Dockerに残っているボリュームマウントしているコンテナを止める

または

  • docker.service を止める
  • /home/$USER に残っているファイルを消す
  • $USER でログインして docker.service を開始する

感想

Dockerも systemd-homed も、それぞれは意図した動作をしているだけなので、困りますね。

Goの型パラメータを使って型付きバリデータを作っている

型パラメータ(generics)とerrors.Joinを使ってバリデータを作っています。

github.com

経緯

Goで値のバリデーションを行う場合、有名なライブラリには以下の2つがあります。

go-playground/validator はよくある構造体のフィールドに validate:"required" のようにタグを付けるスタイルのバリデータです。awesome-goで紹介されているvalidatorのほとんどはこのスタイルで人気もありますが、個人的には validate:"required" 程度ならともかく validate:"oneof='red green' 'blue'" あたりからは、手軽さよりも複雑さの方が強くて少々厳しいなと感じます。

一方、 go-ozzo/ozz-validation の方はコードで記述するスタイルのバリデータです。公式のサンプルを借用すると、

a := Address{
    Street: "123",
    City:   "Unknown",
    State:  "Virginia",
    Zip:    "12345",
}
err := validation.ValidateStruct(&a,
    validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
    validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
    validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
    validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
)
fmt.Println(err)

とても印象は良かったのですが、残念ながら2020年ごろからメンテされなくなっており、IssueやPull requestの状況をみる限りは枯れて安定している様子もないので、今このライブラリに依存するのも怖いなと思います。

go-ozzo/ozzo-validation から派生したリポジトリjellydator/validationというリポジトリがあり、これは今もメンテされているようですが、Go 1.18で入った型パラメータ(generics)とか、1.20で入った errors.Join を使ってみたい気持ちがありました。

そういう理由で、上で挙げた go-validator を自作しています。

go-validatorの使い方

例えば以下のコードでは、createUserRequestValidatorValidate(context.Context, *CreateUserRequest) メソッドを持つバリデータです。

import (
    "context"

    "github.com/lufia/go-validator"
)

var createUserRequestValidator = validator.Struct(func(s validator.StructRule, r *CreateUserRequest) {
    validator.AddField(s, &r.Name, "name", validator.Length[string](5, 20))
    validator.AddField(s, &r.Provider, "provider", validator.In(Google, Apple, GitHub))
    validator.AddField(s, &r.Theme, "theme", validator.In("light", "dark"))
})
err := createUserRequestValidator.Validate(context.Background(), &CreateUserRequest{})
fmt.Println(err)

実装するときに気をつけたことをいくつか挙げます。

型のあるバリデータ

バリデータはanyではなく個別の型を持たせています。これは必ず達成したいと思っていました。Goの型パラメータは関数引数などから推論されるので、上記では validator.Struct に渡している関数や validator.In に渡している値の型などからバリデータの型を決定しています。

同様に、構造体フィールドも型推論によって決定したかったので、validator.AddField にはフィールドのポインタを渡すようにしています。Validateの内部では r の先頭ポインタとのオフセットを使って reflect でフィールドを探しています。

複数のエラーを扱う

複数の項目で違反があった場合は errors.Join で複数のエラーを返すようになっています。個別のエラーをコード側から扱いたい場合は errors.Asinterface { Unwrap() []error } などで掘る必要があって少し面倒ですが、Go 1.20時点で複数のエラーを扱う場合の標準的な方法なのでそれに合わせています。

国際化対応

go-validator では golang.org/x/text/message でエラーメッセージの飜訳をしています。

本当は、ライブラリ側では素朴にエラーを返して、使う側で飜訳してもらう方が好ましいとは思うのですが、バリデータでは

  • 複数のエラーを返す場合がある
  • 構造体やスライスをバリデーションする場合は階層構造になることがある

などがあり、使う側でエラーを飜訳してもらうのも難しそうだったのでライブラリ側で対応しました。言語を切り替えたい場合は context.Context に言語タグを設定しておくと切り替わりますし、ライブラリ側の飜訳では不十分なら独自のカタログを用意する方法もあります。

難しかったところ

メソッドで型パラメータを宣言したい

Go 1.20時点では、メソッドで新しく型パラメータを宣言できません。

// これはできない
func (p *userType) Method[T any](v T)

何度か欲しくなったけれどできないので、別のgenericな型を用意して対応しました。

type Type[T any] struct{...}

// これはできる
func (p *Type[T]) Method(v T)

具体的な型引数を省略できない場合がある

複数の値のうちどれかを選択する validator.In バリデータや、値の範囲が一定以上になっていることを検査する validator.Min バリデータなどは、引数から型推論できるため型引数を省略できますが、値が必須であることの検査をする validator.Required バリデータなどは推論するための型がないので型引数を省略できません。

今の時点では仕方がないので、これは諦めました。今後リリースされるGoのバージョンでは、代入先の型から推論したり、デフォルトの型引数が定義できたりなど拡張されていくようなので期待しています。

lenの引数にできる型の制約を書けない

簡単にできそうだけど意外と困難です*1

len() できる型は

  • string
  • []T
  • map[K]V
  • chan T

などありますが、string には型パラメータが不要だけどスライスには1つ必要、マップはキーと値で2つ必要です。型パラメータを3つ持たせて必要なところだけ使うようにすればできなくはないですが、その実現方法はどうなんだ?とは思います。

基底型がstringまたはStringメソッドを実装している型の制約が書けない

String() string を実装していれば文字列のように扱えると便利じゃないかなと思って、

type stringable interface {
    ~string | String() string
}

としてみたところエラーになりました。

type stringable interface {
    ~string
    String() string
}

これはエラーにはならないけれど、ANDとして扱われるようでうまく実現できませんでした。

参考

Goの型パラメータについてとても詳しくまとまっている情報なのでぜひ読んでください。

*1:できなくはないけど歪なものになる、が正しい