Goでのパッケージの可視性を理解する
序章
Go でパッケージを作成する場合、最終的な目標は通常、他の開発者が高次のパッケージまたはプログラム全体でパッケージにアクセスできるようにすることです。 パッケージをインポートすることにより、コードの一部を他のより複雑なツールの構成要素として機能させることができます。 ただし、インポートできるのは特定のパッケージのみです。 これは、パッケージの可視性によって決まります。
このコンテキストでのVisibilityは、パッケージまたは他の構成を参照できるファイルスペースを意味します。 たとえば、関数で変数を定義する場合、その変数の可視性(スコープ)は、それが定義された関数内にのみ存在します。 同様に、パッケージで変数を定義する場合は、そのパッケージだけに変数を表示することも、パッケージの外部にも表示できるようにすることもできます。
人間工学に基づいたコードを作成する場合、特にパッケージに加える可能性のある将来の変更を考慮する場合は、パッケージの可視性を注意深く制御することが重要です。 バグを修正したり、パフォーマンスを改善したり、機能を変更したりする必要がある場合は、パッケージを使用している人のコードを壊さない方法で変更を加える必要があります。 重大な変更を最小限に抑える1つの方法は、パッケージを適切に使用するために必要なパッケージの部分にのみアクセスを許可することです。 アクセスを制限することで、他の開発者がパッケージをどのように使用しているかに影響を与える可能性を少なくして、パッケージの内部で変更を加えることができます。
この記事では、パッケージの可視性を制御する方法と、パッケージ内でのみ使用する必要があるコードの部分を保護する方法を学習します。 これを行うには、アイテムの可視性の程度が異なるパッケージを使用して、メッセージをログに記録してデバッグするための基本的なロガーを作成します。
前提条件
この記事の例に従うには、次のものが必要です。
- Goのインストール方法とローカルプログラミング環境のセットアップに従ってセットアップされたGoワークスペース。 このチュートリアルでは、次のファイル構造を使用します。
. ├── bin │ └── src └── github.com └── gopherguides
輸出品と未輸出品
public
、private
、protected
などのアクセス修飾子を使用してスコープを指定するJavaやPythonなどの他のプログラム言語とは異なります、Goは、アイテムがexported
およびunexported
であるかどうかを、その宣言方法によって判別します。 この場合、アイテムをエクスポートすると、現在のパッケージの外にvisible
になります。 エクスポートされていない場合は、定義されたパッケージ内からのみ表示および使用できます。
この外部の可視性は、宣言されたアイテムの最初の文字を大文字にすることによって制御されます。 Types
、Variables
、Constants
、Functions
など、大文字で始まるすべての宣言は、現在のパッケージの外部に表示されます。
キャピタライゼーションに注意を払いながら、次のコードを見てみましょう。
greet.go
package greet import "fmt" var Greeting string func Hello(name string) string { return fmt.Sprintf(Greeting, name) }
このコードは、greet
パッケージに含まれていることを宣言します。 次に、Greeting
という変数とHello
という関数の2つのシンボルを宣言します。 どちらも大文字で始まるため、どちらもexported
であり、外部のプログラムで使用できます。 前述のように、アクセスを制限するパッケージを作成すると、APIの設計が改善され、パッケージに依存する誰かのコードを壊すことなく、パッケージを内部で簡単に更新できるようになります。
パッケージの可視性の定義
プログラムでパッケージの可視性がどのように機能するかを詳しく見るために、logging
パッケージを作成して、パッケージの外部で表示したいものと表示しないものを念頭に置いてみましょう。 このログパッケージは、プログラムメッセージをコンソールに記録する役割を果たします。 また、ログに記録しているレベルも確認します。 レベルはログのタイプを表し、info
、warning
、またはerror
の3つのステータスのいずれかになります。
まず、src
ディレクトリ内に、logging
というディレクトリを作成して、ログファイルを次の場所に配置します。
mkdir logging
次にそのディレクトリに移動します。
cd logging
次に、nanoなどのエディターを使用して、logging.go
というファイルを作成します。
nano logging.go
作成したlogging.go
ファイルに次のコードを配置します。
logging / logging.go
package logging import ( "fmt" "time" ) var debug bool func Debug(b bool) { debug = b } func Log(statement string) { if !debug { return } fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement) }
このコードの最初の行は、logging
というパッケージを宣言しました。 このパッケージには、exported
機能があります。Debug
とLog
です。 これらの関数は、logging
パッケージをインポートする他のパッケージから呼び出すことができます。 debug
と呼ばれるプライベート変数もあります。 この変数には、logging
パッケージ内からのみアクセスできます。 関数Debug
と変数debug
はどちらも同じスペルですが、関数は大文字で表記され、変数は大文字ではないことに注意してください。 これにより、スコープが異なる別個の宣言になります。
ファイルを保存して終了します。
このパッケージをコードの他の領域で使用するには、新しいパッケージにインポートします。 この新しいパッケージを作成しますが、最初にこれらのソースファイルを保存するための新しいディレクトリが必要になります。
logging
ディレクトリから移動し、cmd
という名前の新しいディレクトリを作成して、その新しいディレクトリに移動しましょう。
cd .. mkdir cmd cd cmd
作成したcmd
ディレクトリにmain.go
というファイルを作成します。
nano main.go
これで、次のコードを追加できます。
cmd / main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") }
これで、プログラム全体が作成されました。 ただし、このプログラムを実行する前に、コードを正しく機能させるために、いくつかの構成ファイルも作成する必要があります。 Goは、 Go Modules を使用して、リソースをインポートするためのパッケージの依存関係を構成します。 Goモジュールは、パッケージディレクトリに配置される構成ファイルであり、コンパイラにパッケージのインポート元を指示します。 モジュールについて学ぶことはこの記事の範囲を超えていますが、この例をローカルで機能させるために、ほんの数行の構成を書くことができます。
cmd
ディレクトリにある次のgo.mod
ファイルを開きます。
nano go.mod
次に、次の内容をファイルに配置します。
go.mod
module github.com/gopherguides/cmd replace github.com/gopherguides/logging => ../logging
このファイルの最初の行は、cmd
パッケージのファイルパスがgithub.com/gopherguides/cmd
であることをコンパイラーに通知します。 2行目は、パッケージgithub.com/gopherguides/logging
がディスク上の../logging
ディレクトリのローカルにあることをコンパイラに通知します。
logging
パッケージ用のgo.mod
ファイルも必要です。 logging
ディレクトリに戻り、go.mod
ファイルを作成しましょう。
cd ../logging nano go.mod
次の内容をファイルに追加します。
go.mod
module github.com/gopherguides/logging
これは、作成したlogging
パッケージが実際にはgithub.com/gopherguides/logging
パッケージであることをコンパイラーに通知します。 これにより、前に書いた次の行を使用して、main
パッケージにパッケージをインポートできます。
cmd / main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") }
これで、次のディレクトリ構造とファイルレイアウトが作成されます。
├── cmd │ ├── go.mod │ └── main.go └── logging ├── go.mod └── logging.go
すべての構成が完了したので、次のコマンドを使用して、cmd
パッケージからmain
プログラムを実行できます。
cd ../cmd go run main.go
次のような出力が得られます。
Output2019-08-28T11:36:09-05:00 This is a debug statement...
プログラムは、RFC 3339形式で現在の時刻を出力し、その後にロガーに送信したステートメントを出力します。 RFC 3339 は、インターネット上の時間を表すように設計された時間形式であり、ログファイルで一般的に使用されます。
Debug
およびLog
関数はロギングパッケージからエクスポートされるため、main
パッケージで使用できます。 ただし、logging
パッケージのdebug
変数はエクスポートされません。 エクスポートされていない宣言を参照しようとすると、コンパイル時エラーが発生します。
次の強調表示された行をmain.go
に追加します。
cmd / main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") fmt.Println(logging.debug) }
ファイルを保存して実行します。 次のようなエラーが表示されます。
Output. . . ./main.go:10:14: cannot refer to unexported name logging.debug
パッケージ内のexported
およびunexported
アイテムの動作を確認したので、次にfields
およびmethods
をstructs
。
構造体内の可視性
前のセクションで作成したロガーの可視性スキームは単純なプログラムでは機能する可能性がありますが、状態が多すぎるため、複数のパッケージ内から使用することはできません。 これは、エクスポートされた変数が、変数を矛盾した状態に変更する可能性のある複数のパッケージにアクセスできるためです。 この方法でパッケージの状態を変更できるようにすると、プログラムの動作を予測するのが難しくなります。 たとえば、現在の設計では、1つのパッケージでDebug
変数をtrue
に設定し、別のパッケージで同じインスタンスでfalse
に設定できます。 logging
パッケージをインポートしている両方のパッケージが影響を受けるため、これにより問題が発生します。
構造体を作成し、そこにメソッドをぶら下げることで、ロガーを分離することができます。 これにより、ロガーのinstance
を作成して、それを使用する各パッケージで個別に使用できるようになります。
logging
パッケージを次のように変更して、コードをリファクタリングし、ロガーを分離します。
logging / logging.go
package logging import ( "fmt" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(s string) { if !l.debug { return } fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s) }
このコードでは、Logger
構造体を作成しました。 この構造体は、印刷する時間形式やtrue
またはfalse
のdebug
変数設定を含む未エクスポートの状態を格納します。 New
関数は、時間形式やデバッグ状態など、ロガーを作成するための初期状態を設定します。 次に、内部で指定した値を、エクスポートされていない変数timeFormat
およびdebug
に格納します。 また、Logger
タイプにLog
というメソッドを作成し、出力したいステートメントを受け取ります。 Log
メソッド内には、ローカルメソッド変数l
への参照があり、l.timeFormat
やl.debug
などの内部フィールドにアクセスできます。
このアプローチにより、多くの異なるパッケージでLogger
を作成し、他のパッケージがどのように使用しているかに関係なく使用できるようになります。
別のパッケージで使用するには、cmd/main.go
を次のように変更してみましょう。
cmd / main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") }
このプログラムを実行すると、次の出力が得られます。
Output2019-08-28T11:56:49-05:00 This is a debug statement...
このコードでは、エクスポートされた関数New
を呼び出して、ロガーのインスタンスを作成しました。 このインスタンスへの参照をlogger
変数に格納しました。 これで、logging.Log
を呼び出してステートメントを出力できます。
timeFormat
フィールドなど、Logger
からエクスポートされていないフィールドを参照しようとすると、コンパイル時エラーが発生します。 次の強調表示された行を追加して、cmd/main.go
を実行してみてください。
cmd / main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") fmt.Println(logger.timeFormat) }
これにより、次のエラーが発生します。
Output. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
コンパイラは、logger.timeFormat
がエクスポートされていないことを認識しているため、logging
パッケージから取得できません。
メソッド内の可視性
構造体フィールドと同じように、メソッドもエクスポートまたは非エクスポートできます。
これを説明するために、leveledロギングをロガーに追加しましょう。 平準化されたロギングは、ログを分類して、特定のタイプのイベントについてログを検索できるようにする手段です。 ロガーに入れるレベルは次のとおりです。
info
レベル。これは、Program started
やEmail sent
などのアクションをユーザーに通知する情報タイプのイベントを表します。 これらは、プログラムの一部をデバッグおよび追跡して、予期された動作が発生しているかどうかを確認するのに役立ちます。warning
レベル。 これらのタイプのイベントは、Email failed to send, retrying
のように、エラーではない予期しないことが発生したことを識別します。 彼らは私たちが期待したほどスムーズに進んでいない私たちのプログラムの部分を見るのを助けてくれます。error
レベル。これは、プログラムでFile not found
などの問題が発生したことを意味します。 これにより、プログラムの操作が失敗することがよくあります。
また、特定のレベルのロギングをオンまたはオフにすることもできます。特に、プログラムが期待どおりに実行されておらず、プログラムをデバッグしたい場合はそうです。 debug
がtrue
に設定されている場合に、すべてのレベルのメッセージが出力されるようにプログラムを変更して、この機能を追加します。 それ以外の場合、false
の場合、エラーメッセージのみが出力されます。
logging/logging.go
に次の変更を加えて、平準化されたログを追加します。
logging / logging.go
package logging import ( "fmt" "strings" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(level string, s string) { level = strings.ToLower(level) switch level { case "info", "warning": if l.debug { l.write(level, s) } default: l.write(level, s) } } func (l *Logger) write(level string, s string) { fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s) }
この例では、Log
メソッドに新しい引数を導入しました。 これで、ログメッセージのlevel
を渡すことができます。 Log
メソッドは、メッセージのレベルを決定します。 info
またはwarning
メッセージであり、debug
フィールドがtrue
の場合、メッセージを書き込みます。 それ以外の場合は、メッセージを無視します。 error
のように他のレベルの場合は、関係なくメッセージを書き出します。
メッセージが出力されるかどうかを決定するためのロジックのほとんどは、Log
メソッドに存在します。 また、write
と呼ばれる未エクスポートのメソッドを導入しました。 write
メソッドは、実際にログメッセージを出力する方法です。
cmd/main.go
を次のように変更することで、他のパッケージでこの平準化されたロギングを使用できるようになりました。
cmd / main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") }
これを実行すると、次のようになります。
Output[info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed
この例では、cmd/main.go
はエクスポートされたLog
メソッドを正常に使用しました。
これで、debug
をfalse
に切り替えることで、各メッセージのlevel
を渡すことができます。
main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, false) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") }
これで、error
レベルのメッセージのみが出力されることがわかります。
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
logging
パッケージの外部からwrite
メソッドを呼び出そうとすると、コンパイル時エラーが発生します。
main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") logger.write("error", "log this message...") }
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
コンパイラーは、小文字で始まる別のパッケージから何かを参照しようとしていることを認識すると、それがエクスポートされていないことを認識しているため、コンパイラー・エラーをスローします。
このチュートリアルのロガーは、他のパッケージに使用させたい部分のみを公開するコードを作成する方法を示しています。 パッケージのどの部分がパッケージの外部に表示されるかを制御するため、パッケージに依存するコードに影響を与えることなく、将来の変更を行うことができるようになりました。 たとえば、debug
がfalseの場合にのみinfo
レベルのメッセージをオフにしたい場合は、APIの他の部分に影響を与えることなくこの変更を行うことができます。 また、ログメッセージを安全に変更して、プログラムの実行元のディレクトリなどの詳細情報を含めることもできます。
結論
この記事では、パッケージの実装の詳細を保護しながら、パッケージ間でコードを共有する方法を示しました。 これにより、下位互換性のためにほとんど変更されない単純なAPIをエクスポートできますが、将来的に機能を向上させるために、必要に応じてパッケージ内で非公開で変更を行うことができます。 これは、パッケージとそれに対応するAPIを作成する際のベストプラクティスと見なされます。
Goのパッケージの詳細については、GoでのパッケージのインポートおよびGoでのパッケージの作成方法の記事を確認するか、Goでのコーディング方法シリーズ[ X185X]。