ふわっとテック日記

テック系のことをメインに書いていきます。go言語/typescript/javascriptが好きです。たまに趣味(筋トレ)の話や日常系の記事も書きたいな〜と思っています。

ふわっとアドテク〜Amazon Publisher Cloud (Beta)〜

Amazon Publisher Cloudは、Amazon DSPとパブリッシャー間でのファーストパーティーデータを利用した広告配信を可能にするアドテクサービスです。

aps.amazon.com

現在プロダクトはベータ版ですが、従来のプログラマティックキャンペーンによる広告配信に比べてリーチを3.5倍に伸ばすことができたという数値もすでに出ているようです。


配信の最適化のためにAWS Clean Roomsを内部で利用しており、

クリーンルーム内ではパブリッシャー、Amazon Ads双方がプライバシーが安全に守られた状態でファーストパーティーデータにアクセスすることができます。

パブリッシャーのファーストパーティーデータやAmazon Adsのオーディエンスに対する知見をクリーンルーム内で分析することにより、最適なリーチを実現できるそうです。




Amazonの広告ソリューションは招待制のものが多かったりで一般的に広く使用できないものが多いです。

またこのBeta版はパブリッシャーへの提供はまだ限定的なため日本で広く使えるようになるにはまだ時間がかかるかもしれませんが、

将来的にパブリッシャーにとって不可欠なソリューションになるかもしれません。

Go言語ソース覗き見〜slicesパッケージ〜

Go言語のバージョン1.21で標準ライブラリに新たに追加された、slicesパッケージのソースコードをざっと読んでみました。

slicesパッケージではスライスを操作するのに便利な様々な関数が用意されています。

この記事ではソースコードに触れつつ主要どころの関数の説明をしていきたいと思います。

github.com


ソースコードで頻出しているジェネリクスの書き方に関しては、先日の記事で説明しているので参照ください。

rrioh.hatenablog.com


Equal

func Equal[S ~[]E, E comparable](s1, s2 S) bool {
  if len(s1) != len(s2) {
    return false
  }
  for i := range s1 {
    if s1[i] != s2[i] {
      return false
    }
  }
  return true
}

引数に与えられた2つのスライスの要素が全く同じかを判定する関数です。

それぞれのスライスの要素を1から順番に比較していき、等式が成り立たなくなった時点でfalseを返す実装になっています。

引数のスライスの要素はcomparableなものに限られます。要素比較をするので当然ですね。

EqualFunc

func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool {
  if len(s1) != len(s2) {
    return false
  }
  for i, v1 := range s1 {
    v2 := s2[i]
    if !eq(v1, v2) {
      return false
    }
  }
  return true
}

Equal関数とほぼ似ていますが、比較がtrueになるかfalseになるかの判断を引数で与えた関数(eq func(E1, E2) bool)の返り値で判断するという点がミソです。

ソースコードもEqual関数とほぼ同じで、falseの判断を関数実行で行なっている点のみが異なっています。

またeq関数で返り値され返せればスライスの比較ができるので、それぞれのスライスの要素型はなんでも良いことになっています。

例えば、こんな感じの使い方ができます。

var numberMap = map[int]string{
        1: "one",
        2: "two",
        3: "three",
        4: "four",
        5: "five",
}

func main() {
        s1 := []int{1, 2, 3}

        s2 := []string{"one", "two", "three"}
        output := slices.EqualFunc(s1, s2, eq)
        fmt.Println(output) // trueになる

        s2 = []string{"one", "four", "three"}
        output = slices.EqualFunc(s1, s2, eq)
        fmt.Println(output) // falseになる
}

func eq(e1 int, e2 string) bool {
        if v, ok := numberMap[e1]; ok && v == e2 {
                return true
        }
        return false
}

Compare

