The Go Blog

パッケージ名

Sameer Ajmani
2015年2月4日

はじめに

Goのコードはパッケージに整理されています。パッケージ内では、コードはその中で定義されている任意の識別子(名前)を参照できますが、パッケージのクライアントは、パッケージのエクスポートされた型、関数、定数、変数のみを参照できます。そのような参照には、常にパッケージ名がプレフィックスとして含まれます。foo.Barは、インポートされたfooという名前のパッケージ内のエクスポートされた名前Barを参照します。

良いパッケージ名は、コードをより良くします。パッケージの名前は、その内容のコンテキストを提供し、クライアントがパッケージの目的と使用方法を理解しやすくします。この名前は、パッケージのメンテナが、パッケージが進化するにつれて何がパッケージに属し、何が属さないかを判断するのにも役立ちます。適切に命名されたパッケージは、必要なコードを見つけやすくします。

Effective Goでは、パッケージ、型、関数、変数の命名に関するガイドラインが提供されています。この記事では、その議論をさらに深め、標準ライブラリで見られる名前を調査します。また、悪いパッケージ名とそれらを修正する方法についても議論します。

パッケージ名

良いパッケージ名は短く、明確です。これらは小文字で、under_scoresmixedCapsはありません。多くの場合、次のような単純な名詞です。

  • time(時間の測定と表示の機能を提供します)
  • list(双方向連結リストを実装します)
  • http(HTTPクライアントおよびサーバーの実装を提供します)

他の言語で典型的な名前のスタイルは、Goプログラムでは慣用的ではないかもしれません。以下に、他の言語では良いスタイルかもしれませんが、Goにはあまり適さない名前の例を2つ挙げます。

  • computeServiceClient
  • priority_queue

Goパッケージは、いくつかの型と関数をエクスポートできます。たとえば、computeパッケージは、サービスを使用するためのメソッドを持つClient型と、いくつかのクライアント間で計算タスクを分割するための関数をエクスポートできます。

慎重に省略する。パッケージ名は、その省略形がプログラマーにとって馴染みのあるものである場合に省略できます。広く使用されているパッケージは、しばしば圧縮された名前を持ちます。

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

一方、パッケージ名を省略すると曖昧になったり不明瞭になったりする場合は、そうすべきではありません。

ユーザーから良い名前を奪わない。クライアントコードで一般的に使用される名前をパッケージに付けるのは避けてください。たとえば、バッファリングされたI/Oパッケージはbufではなくbufioと呼ばれます。これは、bufがバッファの良い変数名だからです。

パッケージの内容の命名

パッケージ名とその内容の名前は、クライアントコードがそれらを一緒に使用するため、結合されています。パッケージを設計する際には、クライアントの視点に立ってください。

繰り返しを避ける。クライアントコードはパッケージの内容を参照するときにパッケージ名をプレフィックスとして使用するため、それらの内容の名前はパッケージ名を繰り返す必要はありません。httpパッケージが提供するHTTPサーバーはServerと呼ばれ、HTTPServerではありません。クライアントコードはこの型をhttp.Serverと参照するため、曖昧さはありません。

関数名を簡素化する。パッケージpkg内の関数が型pkg.Pkg(または*pkg.Pkg)の値を返す場合、関数名は混乱なく型名を省略できることがよくあります。

start := time.Now()                                  // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM")         // t is a time.Time
ctx = context.WithTimeout(ctx, 10*time.Millisecond)  // ctx is a context.Context
ip, ok := userip.FromContext(ctx)                    // ip is a net.IP

パッケージpkg内のNewという名前の関数は、型pkg.Pkgの値を返します。これは、その型を使用するクライアントコードの標準的なエントリーポイントです。

 q := list.New()  // q is a *list.List

関数が型pkg.Tの値を返す場合(ここでTPkgではない)、クライアントコードを理解しやすくするために、関数名にTを含めることができます。一般的な状況は、複数のNewのような関数を持つパッケージです。

d, err := time.ParseDuration("10s")  // d is a time.Duration
elapsed := time.Since(start)         // elapsed is a time.Duration
ticker := time.NewTicker(d)          // ticker is a *time.Ticker
timer := time.NewTimer(d)            // timer is a *time.Timer

異なるパッケージの型は、クライアントの視点から見るとパッケージ名によって区別されるため、同じ名前を持つことができます。たとえば、標準ライブラリには、jpeg.Readerbufio.Readercsv.Readerなど、いくつかのReaderという名前の型が含まれています。各パッケージ名はReaderと組み合わせて良い型名になります。

