Goでのパッケージの可視性を理解する
序章
Go でパッケージを作成する場合、最終的な目標は通常、他の開発者が高次のパッケージまたはプログラム全体でパッケージにアクセスできるようにすることです。 パッケージをインポートすることにより、コードの一部を他のより複雑なツールの構成要素として機能させることができます。 ただし、インポートできるのは特定のパッケージのみです。 これは、パッケージの可視性によって決まります。
このコンテキストでのVisibilityは、パッケージまたは他の構成を参照できるファイルスペースを意味します。 たとえば、関数で変数を定義する場合、その変数の可視性(スコープ)は、それが定義された関数内にのみ存在します。 同様に、パッケージで変数を定義する場合は、そのパッケージだけに変数を表示することも、パッケージの外部にも表示できるようにすることもできます。
人間工学に基づいたコードを作成する場合、特にパッケージに加える可能性のある将来の変更を考慮する場合は、パッケージの可視性を注意深く制御することが重要です。 バグを修正したり、パフォーマンスを改善したり、機能を変更したりする必要がある場合は、パッケージを使用している人のコードを壊さない方法で変更を加える必要があります。 重大な変更を最小限に抑える1つの方法は、パッケージを適切に使用するために必要なパッケージの部分にのみアクセスを許可することです。 アクセスを制限することで、他の開発者がパッケージをどのように使用しているかに影響を与える可能性を少なくして、パッケージの内部で変更を加えることができます。
この記事では、パッケージの可視性を制御する方法と、パッケージ内でのみ使用する必要があるコードの部分を保護する方法を学習します。 これを行うには、アイテムの可視性の程度が異なるパッケージを使用して、メッセージをログに記録してデバッグするための基本的なロガーを作成します。
前提条件
この記事の例に従うには、次のものが必要です。
- Goのインストール方法とローカルプログラミング環境のセットアップに従ってセットアップされたGoワークスペース。 このチュートリアルでは、次のファイル構造を使用します。
.
├── bin
│
└── src
└── github.com
└── gopherguides
輸出品と未輸出品
JavaやPythonのような他のプログラム言語とは異なり、アクセス修飾子などを使用します。 public
, private
、 また protected
スコープを指定するために、Goはアイテムが exported
と unexported
それがどのように宣言されるかを通して。 この場合、アイテムをエクスポートすると、 visible
現在のパッケージの外。 エクスポートされていない場合は、定義されたパッケージ内からのみ表示および使用できます。
この外部の可視性は、宣言されたアイテムの最初の文字を大文字にすることによって制御されます。 などのすべての宣言 Types
, Variables
, Constants
, Functions
大文字で始まるなどは、現在のパッケージの外に表示されます。
キャピタライゼーションに注意を払いながら、次のコードを見てみましょう。
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
このコードは、それがにあることを宣言します greet
パッケージ。 次に、2つのシンボル、という変数を宣言します。 Greeting
、およびと呼ばれる関数 Hello
. どちらも大文字で始まるため、どちらも exported
外部のプログラムで利用できます。 前述のように、アクセスを制限するパッケージを作成すると、APIの設計が改善され、パッケージに依存する誰かのコードを壊すことなく、パッケージを内部で簡単に更新できるようになります。
パッケージの可視性の定義
プログラムでパッケージの可視性がどのように機能するかを詳しく見るために、 logging
パッケージの外側に何を表示したいのか、何を表示しないのかを念頭に置いてください。 このログパッケージは、プログラムメッセージをコンソールに記録する役割を果たします。 また、ログに記録しているレベルも確認します。 レベルはログのタイプを表し、次の3つのステータスのいずれかになります。 info
, warning
、 また error
.
まず、あなたの中で src
ディレクトリ、というディレクトリを作成しましょう logging
ログファイルを配置するには:
- mkdir logging
次にそのディレクトリに移動します。
- cd logging
次に、nanoなどのエディターを使用して、というファイルを作成します。 logging.go
:
- nano logging.go
次のコードを 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
. このパッケージには2つあります exported
機能: Debug
と Log
. これらの関数は、インポートする他のパッケージから呼び出すことができます。 logging
パッケージ。 と呼ばれるプライベート変数もあります debug
. この変数には、 logging
パッケージ。 機能が機能している間、注意することが重要です Debug
と変数 debug
どちらも同じスペルで、関数は大文字になり、変数は大文字になりません。 これにより、スコープが異なる別個の宣言になります。
ファイルを保存して終了します。
このパッケージをコードの他の領域で使用するには、新しいパッケージにインポートします。 この新しいパッケージを作成しますが、最初にこれらのソースファイルを保存するための新しいディレクトリが必要になります。
から移動しましょう logging
ディレクトリ、という名前の新しいディレクトリを作成します cmd
、そしてその新しいディレクトリに移動します。
- cd ..
- mkdir cmd
- cd cmd
というファイルを作成します main.go
の中に cmd
作成したディレクトリ:
- nano 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モジュールは、パッケージディレクトリに配置される構成ファイルであり、コンパイラにパッケージのインポート元を指示します。 モジュールについて学ぶことはこの記事の範囲を超えていますが、この例をローカルで機能させるために、ほんの数行の構成を書くことができます。
以下を開きます go.mod
のファイル cmd
ディレクトリ:
- nano go.mod
次に、次の内容をファイルに配置します。
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
このファイルの最初の行は、コンパイラに cmd
パッケージのファイルパスは github.com/gopherguides/cmd
. 2行目は、パッケージがパッケージであることをコンパイラに通知します github.com/gopherguides/logging
のディスク上でローカルに見つけることができます ../logging
ディレクトリ。
また、 go.mod
私たちのファイル logging
パッケージ。 に戻りましょう logging
ディレクトリを作成し、 go.mod
ファイル:
- cd ../logging
- nano go.mod
次の内容をファイルに追加します。
module github.com/gopherguides/logging
これはコンパイラに次のことを伝えます logging
私たちが作成したパッケージは実際には github.com/gopherguides/logging
パッケージ。 これにより、パッケージをインポートすることが可能になります main
以前に書いた次の行を含むパッケージ:
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
すべての構成が完了したので、次のコマンドを実行できます。 main
からのプログラム cmd
次のコマンドでパッケージ化します。
- 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
パッケージ。 しかし debug
の変数 logging
パッケージはエクスポートされません。 エクスポートされていない宣言を参照しようとすると、コンパイル時エラーが発生します。
次の強調表示された行をに追加します 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
コードをリファクタリングしてロガーを分離するには、次のようにパッケージ化します。
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
構造体。 この構造体には、印刷する時間形式や debug
の可変設定 true
また false
. The New
関数は、時間形式やデバッグ状態など、ロガーを作成するための初期状態を設定します。 次に、内部で指定した値をエクスポートされていない変数に格納します timeFormat
と debug
. また、というメソッドを作成しました Log
に Logger
印刷したいステートメントを受け取るタイプ。 以内 Log
methodは、そのローカルメソッド変数への参照です l
次のような内部フィールドにアクセスできるようにします l.timeFormat
と l.debug
.
このアプローチにより、 Logger
多くの異なるパッケージで使用し、他のパッケージがどのように使用しているかに関係なく使用します。
別のパッケージで使用するには、変更してみましょう 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
ステートメントを印刷します。
からエクスポートされていないフィールドを参照しようとすると Logger
など timeFormat
フィールドでは、コンパイル時エラーが発生します。 次の強調表示された行を追加して実行してみてください 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ロギングをロガーに追加しましょう。 平準化されたロギングは、ログを分類して、特定のタイプのイベントについてログを検索できるようにする手段です。 ロガーに入れるレベルは次のとおりです。
-
The
info
レベル。ユーザーにアクションを通知する情報タイプのイベントを表します。Program started
、 またEmail sent
. これらは、プログラムの一部をデバッグおよび追跡して、予期された動作が発生しているかどうかを確認するのに役立ちます。 -
The
warning
レベル。 これらのタイプのイベントは、エラーではない予期しないことが発生したときを識別します。Email failed to send, retrying
. 彼らは私たちが期待したほどスムーズに進んでいない私たちのプログラムの部分を見るのを助けてくれます。 -
The
error
レベル。これは、プログラムで次のような問題が発生したことを意味します。File not found
. これにより、プログラムの操作が失敗することがよくあります。
また、特定のレベルのロギングをオンまたはオフにすることもできます。特に、プログラムが期待どおりに実行されておらず、プログラムをデバッグしたい場合はそうです。 プログラムを変更してこの機能を追加します。 debug
に設定されています true
、すべてのレベルのメッセージを出力します。 そうでなければ、それが false
、エラーメッセージのみを出力します。
次の変更を加えて、平準化されたログを追加します。 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
ログメッセージの。 The Log
メソッドは、メッセージのレベルを決定します。 それが info
また warning
メッセージ、および debug
フィールドは true
、次にメッセージを書き込みます。 それ以外の場合は、メッセージを無視します。 それが他のレベルの場合、 error
、関係なくメッセージを書き出します。
メッセージが出力されるかどうかを判断するためのロジックのほとんどは、 Log
方法。 また、エクスポートされていないメソッドと呼ばれるものを導入しました write
. The write
メソッドは、実際にログメッセージを出力するものです。
変更することで、他のパッケージでこの平準化されたロギングを使用できるようになりました 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
方法。
これで、 level
切り替えによる各メッセージの debug
に false
:
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
電話をかけようとすると write
外部からの方法 logging
パッケージの場合、コンパイル時エラーが発生します。
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)
コンパイラーは、小文字で始まる別のパッケージから何かを参照しようとしていることを認識すると、それがエクスポートされていないことを認識しているため、コンパイラー・エラーをスローします。
このチュートリアルのロガーは、他のパッケージに使用させたい部分のみを公開するコードを作成する方法を示しています。 パッケージのどの部分がパッケージの外部に表示されるかを制御するため、パッケージに依存するコードに影響を与えることなく、将来の変更を行うことができるようになりました。 たとえば、オフにするだけの場合 info
レベルのメッセージ debug
がfalseの場合、APIの他の部分に影響を与えることなくこの変更を行うことができます。 また、ログメッセージを安全に変更して、プログラムの実行元のディレクトリなどの詳細情報を含めることもできます。
結論
この記事では、パッケージの実装の詳細を保護しながら、パッケージ間でコードを共有する方法を示しました。 これにより、下位互換性のためにほとんど変更されない単純なAPIをエクスポートできますが、将来的に機能を向上させるために、必要に応じてパッケージ内で非公開で変更を行うことができます。 これは、パッケージとそれに対応するAPIを作成する際のベストプラクティスと見なされます。
Goのパッケージの詳細については、GoでのパッケージのインポートおよびGoでのパッケージの作成方法の記事を確認するか、