func Compare[S ~[]E, E cmp.Ordered](s1, s2 S) int {
  for i, v1 := range s1 {
    if i >= len(s2) {
      return +1
    }
    v2 := s2[i]
    if c := cmp.Compare(v1, v2); c != 0 {
      return c
    }
  }
  if len(s1) < len(s2) {
    return -1
  }
  return 0
}

引数に与えられた2つのスライスの比較をします。1つ目のスライスの長さの方が長ければ1、2つ目のスライスの長さの方が長ければ-1になります。

2つの長さが等しい場合、1つ目の要素から1つずつ比較をしていき、1つ目のスライス要素 > 2つ目のスライス要素となった時点で1、逆であればとの時点で-1を返します。

最後まで要素が等しかった場合(Equal関数でtrueを返すのと等しい)、0を返します。

要素の大小比較をするので、当然引数のスライスの要素はcmp.Orderedの型に制限されます。

CompareFunc

func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
  for i, v1 := range s1 {
    if i >= len(s2) {
      return +1
    }
    v2 := s2[i]
    if c := cmp(v1, v2); c != 0 {
      return c
    }
  }
  if len(s1) < len(s2) {
    return -1
  }
  return 0
}

Compare関数とほぼ同じですが、要素の比較をcmp.Compareではなく、引数で与えた関数で行うようにしたものです。

Equal関数に対するEqualFuncと非常によく似ていますね。ソースコードもCompare関数とほとんど同じです。

Index

func Index[S ~[]E, E comparable](s S, v E) int {
  for i := range s {
    if v == s[i] {
      return i
    }
  }
  return -1
}

第一引数のスライスに対して、第二引数の値の要素のインデックス値を返す関数です。

該当する要素がなければ-1を返します。

ソースコードも非常にシンプルです。スライスの要素を順番に見ていって、第二引数と等しいかを順に確かめているだけですね。

IndexFunc

func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
  for i := range s {
    if f(s[i]) {
      return i
    }
  }
  return -1
}

Index関数と非常に似ていますが、第二引数には関数が与えられます。

第一引数のスライスの要素を走査していき、引数に入れて第二引数の関数を実行した際にtrueを返す要素のインデックスを返します。

Contains

func Contains[S ~[]E, E comparable](s S, v E) bool {
  return Index(s, v) >= 0
}

Index関数が-1以外を返せばtrue、-1を返せばfalseとなります。

つまり第二引数の要素が第一引数のスライスに存在すればtrueです。

ContainsFunc

func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
  return IndexFunc(s, f) >= 0
}

IndexFunc関数が-1以外を返せばtrue、-1を返せばfalseとなります。

第二引数の関数がtrueを返す要素が第一引数のスライスに存在すればtrueです。

Insert

func Insert[S ~[]E, E any](s S, i int, v ...E) S {
  ....
}

スライスsのインデックスiの箇所から、vで指定した値を要素に入れ込みます。

元々インデックスi以降にあった要素は、vの数だけ後ろにずれます。

ソースコードは長いので割愛しますが、

sの要素が増えた際にvのメモリアドレスと重複する際の場合分けなど、若干複雑な処理がされています。

Delete

func Delete[S ~[]E, E any](s S, i, j int) S {
  _ = s[i:j] // bounds check

  return append(s[:i], s[j:]...)
}

スライスsのインデックスが[i,j)の範囲を削除します。

_ = s[i:j]を実行することにより、sの範囲外のインデックス値を指定していないことをassertしています。

範囲外のインデックスを指定している場合はここでpanicがおきます。

DeleteFunc

func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
  i := IndexFunc(s, del)
  if i == -1 {
    return s
  }
  // Don't start copying elements until we find one to delete.
  for j := i + 1; j < len(s); j++ {
    if v := s[j]; !del(v) {
      s[i] = v
      i++
    }
  }
  return s[:i]
}

スライスsの要素の内、del関数がtrueを返す要素を削除する関数です。

まずIndexFunc関数を実行してdel関数がtrueの最初の要素のインデックス値を割り出します。

その次のインデックスの要素からsの要素を順に見ていき、del関数がfalseであれば(削除対象でなければ)、

