Goブログ

パッケージ名

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パッケージはbufioと呼ばれ、bufではありません。なぜなら、bufはバッファに適した変数名だからです。

パッケージの内容の命名

パッケージ名とその内容の名前は、クライアントコードで一緒に使用されるため、結び付けられています。パッケージを設計するときは、クライアントの観点を考慮してください。

繰り返しを避けてください。クライアントコードはパッケージ名を参照するときにプレフィックスとして使用するため、内容の名前はパッケージ名を繰り返す必要はありません。httpパッケージによって提供されるHTTPサーバーは、HTTPServerではなくServerと呼ばれます。クライアントコードはこの型を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パッケージには、名前とパスの両方があります。パッケージ名はソースファイルのpackageステートメントで指定されています。クライアントコードは、パッケージのエクスポートされた名前のプレフィックスとして使用します。クライアントコードは、パッケージをインポートするときにパッケージパスを使用します。慣例により、パッケージパスの最後の要素はパッケージ名です。

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

ビルドツールは、パッケージパスをディレクトリにマッピングします。goツールは、GOPATH環境変数を使用して、パス"github.com/user/hello"のソースファイルをディレクトリ$GOPATH/src/github.com/user/hello内で検索します。(この状況は当然のことながら慣れているはずですが、パッケージの用語と構造について明確にすることが重要です。)

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

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

悪いパッケージ名

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

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

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

結論

パッケージ名は、Goプログラムでの適切な命名の中心です。時間をかけて適切なパッケージ名を選択し、コードを適切に編成してください。これにより、クライアントはパッケージを理解して使用しやすくなり、保守担当者はそれらを適切に成長させることができます。

さらに読む

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