前書き
package in Goを作成する場合、最終的な目標は通常、他の開発者が高次のパッケージまたはプログラム全体で使用できるようにパッケージにアクセスできるようにすることです。 importing the packageにより、コードは他のより複雑なツールの構成要素として機能します。 ただし、インポートできるのは特定のパッケージのみです。 これは、パッケージの可視性によって決まります。
このコンテキストでのVisibilityは、パッケージまたはその他の構成を参照できるファイルスペースを意味します。 たとえば、関数で変数を定義する場合、その変数の可視性(スコープ)は、それが定義された関数内にのみ存在します。 同様に、パッケージ内で変数を定義する場合、そのパッケージのみに変数を表示することも、パッケージの外部にも変数を表示することもできます。
人間工学に基づいたコードを記述する場合、特にパッケージに加えたい将来の変更を考慮する場合は、パッケージの可視性を慎重に制御することが重要です。 バグの修正、パフォーマンスの改善、または機能の変更が必要な場合は、パッケージを使用している人のコードを壊さない方法で変更を行う必要があります。 重大な変更を最小限に抑える方法の1つは、パッケージを適切に使用するために必要な部分のみにアクセスを許可することです。 アクセスを制限することにより、他の開発者がパッケージを使用する方法に影響を与える可能性を少なくして、パッケージを内部的に変更できます。
この記事では、パッケージの可視性を制御する方法と、パッケージ内でのみ使用する必要があるコードの部分を保護する方法を学習します。 これを行うために、アイテムの可視性の度合いが異なるパッケージを使用して、メッセージを記録およびデバッグする基本的なロガーを作成します。
前提条件
この記事の例を実行するには、次のものが必要です。
-
How To Install Go and Set Up a Local Programming Environmentに従ってセットアップされたGoワークスペース。 このチュートリアルでは、次のファイル構造を使用します。
.
├── bin
│
└── src
└── github.com
└── gopherguides
エクスポートされたアイテムとエクスポートされていないアイテム
スコープを指定するためにpublic
、private
、protected
などのaccess modifiersを使用するJavaやPythonなどの他のプログラム言語とは異なり、Goはアイテムが%かどうかを判断します(t5)sおよび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
パッケージを作成して、パッケージの外部で表示したいものと表示しないものを念頭に置いてみましょう。 このロギングパッケージは、プログラムメッセージをコンソールに記録する役割を果たします。 また、ログに記録しているlevelも確認します。 レベルはログのタイプを記述し、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
というパッケージを宣言しました。 このパッケージには、Debug
とLog
の2つのexported
関数があります。 これらの関数は、logging
パッケージをインポートする他のパッケージから呼び出すことができます。 debug
と呼ばれるプライベート変数もあります。 この変数には、logging
パッケージ内からのみアクセスできます。 関数Debug
と変数debug
はどちらも同じスペルですが、関数は大文字で、変数は大文字ではないことに注意してください。 これにより、スコープが異なる明確な宣言になります。
ファイルを保存して終了します。
このパッケージをコードの他の領域で使用するには、import
it into a new packageを使用できます。 この新しいパッケージを作成しますが、これらのソースファイルを最初に保存するための新しいディレクトリが必要です。
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のパッケージの詳細については、Importing Packages in GoとHow To Write Packages in Goの記事を確認するか、How To Code in Go series全体を調べてください。