要素を左にずらしていく(削除対象の要素を上書きする形で)、という処理がされていますね。

Replace

func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
  ....
}

スライスsのインデックスが[i, j)の箇所の要素を、vで置き換える関数です。

ソースコードは長いので割愛します。

Insert関数と同じく、スライスの要素を増やしていく過程でvとメモリアドレスが衝突する場合を分岐分けしており、比較的複雑な処理になっています。

Compact

func Compact[S ~[]E, E comparable](s S) S {
  if len(s) < 2 {
    return s
  }
  i := 1
  for k := 1; k < len(s); k++ {
    if s[k] != s[k-1] {
      if i != k {
        s[i] = s[k]
      }
      i++
    }
  }
  return s[:i]
}

スライス2で同じ値が2つ以上連続した場合は、それらを1つの要素にまとめてしまう関数です。

例えば、[]int{1,2,3,3,4,5,5,5,6}というスライスを[]int{1,2,3,4,5,6}に変換します。

ソースコードでは、スライスsの要素を順番に見ていきます。そして連続する値を検知した時、その時点でのインデックス(連続した値の中で2番目の要素位置)を変数iに保管しておきます。

そしてそれ以降の値の異なる要素を、インデックスiから詰め直します。

CompactFunc

func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
  if len(s) < 2 {
    return s
  }
  i := 1
  for k := 1; k < len(s); k++ {
    if !eq(s[k], s[k-1]) {
      if i != k {
        s[i] = s[k]
      }
      i++
    }
  }
  return s[:i]
}

Compact関数と非常によく似ていますが、第二引数の関数がtrueを返す連続した要素を1つにまとめます。

この時残される要素は、連続する中で1番最初の要素になります。

例えば下記のサンプルでは、連続して偶数が出現する場合にまとめるようにしています。

[6, 8, 10]がまとめられて[6]になります。

func main() {
        s := []int{1, 2, 3, 4, 5, 6, 8, 10, 9}

        output := slices.CompactFunc(s, eq)

        fmt.Println(output) // expect: [1, 2, 3, 4, 5, 6, 9]
}

func eq(e1, e2 int) bool {
        if e1 % 2 == 0 && e2 % 2 == 0 {
                return true
        }
        return false
}

Grow

func Grow[S ~[]E, E any](s S, n int) S {
  if n < 0 {
    panic("cannot be negative")
  }
  if n -= cap(s) - len(s); n > 0 {
    s = append(s[:cap(s)], make([]E, n)...)[:len(s)]
  }
  return s
}

スライスsのキャパシティを、残りn個の要素を追加しても拡張する(アロケートし直す)必要がない大きさにまで増やします。

Clip

func Clip[S ~[]E, E any](s S) S {
  return s[:len(s):len(s)]
}

スライスsの無駄なキャパシティを削除し、要素長と同じにします。

ここで使われているコロンを2つ使ったスライスの表現方法ですが、Go仕様にあるFull slice expressionsというものです。

コロンで区切られた値の3つ目は、キャパシティの大きさを指定しています。

go.dev

ふわっとアドテク ~Header Bidding~

広告テクノロジー業界で有名な配信手法の一つである、Header Biddingについてのふわっと解説です。

Header Biddingとは?

Header Bidding(ヘッダービディング)とは、プログラマティック広告の配信機能の仕組みの一つです。

Header Bidding登場前は、web広告の配信システムはいわゆる「ウォーターフォール形式」でした。

これは、複数のアドサーバに対して広告リクエストを送る順番があらかじめ決められているというものです。

アドサーバAへのリクエストがアドサーバBよりも先にされる設定になっていた場合、アドサーバBがより高単価の案件を持っていたとしてもアドサーバAがレスポンスした広告が表示されてしまいます。

ウォーターフォール形式での機会損失の例


こうした機会損失を防ぐために、複数のエクスチェンジャーやSSPに同時に広告リクエストを送り、勝者を決定するという仕組みがHeader Biddingです。

