序章

Go パッケージを作成する場合、最終的な目標は通常、他の開発者が高次のパッケージまたはプログラム全体でパッケージにアクセスできるようにすることです。 パッケージをインポートすることにより、コードの一部を他のより複雑なツールの構成要素として機能させることができます。 ただし、インポートできるのは特定のパッケージのみです。 これは、パッケージの可視性によって決まります。

このコンテキストでのVisibilityは、パッケージまたは他の構成を参照できるファイルスペースを意味します。 たとえば、関数で変数を定義する場合、その変数の可視性(スコープ)は、それが定義された関数内にのみ存在します。 同様に、パッケージで変数を定義する場合は、そのパッケージだけに変数を表示することも、パッケージの外部にも表示できるようにすることもできます。

人間工学に基づいたコードを作成する場合、特にパッケージに加える可能性のある将来の変更を考慮する場合は、パッケージの可視性を注意深く制御することが重要です。 バグを修正したり、パフォーマンスを改善したり、機能を変更したりする必要がある場合は、パッケージを使用している人のコードを壊さない方法で変更を加える必要があります。 重大な変更を最小限に抑える1つの方法は、パッケージを適切に使用するために必要なパッケージの部分にのみアクセスを許可することです。 アクセスを制限することで、他の開発者がパッケージをどのように使用しているかに影響を与える可能性を少なくして、パッケージの内部で変更を加えることができます。

この記事では、パッケージの可視性を制御する方法と、パッケージ内でのみ使用する必要があるコードの部分を保護する方法を学習します。 これを行うには、アイテムの可視性の程度が異なるパッケージを使用して、メッセージをログに記録してデバッグするための基本的なロガーを作成します。

前提条件

この記事の例に従うには、次のものが必要です。

.
├── bin 
│ 
└── src
    └── github.com
        └── gopherguides

輸出品と未輸出品

JavaやPythonのような他のプログラム言語とは異なり、アクセス修飾子などを使用します。 public, private、 また protected スコープを指定するために、Goはアイテムが exportedunexported それがどのように宣言されるかを通して。 この場合、アイテムをエクスポートすると、 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 パッケージ。 次に、2つのシンボル、という変数を宣言します。 Greeting、およびと呼ばれる関数 Hello. どちらも大文字で始まるため、どちらも exported 外部のプログラムで利用できます。 前述のように、アクセスを制限するパッケージを作成すると、APIの設計が改善され、パッケージに依存する誰かのコードを壊すことなく、パッケージを内部で簡単に更新できるようになります。

パッケージの可視性の定義

プログラムでパッケージの可視性がどのように機能するかを詳しく見るために、 logging パッケージの外側に何を表示したいのか、何を表示しないのかを念頭に置いてください。 このログパッケージは、プログラムメッセージをコンソールに記録する役割を果たします。 また、ログに記録しているレベルも確認します。 レベルはログのタイプを表し、次の3つのステータスのいずれかになります。 info, warning、 また error.

まず、あなたの中で src ディレクトリ、というディレクトリを作成しましょう logging ログファイルを配置するには:

  1. mkdir logging

次にそのディレクトリに移動します。

  1. cd logging

次に、nanoなどのエディターを使用して、というファイルを作成します。 logging.go:

  1. 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. このパッケージには2つあります exported 機能: DebugLog. これらの関数は、インポートする他のパッケージから呼び出すことができます。 logging パッケージ。 と呼ばれるプライベート変数もあります debug. この変数には、 logging パッケージ。 機能が機能している間、注意することが重要です Debug と変数 debug どちらも同じスペルで、関数は大文字になり、変数は大文字になりません。 これにより、スコープが異なる別個の宣言になります。

ファイルを保存して終了します。

このパッケージをコードの他の領域で使用するには、新しいパッケージにインポートします。 この新しいパッケージを作成しますが、最初にこれらのソースファイルを保存するための新しいディレクトリが必要になります。

から移動しましょう logging ディレクトリ、という名前の新しいディレクトリを作成します cmd、そしてその新しいディレクトリに移動します。

  1. cd ..
  2. mkdir cmd
  3. cd cmd

