📦

Golangのパッケージ名はどうするのが正解か

2021/12/06に公開
2

Golangのパッケージ名はどうするのが正解か

この記事は2021年Goアドベントカレンダー17日目の記事です。

この記事は主に以下の記事内容をベースにまとめたものです。

https://go.dev/blog/package-names
https://golang.org/doc/effective_go#names

日本語で原文を読みたい方はスクラップに翻訳等まとめていますので参考にしてください。(2021/11)

https://zenn.dev/_kazuya/scraps/fdc65096b0d1d7

目次

  • 基本原則
  • 良いパッケージ名
  • 悪いパッケージ名
  • 省略
  • 抽象化境界
  • -(ハイフン)
  • プロジェクトでの定義例
  • まとめ

基本原則

  • Go のコードはパッケージにまとめられる
  • パッケージ内では、コードで定義された任意の識別子 (名前) を参照できる
  • パッケージのクライアントは、パッケージのエクスポートされた型、関数、定数、および変数のみを参照できる

  • 参照には、常にパッケージ名がプレフィックス: foo.Bar として含まれる
  • Bar は、foo という名前のインポートされたパッケージ内のエクスポートされた名前 Bar を参照する
  • 外部パッケージの名前の一文字目が大文字かどうかで可視性が決定される

  • パッケージ名の衝突(コリジョン)を事前に考慮する必要は無い
  • パッケージ名が全てのソースコードを跨ぐ際、ユニークである必要は無い

※ go playground でのサンプル(ただしこれは良い例というわけではない点に注意)
https://go.dev/play/p/dtkPvbqZOoR


  • パッケージ名がソースディレクトリのベース名である(下図は一例です)

import "project/foo"

func sample() {
    foo.Bar()
}

※ projectはモジュール名の一例

※ go playground でのサンプル
https://go.dev/play/p/qKchzLiuO-u

良いパッケージ名

まず、良いパッケージ名は

  • 短く
  • 簡潔
  • 明快

もう少し具体的に挙げると

  • 小文字
  • 1単語

ではなぜ短く簡潔に明快にしなければならないかというと、
それは、パッケージを使う人々が使う度にパッケージ名を打ち込んでいるということを考えなければならないからです。

つまり、長く、不明瞭で、難解な場合、それだけでそのパッケージを使う人が時間を浪費して不幸になるからですね。

利用者の視点に立ってパッケージ名を決めるというのは一見当たり前のことのように思いますが、プログラムを書いていると案外自分本位になっていて、なかなか意識しなければ難しいと思います。
パッケージ名なんかに時間を割く意味はないという方もいるかもしれませんが、

Names are as important in Go as in any other language. They even have semantic effect: the visibility of a name outside a package is determined by whether its first character is upper case. It's therefore worth spending a little time talking about naming conventions in Go programs.

とあるように、ネーミング(いわゆる命名規則)について多少の時間をとって考え議論することは価値のあることだと effective go に記述があります。

パッケージ名を決める上でどんな名称でクライアント側にimportさせたいかを意識する

ということがパッケージ名を考察する上で大切だと思います。

悪いパッケージ名

  • 小文字や大文字の混在
  • 複数単語
  • アンダースコアの使用
  • 繰り返し

良いパッケージ名は短くて明確です。「under_scores」(アンダースコア 👉 _ ) または 「mixedCaps」(意味区切りで大文字) は良くないです。

  • Goでは適切でない名前例
computeServiceClient
priority_queue

このような名前はクライアント側からみると、長く、不明瞭で、難解であることが実感できると思います。

また、繰り返しは避けなければならないです。
クライアントコードでは、パッケージの内容を参照するときにパッケージ名をプレフィックスとして使用するため、パッケージ名を繰り返す必要はありません。

api.Api (apiという名前を繰り返すよくない例)

また、意味のないパッケージ名は避けてください。

例えば

  • util
  • common
  • misc

などの名前のパッケージは、パッケージの内容が曖昧すぎるので、認識されないということです。

省略

パッケージ名は、その省略形がなじみのあるものであれば省略することができます。
また、広く使用されているパッケージには、圧縮された名前が付いていることがよくあります。

以下にサンプルをあげておきます。

strconv (文字列変換)
syscall (システムコール)
fmt (フォーマットされたI/O)

異なるパッケージ内の型には同じ名前を付けることができます、

つまり、
クライアントの観点からは、このような名前はパッケージ名によって区別されるため問題ないということです。
たとえば、標準ライブラリには、jpegなど、Readerという名前のいくつかの型が含まれている場合は次にように定義できます。

jpeg.Reader
bufio.Reader
csv.Reader

ここで、各パッケージ名は、適切な型名を生成するために Reader という名前を適用していることがわかります。

※ go playground でのサンプル(分かりやすいよう標準ライブラリと同名で定義している)
https://go.dev/play/p/UsfLZRdn-Vi

もう少し具体的な例を出すと、

  • OpenAPIなどから自動生成されたGoのSchema
  • entで自動生成されたGoのSchema

これらに関して同じ型名が自動生成されてしまうケースがあると思いますが、
同様に全く問題ないということが分かると思います。

openapi.GetStaffs
ent.GetStaffs

ここで、自動生成された GetStaffs という型名が重複しますが、パッケージ名によって区別されるため問題ありません。

※ go playground でのサンプル
https://go.dev/play/p/FlVyYNtbT8a

抽象化境界