Header Biddingでの同時リクエストの例


サーバサイドHeader Bidding VS クライアントサイド Header Bidding

Header Biddingの実装方法としては、サーバサイド方式とクライアントサイド方式の2種類があります。

サーバサイド方式

サーバサイド方式は、パブリッシャーからアドサーバに対して単一のシンプルなリクエストを送るだけで、アドサーバが各エクスチェンジャーやSSPに対してよしなにHeader Biddingを実行して広告をレスポンスしてくれるというものです。

パブリッシャーに対して複雑な実装が不必要なため簡単に準備することができ、またHeader Biddingがサーバで行われるためクライアントサイトでのページ遅延などが発生しません。

その反面、オークションの実行がサーバ内で行われるために透明性が低いといったデメリットもあります。

サーバサイド方式のHeader Biddingサービスの有名どころとしては、APSAmazon Publisher Services)が提供するUAM、TAMなどがあります。

UAM(Unified Ad Marketplace)

APSが提供する、主に中小規模のパブリッシャー向けのウェブ特化Header Biddingサービスです。

サーバサイド方式であり、かつ各デマンドパートナーとの個別契約なども必要ないため、パブリッシャーが比較的容易に適用できるようになっています。

支払いに際してマージンが差し引かれるため、実質的にパブリッシャーにとっては有料となっています。

aps.amazon.com


TAM(Transparent Ad Marketplace)

こちらもAPSが提供するHeader Biddingサービスです。

UAMとは異なり、大規模なパブリッシャーが対象です。

各デマンドパートナーにAmazonも加わる形でオークションが展開されます。デマンドとしてのAmazonAmazon商品のリターゲティング案件や各種セールの案件などを豊富に取り揃えているため、対象ユーザや時期が合致すれば大きな収益を得られます。

ただし、他のデマンドパートナーは、パブリッシャーが独自に契約を結んでいる必要があります。

パブリッシャーに対する課金はなく、アプリでも使うことができます。

aps.amazon.com


クライアントサイド方式

一方クライアントサイド方式は、オークションをクライアントサイドで実行します。

メリットとしては、オークションの透明性がより高くなる点や、cookieを用いたターゲティングがサーバサイド方式よりも行いやすいため収益が出やすいといった点が挙げられます。

デメリットとしては、クライアントサイドでオークション処理を実装する必要があるため、準備がサーバサイド方式に比べて複雑になる点があります。

クライアントサイドのHeader Biddingのサービスとしては、Prebid.jsが有名です。

Prebid.js

Prebid.jsは特定の企業のマネージドサービスではなく、アドテク業界の有志が協力して作り上げたオープンソースのプロダクトです。

prebid.org


誰でもダウンロードし、自分のメディアに実装することでHeader Biddingを始めることができます(ただし複雑です)。

オープンソースとはいえ、Prebid.jsをダウンロードしてHeader Biddingを独自に実装するのはかなり大変です。

そのため、Prebid.jsを元に簡単にHeader Biddingを実現できるソリューションを提供している企業もあります。FLUXなどはその代表例です。

サーバサイド/クライアントサイドHeader Bidding比較(相対評価

Go言語 - ジェネリクスで型引数を分解するというテクニック

Go言語の1.21ではslicesmapsパッケージが追加され、スライスやマップに対する操作が格段にシンプルにできるようになりました。

本記事ではそんなslicesパッケージのClone関数を手本に

その実装の詳細説明と、そこから導けるジェネリクスの型制約実装におけるテクニックの解説をしていきます。


本記事は下記記事の翻訳、要約をしつつ執筆しています。

go.dev




スライスのコピーを作成するslices.Clone関数は、次のような実装になっています。

func Clone[S ~[]E, E any](s S) S {
    return append(s[:0:0], s...)
}

この関数はボディ(return append(s[:0:0], s...))のシンプルさに比べて、シグネチャClone[S ~[]E, E any](s S) S)が複雑な構成になっています。