というファイルを作成します main.go の中に cmd 作成したディレクトリ:

  1. 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モジュールは、パッケージディレクトリに配置される構成ファイルであり、コンパイラにパッケージのインポート元を指示します。 モジュールについて学ぶことはこの記事の範囲を超えていますが、この例をローカルで機能させるために、ほんの数行の構成を書くことができます。

以下を開きます go.mod のファイル cmd ディレクトリ:

  1. 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 ディレクトリ。

また、 go.mod 私たちのファイル logging パッケージ。 に戻りましょう logging ディレクトリを作成し、 go.mod ファイル:

  1. cd ../logging
  2. 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

すべての構成が完了したので、次のコマンドを実行できます。 main からのプログラム cmd 次のコマンドでパッケージ化します。

  1. cd ../cmd
  2. go run main.go

次のような出力が得られます。

Output
2019-08-28T11:36:09-05:00 This is a debug statement...

プログラムは、RFC 3339形式で現在の時刻を出力し、その後にロガーに送信したステートメントを出力します。 RFC 3339 は、インターネット上の時間を表すように設計された時間形式であり、ログファイルで一般的に使用されます。

なぜなら DebugLog 関数はロギングパッケージからエクスポートされ、 main パッケージ。 しかし debug の変数 logging パッケージはエクスポートされません。 エクスポートされていない宣言を参照しようとすると、コンパイル時エラーが発生します。

次の強調表示された行をに追加します 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

これで、 exportedunexported パッケージ内のアイテムは動作します。次に、どのように動作するかを見ていきます fieldsmethods からエクスポートできます 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 構造体。 この構造体には、印刷する時間形式や debug の可変設定 true また false. The New 関数は、時間形式やデバッグ状態など、ロガーを作成するための初期状態を設定します。 次に、内部で指定した値をエクスポートされていない変数に格納します timeFormatdebug. また、というメソッドを作成しました LogLogger 印刷したいステートメントを受け取るタイプ。 以内 Log methodは、そのローカルメソッド変数への参照です l 次のような内部フィールドにアクセスできるようにします l.timeFormatl.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...")
}

このプログラムを実行すると、次の出力が得られます。

Output
2019-08-28T11:56:49-05:00 This is a debug statement...

このコードでは、エクスポートされた関数を呼び出してロガーのインスタンスを作成しました New. このインスタンスへの参照をに保存しました logger 変数。 これで電話できます logging.Log ステートメントを印刷します。

からエクスポートされていないフィールドを参照しようとすると Logger など timeFormat フィールドでは、コンパイル時エラーが発生します。 次の強調表示された行を追加して実行してみてください 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ロギングをロガーに追加しましょう。 平準化されたロギングは、ログを分類して、特定のタイプのイベントについてログを検索できるようにする手段です。 ロガーに入れるレベルは次のとおりです。

  • The info レベル。ユーザーにアクションを通知する情報タイプのイベントを表します。 Program started、 また Email sent. これらは、プログラムの一部をデバッグおよび追跡して、予期された動作が発生しているかどうかを確認するのに役立ちます。

  • The warning レベル。 これらのタイプのイベントは、エラーではない予期しないことが発生したときを識別します。 Email failed to send, retrying. 彼らは私たちが期待したほどスムーズに進んでいない私たちのプログラムの部分を見るのを助けてくれます。

  • The 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 ログメッセージの。 The Log メソッドは、メッセージのレベルを決定します。 それが info また warning メッセージ、および debug フィールドは true、次にメッセージを書き込みます。 それ以外の場合は、メッセージを無視します。 それが他のレベルの場合、 error、関係なくメッセージを書き出します。

メッセージが出力されるかどうかを判断するためのロジックのほとんどは、 Log 方法。 また、エクスポートされていないメソッドと呼ばれるものを導入しました write. The 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 方法。

これで、 level 切り替えによる各メッセージの debugfalse:

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

電話をかけようとすると write 外部からの方法 logging パッケージの場合、コンパイル時エラーが発生します。

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...")
}
Output
cmd/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でのパッケージの作成方法の記事を確認するか、Goでのコーディング方法シリーズ[ X185X]。