抽象化境界の観点においても、やはり意味のないパッケージ名は避けなければなりません。
util、common、またはmiscという名前のパッケージは、そもそもパッケージの内容を認識すらできないということです。

また、すべてのAPI等に単一のパッケージを使用するのは、一見問題ないように思うかもしれませんが、このことについても言及があります。

プログラマは、プログラムによって公開されているすべてのインタフェースを、

  • api
  • types
  • interfaces

というような名前の単一のパッケージに入れ、コードベースへのエントリポイントを見つけやすくすると考えることがありますが、これらは間違いです。

このようなパッケージは、utilやcommonという意味のない名前のパッケージと同じ問題に悩まされ、制約を受けません。
また、ユーザーに何のガイダンスも無く、依存関係を蓄積し、他のインポートと衝突してしまうことになります。

公開パッケージと実装を分離するためにディレクトリを使用するなどして分割します。

このことから、抽象化する境界、つまり、どうパッケージに分けるかをきちんと定義してから、パッケージを切ることが重要だと分かると思います。
これを怠ることにより、クライアント側でパッケージを使用するのが難しくなり、結果的にメンテナンスが難しくなるということです。

-(ハイフン)

@Yoichiro Shimizu さんからご指摘をいただきました、ありがとうございます。
-(ハイフン)を使ってパッケージを定義できませんでした。

ハイフンはそもそも言語仕様的にpacakge名に使えないから記述が無いんだと思います。
kube-proxy など、main関数を置くディレクトリとしては利用できますが、 kube-proxy packgeは作れません。

https://go.dev/ref/spec#Package_clause

https://go.dev/play/p/xzW5k5J3Yo-

Golangで記述されている kubernetes https://github.com/kubernetes/kubernetes/tree/master/cmd
cmd ディレクトリなど見た際、パッケージを集約し、インターフェースを提供する場合に機能に基づいて「プレフィックスの境界」として-(ハイフン)を利用しているように見えるが、
このことからプレフィックスとして -(ハイフン) を利用する等は問題ないと思われます。
ちなみに、-(ハイフン)を使ってはいけない等記述は effective go や公式ブログなどには記述がないので、これはプロジェクトもしくはコーダーの各々のルールに従えば良いと思います。

プロジェクトでの定義例等

Golang

まず、Golangのソースは公開されているので、参考にしない手はないと思います、ただし、言語処理系なので、アプリケーション開発の際のパッケージ名定義の参考にできる部分は限られているという点に注意しましょう。
https://github.com/golang/go

kubernetes

Golangを利用した大規模プロジェクトといえばkubernetesが一番に挙げられるのではないでしょうか。
kubernetesのパッケージ構成やパッケージ名等は綺麗にまとまっています。
https://github.com/kubernetes/kubernetes

Standard Go Project Layout

Goアプリケーションプロジェクトの基本的なレイアウトがまとめられている、ちなみにこれは公式が提供しているものではないので注意が必要だが、かなり参考にできると思います。
https://github.com/golang-standards/project-layout

ent

WebアプリケーションなどをGoで作りたいならFacebook製のエンティティーフレームワーク ent を使ってみましょう。
色々とジェネレートされたコードなどを読んでみると、パッケージの切り方や構成などとても参考になると思います。
https://github.com/ent/ent

まとめ

  • 良いパッケージ名は短くて明確

  • 「under_scores」(アンダースコア 👉 _ ) または 「mixedCaps」(意味区切りで大文字) は使うべきでない

  • パッケージ名は、その省略形がプログラマにとってなじみのあるものであれば省略することができる

  • クライアントコードで一般的に使用される名前をパッケージに付けることは避けなければならない

  • クライアントコードでは、パッケージの内容を参照するときにパッケージ名をプレフィックスとして使用するため、パッケージ名を繰り返す必要はない

  • 異なるパッケージ内の型には同じ名前を付けることができる

  • パッケージの内容に対して意味のあるプレフィックスとなるパッケージ名を見つけることができない場合、パッケージの抽象化境界が間違っている可能性がある

  • 意味のないパッケージ名は避けなければならない

  • すべてのAPIに単一のパッケージを使用してはならない

  • 異なるディレクトリにあるパッケージには同じ名前を付けることができますが、頻繁に一緒に使用されるパッケージには異なる名前を付ける必要がある

最後にどうするのが正解か

今後新たなパッケージ名を考える際に

  • 基本原則を守れているか
  • パッケージ名を決める上でクライアント側にどんな名称でimportできるとお互いが幸せになるかを意識する
  • パッケージに切る境界をきちん定義できているか

この点に最低限気をつけていけば良いと思いました。

私はGolangを触り始めて日が浅く、まだまだ未熟なので、今後もGolangにおけるベストプラクティスを意識しながら、日々勉強していきたいと思います。
ここまでお読みいただき本当にありがとうございました。

参考文献

GitHubで編集を提案

Discussion

budougumi0617budougumi0617

https://zenn.dev/_kazuya/articles/ce19973159a028#-(ハイフン)

ちなみに、-(ハイフン)を使ってはいけない等記述は effective go や公式ブログなどには記述がないので

ハイフンはそもそも言語仕様的にpacakge名に使えないから記述が無いんだと思います。
kube-proxy など、main関数を置くディレクトリとしては利用できますが、 kube-proxy packgeは作れません。

https://go.dev/ref/spec#Package_clause

letter        = unicode_letter | "_" .
identifier = letter { letter | unicode_digit } .
PackageName    = identifier .