ここでは、シグネチャがなぜこういった書き方になっているのかの解説と、そこから学べるジェネリクスの型引数定義のテクニックについて解説していきます。

単純なクローン

任意の型のスライスを引数に取り、新しいスライスを返す関数

func Clone1[E any](s []E) []E {
    // body省略
}

上記は型引数をEの1つのみ持っています。型Eのスライスであるsを引数に取り、同様の型のスライスを返します。

このシグネチャはGoのジェネリックに馴染みのある方ならシンプルで分かりやすいと思います。

しかし、 これには問題が1つあります。

名前付きのスライス型はGoではあまり使われませんが、このような感じで使うことができます

type MySlice []string

func (s MySlice) String() string {
    return strings.Join(s, "+")
}

ここで、MySliceのコピーを作成し、ソートをかけた後にString()関数の結果を返したいとします。

func PrintSorted(ms MySlice) string {
    c := Clone1(ms)
    slices.Sort(c)
    return c.String() // コンパイルエラー
}

残念ながら、このコードはこのようなエラーを吐いて失敗してしまいます。

c.String undefined (type []string has no field or method String)

Clone1関数を以下のようなシグネチャに置き換えると原因が見えやすくなります。

func Clone1(s []string) []string

Go asssignment rulesで記載されている通り、MySlice型をstring型の引数としてClone1に渡すことはできます。

go.dev

しかし、Clone1の返り値はMySlice型ではなくstringです。それゆえに、[]stringがStringメソッドを持っていないということでこのエラーが起きるわけです。

柔軟なクローン

上記のような問題を解決するためには、引数と全く同様の型を返す必要があります。

MySlice型を引数に指定したなら、MySlice型を返すようにしなければなりません。

そうするには、このようにする必要があるでしょう。

func Clone2[S ?](s S) S // このままだと動作しないので注意

このClone2関数は、引数と全く同じ型を返します。

型制約の?部分はもちろんこのままではダメで、関数の処理が書けるような制約を定義する必要があります。

Clone1ではanyを指定していましたが、ここではsがスライスになるような制約にしなければいけません。スライスの要素の型はなんでも良いです。

仮にこの要素型をEとし、Clone1のようなシグネチャにしてみましょう。

func Clone3[S []E](s S) S // まだ動作しない

これでもまだ有効ではありません。Eの定義がされていないからです。要素型はなんでも良いので、Eはanyとして定義できます。

func Clone4[S []E, E any](s S) S

最終形に近づいてきました。

このClone4をコンパイルして呼び出すと、このようなエラーが発生します。

MySlice does not satisfy []string (possibly missing ~ for []string in []string)

Eという型制約はスライス型のリテラル(string)のみ許可するため、MySliceは[]Eを満たしていないのです。

Underlying type 制約

上記エラーは~(チルダ)を型パラメータに付与することで解決できます。

func Clone5[S ~[]E, E any](s S) S

S []E, E any」として定義された制約は、名前の付いていないスライス型ならなんでも適用できます。

しかし、スライスリテラルとして定義された名前付きの型は適用できません。

チルダをつけて「S ~[]E, E any」とすることで、SはUnderlying typeがスライス型のものはなんでも適用できる、という意味になります。

MySliceのUnderlying typeは[]stringなので、この型制約に合致するということになります。


こういった理屈により、冒頭のClone関数のシグネチャは動作しています。


2022に執筆した私の記事でも、Underlying typeについて軽く解説しているのでご参照ください。

rrioh.hatenablog.com



そもそも、なぜチルダをわざわざつけないといけないのでしょうか?

Underlying typeをデフォルトで受け付けるようにしても良いんじゃないかという声も聞こえてきそうです。


これを掘り下げていくには、まず「[T ~MySlice]」のような制約がなんの意味もなさないことを確認するところから始めます。