パッケージの内容の意味のあるプレフィックスとなるパッケージ名を考案できない場合、パッケージの抽象化境界が間違っている可能性があります。クライアントがパッケージを使用するようにコードを書き、結果が悪いと思われる場合はパッケージを再構築してください。このアプローチにより、クライアントが理解しやすく、パッケージ開発者が保守しやすいパッケージが得られます。

パッケージパス

Goパッケージには、名前とパスの両方があります。パッケージ名は、そのソースファイルのパッケージステートメントで指定されます。クライアントコードは、パッケージのエクスポートされた名前のプレフィックスとしてそれを使用します。クライアントコードは、パッケージをインポートするときにパッケージパスを使用します。慣例により、パッケージパスの最後の要素はパッケージ名です。

import (
    "context"                // package context
    "fmt"                    // package fmt
    "golang.org/x/time/rate" // package rate
    "os/exec"                // package exec
)

ビルドツールはパッケージパスをディレクトリにマッピングします。goツールは、ディレクトリ$GOPATH/src/github.com/user/helloにあるパス"github.com/user/hello"のソースファイルを見つけるためにGOPATH環境変数を使用します。(もちろん、この状況はよく知られているはずですが、パッケージの用語と構造を明確にすることが重要です)。

ディレクトリ。標準ライブラリは、cryptocontainerencodingimageなどのディレクトリを使用して、関連するプロトコルやアルゴリズムのパッケージをグループ化しています。これらのディレクトリ内のパッケージ間には実際には関係はありません。ディレクトリはファイルを整理する方法を提供するだけです。インポートがサイクルを作成しない限り、どのパッケージも他のどのパッケージもインポートできます。

異なるパッケージの型が曖昧さなく同じ名前を持つことができるのと同様に、異なるディレクトリのパッケージも同じ名前を持つことができます。たとえば、runtime/pprofpprofプロファイリングツールが期待する形式でプロファイリングデータを提供し、net/http/pprofはこの形式でプロファイリングデータを提示するためのHTTPエンドポイントを提供します。クライアントコードはパッケージをインポートするためにパッケージパスを使用するため、混乱はありません。ソースファイルが両方のpprofパッケージをインポートする必要がある場合、どちらかまたは両方をローカルで名前変更できます。インポートされたパッケージを名前変更する場合、ローカル名はパッケージ名と同じガイドライン(小文字、under_scoresmixedCapsなし)に従うべきです。

悪いパッケージ名

悪いパッケージ名は、コードのナビゲートと保守を困難にします。悪い名前を認識し、修正するためのいくつかのガイドラインを以下に示します。

意味のないパッケージ名を避ける。utilcommonmiscという名前のパッケージは、クライアントにパッケージの内容についての感覚を与えません。これにより、クライアントがパッケージを使用することが難しくなり、メンテナがパッケージを集中させることが難しくなります。時間が経つにつれて、特に大規模なプログラムでは、コンパイルを著しく不必要に遅くする可能性のある依存関係が蓄積されます。そして、そのようなパッケージ名は一般的であるため、クライアントコードによってインポートされた他のパッケージと衝突する可能性が高く、クライアントはそれらを区別するために名前を考案せざるを得なくなります。

汎用パッケージを分割する。そのようなパッケージを修正するには、共通の名前要素を持つ型や関数を探し、それらを独自のパッケージに引き出します。たとえば、次のようなものがある場合:

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に単一のパッケージを使用しない。多くの善意のプログラマーは、プログラムが公開するすべてのインターフェースを、apitypesinterfacesという名前の単一のパッケージに入れ、コードベースへのエントリーポイントを見つけやすくすると考えています。これは間違いです。そのようなパッケージは、utilcommonという名前のパッケージと同じ問題を抱えており、際限なく肥大化し、ユーザーにガイダンスを提供せず、依存関係が蓄積し、他のインポートと衝突します。それらを分割し、おそらくディレクトリを使用して公開パッケージと実装を分離してください。

不必要なパッケージ名の衝突を避ける。異なるディレクトリにあるパッケージは同じ名前を持つことができますが、頻繁に一緒に使用されるパッケージは異なる名前を持つべきです。これにより、混乱とクライアントコードでのローカルな名前変更の必要性が減ります。同じ理由で、iohttpのような人気のある標準パッケージと同じ名前を使用することは避けてください。

まとめ

Goプログラムでは、パッケージ名が良い命名の中心です。良いパッケージ名を選択し、コードをうまく整理するために時間をかけてください。これは、クライアントがパッケージを理解し、使用するのに役立ち、メンテナがパッケージを適切に成長させるのに役立ちます。

さらに読む

次の記事:Goのテスト可能な例
前の記事:エラーは値である
ブログインデックス