パッケージ名 #
Package names By Sameer Ajmani
序文 #
Goのコードはパッケージの形で整理されています。同一パッケージ内では、どのような識別子(名前)も
参照することが可能ですが、そのパッケージを利用する場合は、パッケージが外部に公開している型、関数、
定数、変数しか参照できません。パッケージの参照元は常に接頭辞としてパッケージ名を付ける必要があります。
例えば、 foo.Bar
はインポートしている foo
というパッケージ内の Bar
という公開された名前を
参照しています。
良いパッケージ名はコードをより良いものにします。パッケージ名は、その中身の文脈を教えてくれ、参照元で 利用しているパッケージの目的と使用方法を理解しやすくしてくれます。またパッケージ名は、次第にそれが 大きくなるにつれて、メンテナーに何がそのパッケージに入るべきで、何が入るべきでないかを決定する手助けとなります。 良い名前を付けられたパッケージは必要なコードも探しやすくなります。
Effective Goではパッケージ、型、関数、変数の命名に関する ガイドライン を提供しています。 この文章ではそちらに書かれている内容をさらに発展させて、標準パッケージ内で見られる 名前について調べてみようと思います。また悪いパッケージ名についても議論し、その修正方法についても 考えてみましょう。
パッケージ名 #
良いパッケージ名は短くて明確です。小文字で、アンダースコア( under_score
)がなく、
大文字小文字が混ざったもの( mixedCaps
)でもありません。簡潔な名詞であることが多いです。
たとえば次のようなものが挙げられます。
time
(時間や時刻を計測したり表示する機能を提供している)list
(双方向リストを実装している)http
(HTTPクライアントとサーバの実装を提供している)
他の言語で典型的な命名規則はGoのプログラムではおそらく自然なものではないでしょう。 ここに、他言語では良いスタイルとされているけれどGoでは不適切なパッケージ名の2つ例を挙げてみます。
computeServiceClient
priority_queue
Goのパッケージではいくつかの型や関数を公開する方法をとっています。たとえば compute
パッケージでは
Client
型とサービスを使うためにメソッドを公開したり、複数のクライアント間での処理を分割するために
関数を公開することもできるでしょう。
短縮形は慎重に使うこと。 パッケージ名ではプログラマによく知られた短縮形であれば使ってもよいでしょう。 広く使われるパッケージ名はしばしば短縮形の名前になっています。
strconv
(string conversion、文字列の変換)syscall
(system call、システムコール)fmt
(formmated I/O)
一方で、パッケージ名を短くしたせいでそのパッケージが曖昧であったり不明瞭になるようであれば、短縮をやめましょう。
ユーザから良い名前をとってしまわないようにしましょう。 クライアント側のコード内でよく使われる名前をパッケージ名に
するのはやめましょう。たとえば、バッファ済みI/Oのパッケージは bufio
であり、 buf
ではありません。
これは buf
がバッファ用の変数名として都合が良いものだからです。
パッケージ内の要素の命名 #
クライアント側ではパッケージ名とそのパッケージ内の要素の名前を一緒に使うので、両者は対をなすものです。 パッケージを設計する際には、クライアントで利用されるときのことを考えましょう。
どもった名前を避けましょう。 クライアント側ではパッケージの要素を参照する際にパッケージ名を接頭辞として使うので、
そのような要素ではパッケージ名を繰り返さないようにしましょう。 http
パッケージで提供されるHTTPサーバは Server
であり、
HTTPServer
ではありません。クライアント側ではこの要素を http.Server
という形で参照するので、曖昧さはありません。
関数名を簡潔にしましょう。 pkgパッケージの関数が pkg.Pkg
型(あるいは *pkg.Pkg
型)の値を返すときは、
関数名では混乱を避けるために型名を省略することがしばしばあります。
start := time.Now() // start は time.Time 型
t, err := time.Parse(time.Kitchen, "6:06PM") // t は time.Time 型
pkg
パッケージの New
という関数は pkg.Pkg
型の値を返します。この関数はクライアント側でその型を使うときの
標準的なエントリーポイントです。
q := list.New() // q は *list.List 型
関数が pkg.T
型の値を返し、 T
が pkg
出ない場合、関数名にはクライアント側でわかりやすいように T
が入るでしょう。
よくある状況はパッケージ内に複数の New 系の関数がある場合です。
d, err := time.ParseDuration("10s") // d は time.Duration 型
elapsed := time.Since(start) // elapsed は time.Duration 型
ticker := time.NewTicker(d) // ticker は *time.Ticker 型
timer := time.NewTimer(d) // timer は *time.Timer 型
クライアント側ではパッケージ名で区別できるため、異なるパッケージで同名の型を持つことが可能です。
たとえば、標準ライブラリには Reader
という名前の型がいくつかあります。 jpeg.Reader
、
bufio.Reader
、 csv.Reader
などがそれにあたります。パッケージ名 が Reader
という名前と
うまく合うようにすると、良い型名を作ることができます。
もしパッケージの中身をよく表している接頭辞になるようなパッケージ名が思い浮かばない場合は、 おそらくパッケージの抽象化が間違っているでしょう。クライアント側でそう使われるべきコードを書き、 書いたコードがみすぼらしい場合はパッケージを再構築しましょう。こういった姿勢でいれば、クライアント側で わかりやすいパッケージ名を定義することができ、メンテナンスもしやすいパッケージを作れます。
パッケージのパス #
Goのパッケージには名前とパスがあります。パッケージ名はソースファイルの中のpackage宣言で確認できます。 クライアントコードではパッケージ名を公開された名前として接頭辞に使います。クライアントコードでは パッケージをインポートするときにパッケージのパスを使います。慣例的に、パッケージのパスの最後の要素は パッケージ名になっています。
import (
"fmt" // fmt パッケージ
"os/exec" // exec パッケージ
"golang.org/x/net/context" // context パッケージ
)
ビルドツールはパッケージ名をディレクトリと対応させます。goツールは環境変数 GOPATH を使って、 “github.com/user/hello
”
というパスのファイルを $GOPATH/src/github.com/user/hello
というディレクトリから探します。
(もちろん、この動作はよく知られていることだとは思いますが、専門用語とパッケージの構造に関して明確にしておくことは大事です。)
ディレクトリ 標準ライブラリでは、関連したプロトコルやアルゴリズムを使っているパッケージをまとめるために
crypto
、 cnotainer
、 encoding
、 image
というようにディレクトリを使っています。
こういったディレクトリ内のパッケージ間は関連はありません。ディレクトリは単純にファイルを整理しているだけです。
循環参照をしていない限り、どんなパッケージでもインポートすることが可能です。
異なるパッケージで曖昧さなしに同名の型を使えるように、異なるディレクトリで同名のパッケージを作ることができます。
たとえば runtime/pprof はプロファイリングツール pprof で
期待される書式のプロファイリングデータを生成し、一方で net/http/pprof はこの書式でプロファイリングデータを
表示するHTTPエンドポイントを提供します。クライアントコードではそれらのパッケージをインポートする際にはパッケージのパスを利用するので、
混乱することはありません。もしクライアントのコードが今挙げた両方の pprof
パッケージを利用する必要がある場合には、片方あるいは両方のパッケージを
リネーム することができます。パッケージをリネームする際には、
ローカルの名前はパッケージ名のためのガイドラインに従いましょう。(すべて小文字で under_score
や mixedCaps
がないもの)
悪いパッケージ名 #
パッケージ名が悪いとコードが読みづらく、メンテナンスが難しくなります。いつくか悪いパッケージ名を認識し修正するためのガイドラインを挙げてみます。
意味のないパッケージ名を避ける util
、 common
、 misc
という名前では、クライアント側でそのパッケージが何のためのものなのかまったくわかりません。
こういったパッケージ名では、クライアント側でどのようにそのパッケージを利用するかがわかりませんし、メンテナーがパッケージのメンテナンスに集中
することが難しくなります。やがて、こういったパッケージは依存関係が大きくなり、ビルド時間が深刻にそして不必要に遅くなります。特に、大きな
プログラムでこの傾向があります。またこのようなパッケージ名は一般的な名前なので、クライアント側でインポートされている他のパッケージと
衝突しやすく、区別するためにクライアント側に新しい名前を考えさせることを強制します。
一般的なパッケージに分割する このようなパッケージを修正するために、共通の名前の要素を持った型と関数を探し そのような型や関数を独自のパッケージにまとめましょう。たとえば、次のようなコードがあったとします。
package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}
クライアント側のコードは次のようになっているとします。
set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))
これらの関数を util
パッケージから取り出して、その内容に合う名前の新しいパッケージに移します。
package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}
するとクライアント側のコードはこうなります。
set := stringset.New("c", "a", "b")
fmt.Println(stringset.Sort(set))
一度このような変更をしてしまえば、新しいパッケージにどのような変更を加えればよいかわかりやすくなるでしょう。
package stringset
type Set map[string]bool
func New(...string) Set {...}
func (s Set) Sort() []string {...}
このように変更することでクライアント側のコードはずっと簡潔になります。
set := stringset.New("c", "a", "b")
fmt.Println(set.Sort())
パッケージ名はパッケージの設計において最重要項目です。プロジェクト内の無意味なパッケージ名は排除しましょう。
全APIを単一パッケージにしない 多くの善意なプログラマは、コードベースの中でエントリーポイントを見つけやすいように
公開するすべてのインターフェースを api
、 types
、 interfaces
といった名前の単一のパッケージに
まとめています。これは間違いです。そのようなパッケージは util
や common
というパッケージ名で
起きた問題と同じ問題に悩むことになります。つまり境界がどんどん大きくなり、ユーザにわかりにくく、
依存が強くなり、他のインポートしたパッケージと名前がぶつかるといった問題です。
ディレクトリを使って実装から公開するパッケージを切り離す形で分割しましょう。
不必要なパッケージ名の衝突を避ける 異なるディレクトリ内では同名のパッケージを置くことができるとはいっても、
パッケージが一緒に使われることはよくあるので、パッケージには個別の名前をつけるべきです。そうすることで、
混乱を減らし、クライアント側のコードでパッケージ名のリネームをする必要が少なくなります。
同様の理由で、よく知られた io
や http
といった標準パッケージと同名のパッケージ名も避けましょう。
結論 #
パッケージ名はGoのプログラムを書く上で良い命名の中心となるものです。時間をかけて良いパッケージ名を選び、 コードをきれいに整頓しましょう。そうすることであなたのパッケージは使う側が理解しやすく使いやすいものとなり、 またメンテナーが優雅に開発を続けられるプロジェクトとなることでしょう。
あわせて読みたい #
- Effective Go
- How to Write Go Code
- Organizing Go Gode (2012年のブログポスト)
- Organizing Go Code (2014年のGoogle I/Oでのプレゼン)
By Sameer Ajmani