例えばtype MySlice2 MySliceのような型を定義したとしても、MySlice2のUnderlying typeはMySliceではなく、[]stringになります。

MySliceがUnderlying typeの型は存在し得ないため、[T ~MySlice]は、一切の型を受け付けません。

これを避けるため、コンパイラはこのようなエラーを出力します。

invalid use of ~ (underlying type of MySlice is []string)

もしチルダをなくして[S []E]がUnderlying typeが[]Eのもの全てを受け付けるようになれば、[S MySlice]のような表記の意味を別に定義しなくてはいけません。

[S MySlice]を禁止したり、[S MySlice]の場合はMySliceのみ受け付ける、などといった仕様策定も考えられますが、どちらにしても事前定義された型との兼ね合いで混乱が生じます。

なぜならintのような事前定義された型は、Underlying typeが自分自身だからです。

デフォルトでUnderlying typeが全て許可されているとしたら、Underlying typeがintの制約は[T int]となるでしょう。

しかし、この場合は上述の[S MySlice]と形がほぼ同じなのに別の挙動をすることになり、混乱極まってしまいますね。

こういった事情により、Underlying typeの許可はデフォルトではなく、チルダの使用が必要なのです。

型推定

ここでは、slices.Cloneの実行が型推定によってどのように単純化されているのかをみていきます。

シグネチャ: func Clone[S ~[]E, E any](s S) S

Cloneの呼び出しで、パラメータsにスライスが渡されます。シンプルな型推定で、コンパイラは型パラメータSに当たる型引数の型が、Cloneに渡されたスライスの型であると推定します。

そして、Eの型が、Sに渡された引数の要素の型であると推定します。これはつまり、

c := Clone[MySlice, string](ms)

このような書き方をしなくても

 c := Clone(ms)

こう書くだけで型推定が働き、コードが動作するということです。


Cloneに言及するだけで実行はしない場合は、コンパイラにとって型推定に使う材料が存在しないことになります。

そのため、Sにあたる型引数を指定する必要があります。

この場合でも、EはSから推定されるため、指定する必要はありません。つまり、

myClone := Clone[MySlice, string]

こう書く必要はなく、

myClone := Clone[MySlice]

これで良いということです。

型引数の分解

上記で見てきたテクニックは、型引数(S)を別の型引数(E)を使って定義するというものであり、ジェネリック関数のシグネチャ内で型を分解する方法の1つです。

型を分解することによって、型の命名や制約が余すところなくできます。

例えばこれはmaps.Cloneシグネチャです。

func Clone[M ~map[K]V, K comparable, V any](m M) M

slices.Cloneと同様に、引数mの型に型引数を使用しています。そしてそれをK、Vという別の型引数に分解しています。


func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)

この関数では、引数がスライスである必要があり、その要素はStringメソッドを持つ必要があります。




ジェネリクスの型制約は、少し複雑な定義をしようとするとたちまち複雑化してしまったりします。

型引数を別の型引数に分解するという手法は、そんな複雑な型制約をシンプルにしてくれるとても優れたテクニックだと思います。

Goのコーディングの際にはぜひ頭の片隅に入れておきたい技ですね。

Go言語のプロジェクト構成 - 公式のススメ

有名なGoのプロジェクト構成のパターンとしては、以下のgolang-standardsが有名です。

github.com

このパターンは賛否両論ありつつも長い間多くのプロジェクトで採用されてきたのですが、Go公式のものではありません。

そして Goの公式はこれまで、プロジェクト構成に関するドキュメントを公開してきませんでした。


しかしここにきて、公式ドキュメントが公開されました。 それがこれです。

go.dev


本記事ではこの公式ドキュメントが勧めるプロジェクト構成を、和訳しつつかいつまんで解説します。


単一のパッケージ

全てのコードをプロジェクトのルートディレクトリに収めます。

project-root-directory/
  go.mod
  modname.go
  modname_test.go

