焼売飯店

GoとかTS、JSとか

ライブラリとして公開したGoのinterfaceを変更するのは難しいと言う話

昨日Twitterに書いた内容に、sivchariさんとhajimehoshiさんからリプライをいただいたので、備忘録的にまとめておきます。

  • 発端
  • interface型と非interface型の後方互換性について
    • interface型を公開した場合
      • 1. メソッドの追加
      • 2. メソッドのシグニチャの変更
      • 3. メソッドの削除
      • 公開されたinterface型を変更する場合の後方互換性についてのまとめ
    • 非interface型を公開した場合
      • 1. メソッドの追加
      • 2. メソッドのシグニチャの変更
      • 3. メソッドの削除
      • 公開された非interface型を変更する場合の後方互換性についてのまとめ
  • その他の後方互換性を崩さない機能拡張のパターンの紹介
    • 他から実装できないinterfaceにする
    • interfaceを合成する
    • 内部用のinterfaceを分け、構造体型を公開する
  • まとめ

発端

昨日、フューチャー技術ブログに掲載された、こちらのManoさんの記事を読みました。

future-architect.github.io

内容としてはGo 1.20で追加される見込みの context.WithCancelCausecontext.Cause に関するもので、気になったのは context.Cause 関数についての次の記述でした。

Cause()ですが、以下のように context.Context のインターフェースにCause()といった関数を追加してくれた方が利用者としては便利じゃないかと思いますよね。これはGo1互換性ポリシーに書いてあるように、パッケージエクスポートされたインターフェースに新しい関数を追加することは許可されてないということで否定されていました(そのため、context.Contextを引数にとる現在のかたちで提供されています)。

記事中で紹介されているコード例も引用させていただきます。

// 互換性をぶっ壊すAPIイメージ
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
+   Cause() error // ★後方互換性を壊すためインターフェースに新規関数の追加はできない
}

これはとても共感できる内容で、ここで自分が持った疑問は "なぜcontext.Contextは構造体型ではなくinterface型で定義されているのか?" と言うものでした。

それでは、 context.Context のような公開された型に、後方互換性を破壊することなく Cause() メソッドのような公開メソッドを追加する方法について考えてみましょう。

続きを読む

GitHub Sponsorsで支援している方を勝手に紹介する記事【2022年度版】

本記事では、2022年末時点でGitHub Sponsorsで支援している方を勝手に紹介していこうと思います!

現在支援させていただいているのはこちらの方々です。

以下、順番に紹介させていただきます。

続きを読む

Go Quiz Advent Calendar【17日目】 - メソッド値は難しかった編

こちらは Goクイズ Advent Calendar 2020 - Qiita の17日目の記事です。


問題

今回はMethod values (メソッド値) の問題です。(前回のGo Language Specification輪読会で見付けたものです)

package main

import "fmt"

type I interface {
    M() int
}

type T struct {
    a int
}

func (t T) M() int {
    return t.a
}

func main() {
    var t *T = &T{a: 1}
    f1 := t.M

    var i I
    i = t
    f2 := i.M

    t.a = 2

    fmt.Println(f1(), f2())
}

f1(), f2() の結果を問う、引っ掛け無しの問題となります。 さて、答えはどれでしょう?

  1. 1 1
  2. 1 2
  3. 2 2
続きを読む

Go Quiz Advent Calendar【10日目】 - iotaのカラクリ編

こちらは Goクイズ Advent Calendar 2020 - Qiita の10日目の記事です。


問題

今回は、皆さん大好きなiotaの問題です。

package main

import "fmt"

const (
    X       = 0
    A, B, C = iota, iota + 1, iota * 2
    D, _, E
    _, F, _
    G = iota + iota
)

func main() {
    fmt.Println(D + E + F + G)
}

さて、答えはどれでしょう?

  1. compile error
  2. 12
  3. 18
  4. 24
続きを読む

Protocol Buffersのpackageの名前解決ルールを調べた

Protocol Buffersのpackageの名前解決ルールを調べました。 このブログはproto3のドキュメントベースで書いています。

developers.google.com

protobufのpackageについて

protobufでは、messageなどの定義名が衝突することを防ぐことを目的として、名前空間を分離できる package と言う機構が提供されています。

説明を簡略化するために、本家のドキュメントを引用します。


Packages

You can add an optional package specifier to a .proto file to prevent name clashes between protocol message types.

package foo.bar;
message Open { ... }

You can then use the package specifier when defining fields of your message type:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

From: Language Guide (proto3)  |  Protocol Buffers  |  Google Developers


上記のように、importした側のprotoで、 foo.bar.Open と言った形で別packageのmessageを参照することが出来ます。

protobufの名前解決ルールについて

protobufのPackage名ベースでの名前解決は、初めに innermost scope から検索を開始する形で行われます。例としては、 foo.bar package から X messageの検索を始め、foo.bar package 内に見付からなければ、次は foo package 内に無いか探しに行くといった感じです。

https://developers.google.com/protocol-buffers/docs/proto3#packages_and_name_resolution

続きを読む