このパッケージがGitHubgithub.com/someuser/modnameという形でアップロードされた場合、go.modファイルに表記されるモジュール名はgithub.com/someuser/modnameになります。

modname.goはパッケージ名を以下のように宣言します。

package modname

// ... package code here

このパッケージをimportする時は以下のような感じになります。

import "github.com/someuser/modname"

以下のようにコードを複数のファイルに分割することもできます。この場合も、パッケージ宣言は全ファイルでpackage modnameとなります。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth.go
  auth_test.go
  hash.go
  hash_test.go

単一のコマンド

実行可能プログラム(コマンドラインツール)は、main関数が定義されたファイルを中心に構成されます。

コード規模が大きい場合はファイルを分割しても良いですが、全てのファイルでpackage mainのパッケージ宣言が必要です。

project-root-directory/
  go.mod
  auth.go
  auth_test.go
  client.go
  main.go

上記の例ではmain.goがmain関数を持っている想定ですが、これはGo言語の慣習によるものです。main関数を持つファイル名は別名(modname.goなど)でもかまいません。

GitHub github.com/someuser/modnameリポジトリにアップロードされた場合、go.modファイルのモジュール名は以下のようになります。

module github.com/someuser/modname

このモジュールのユーザは以下のようにしてコマンドをインストールします。

$ go install github.com/someuser/modname@latest

内部にサブパッケージを持つパッケージ/コマンド

規模の大きなパッケージやコマンドは、内部的に使うコードを複数のパッケージに分割することがあります。

こういった内部利用のためのパッケージは、internalディレクトリの配下に置くことが推奨されています。

internal配下のパッケージは、モジュールの外部からimportすることができなくなります。

internal配下はモジュール外部に影響しないことが確約されているため、リファクタリングなどの作業がスムーズになります。

(下記に記載されている通り、internal配下のパッケージは、internalの親ディレクトリ配下のコードからのみimportできます。

pkg.go.dev

この場合のプロジェクト構成は下記のようになります。

project-root-directory/
  internal/
    auth/
      auth.go
      auth_test.go
    hash/
      hash.go
      hash_test.go
  go.mod
  modname.go
  modname_test.go

上記の場合、modname.goからauthパッケージをimportすることはできますが、project-root-directory外部のパッケージからimportすることはできません。

import "github.com/someuser/modname/internal/auth" // 内部からのみ可能

複数のパッケージ

モジュールが複数のimport可能なパッケージで構成されていることもあります。以下のような構成の時です。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
    token/
      token.go
      token_test.go
  hash/
    hash.go
  internal/
    trace/
      trace.go

modnameパッケージは通常通り、以下のようにimportできます。

import "github.com/someuser/modname"

同梱されているサブパッケージは、以下のようにimportできます。

import "github.com/someuser/modname/auth"
import "github.com/someuser/modname/auth/token"
import "github.com/someuser/modname/hash"

internalディレクトリ以下はimportできません。internal配下に隠蔽できるコードは、できるだけ入れてしまうことが推奨されています。

複数のコマンド

次は、モジュールが複数のimport可能なコマンドで構成されているパターンです。

この場合も、コマンドごとに別のディレクトリで構成されます。

project-root-directory/
  go.mod
  internal/
    ... shared internal packages
  prog1/
    main.go
  prog2/
    main.go

それぞれのディレクトリ内で、goファイルはpackage mainを宣言しています。

internalは例によって、コマンド横断で共通して使うパッケージを格納します。

コマンドのインストールは以下のようになります。

$ go install github.com/someuser/modname/prog1@latest
$ go install github.com/someuser/modname/prog2@latest

よくある慣習では、全てのコマンドをcmdディレクトリに入れてしまいます。

コマンドだけで構成されているリポジトリの場合、この慣習には必ずしも従う必要はないです。

しかし、コマンドとimport可能パッケージが混在するリポジトリの場合では、cmdディレクトリの使用がより推奨されます(次項参照)。

リポジトリにパッケージとコマンドが混在する場合

単一リポジトリで、パッケージとコマンドを両方提供しているというケースは時々あります。

そのような時のプロジェクト構成は以下のようになります。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
  internal/
    ... internal packages
  cmd/
    prog1/
      main.go
    prog2/
      main.go

このモジュールがgithub.com/someuser/modnameの場合、以下のようにパッケージをimportできます。

import "github.com/someuser/modname"
import "github.com/someuser/modname/auth"

また以下のようにコマンドのインストールもできます。

$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest

サーバプロジェクト

サーバプロジェクトは通常export用のパッケージは持ちません。そのため、サーバのロジックを実装したパッケージはinternalディレクトリ配下に置くのが望ましいです。

またプロジェクトはGo言語ではないファイルで構成されたディレクトリも多く持ちうるため、Goのコマンドは全てcmdディレクトリ内に置くのが良いでしょう。

project-root-directory/
  go.mod
  internal/
    auth/
      ...
    metrics/
      ...
    model/
      ...
  cmd/
    api-server/
      main.go
    metrics-analyzer/
      main.go
    ...
  ... the project's other directories with non-Go code

サーバのリポジトリ内で他プロジェクトにも流用できそうなパッケージを作成した場合は、モジュールごと分けてしまうのがベストです。




以上、Goらしく非常にシンプルなドキュメントになっています。


今までの実質的なスタンダードだったgolang-standardsでもcmdinternalの使用は推奨していますし、そこまで劇的に異なっているというわけでもなさそうです。

ただ公式がドキュメントを公開したということは、より不動の立ち位置をもつスタンダードが確立したという意味で大きな意義がありそうですね。


Go初学者にもまずこの公式ドキュメントを理解してもらった上で、場合によってはgolang-standardsの方も理解してもらうといった流れもできそうで

公式の見解が示されるというのは学習の指針も明確になりますし皆にとって良いことなのではないかと思います。

全てにおいてYAMLはXMLに劣っている...らしい

色々と議論を呼びそうなタイトルですが、とあるポッドキャストの会話の一部を文字起こしした記事の内容です。

原文はこちら

changelog.com


スピーカーの体験を元にした話によると、

XMLは場合によって使いづらいこともあるし、適していることもある。

一方、YAMLが優れているシチュエーションは存在しない(常に他のより良い代替手段がある)。

ということらしいです。


Go言語のテストをYAMLで実施する際に、YAMLの表記のGo 1.20の1.20の部分が1.2と解釈され、テストが通らなくなったという話を冗談半分でしています。

これは確かに厄介そうな機能ですね。

他にもYAMLはバージョンによる違いが色々と事故の元になったり、余計な仕様が多かったりして煙たがられることが多いみたいですね。


一方、XMLは他フォーマットよりも記述量が多いなどの理由で嫌っている人が多い印象です。

ただその拡張性の高さや情報のわかりやすさは一目に値すると個人的には思っています。


YAMLはそもそもXMLコミュニティから生まれ出たものなんですね。

www.xmlhack.com

結果的にXML vs YAMLの論争に発展するとはなんとも皮肉な感じで面白いです。

Gopls v0.12

Go言語のLanguage ServerであるGoplsが今夏アップグレードされ、v0.12がリリースされました。

go.dev



前バージョンに比較してメモリ効率がレベルアップされています。


またファイルベースでのキャッシュで各パッケージの情報を保持するのですが、プロセスを横断してキャッシュが持続するため

goplsを再起動した場合でも動作が速いままだったり、複数のgoplsインスタンスを起動した際のパフォーマンスのシナジーが大きくなったりするようです。


また今まではインメモリのパッケージに対してしか静的分析ができなかったのに対し、v0.12ではこのメモリ制限が撤廃されたことによって

パッケージ間の依存関係の分析が正確にできるようになりました。

これにより、Go標準のパッケージの関数を自前でラップした、というような場合でも、そのフォーマットの間違いを捕捉できるようになったりするそうです。