Goでのパッケージの可視性を理解する

提供:Dev Guides
移動先:案内検索

序章

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

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

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

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

前提条件

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

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

輸出品と未輸出品

publicprivateprotectedなどのアクセス修飾子を使用してスコープを指定するJavaやPythonなどの他のプログラム言語とは異なります、Goは、アイテムがexportedおよびunexportedであるかどうかを、その宣言方法によって判別します。 この場合、アイテムをエクスポートすると、現在のパッケージの外にvisibleになります。 エクスポートされていない場合は、定義されたパッケージ内からのみ表示および使用できます。

この外部の可視性は、宣言されたアイテムの最初の文字を大文字にすることによって制御されます。 TypesVariablesConstantsFunctionsなど、大文字で始まるすべての宣言は、現在のパッケージの外部に表示されます。

キャピタライゼーションに注意を払いながら、次のコードを見てみましょう。

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パッケージを作成して、パッケージの外部で表示したいものと表示しないものを念頭に置いてみましょう。 このログパッケージは、プログラムメッセージをコンソールに記録する役割を果たします。 また、ログに記録しているレベルも確認します。 レベルはログのタイプを表し、infowarning、または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機能があります。DebugLogです。 これらの関数は、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およびmethodsstructs

構造体内の可視性

前のセクションで作成したロガーの可視性スキームは単純なプログラムでは機能する可能性がありますが、状態が多すぎるため、複数のパッケージ内から使用することはできません。 これは、エクスポートされた変数が、変数を矛盾した状態に変更する可能性のある複数のパッケージにアクセスできるためです。 この方法でパッケージの状態を変更できるようにすると、プログラムの動作を予測するのが難しくなります。 たとえば、現在の設計では、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またはfalsedebug変数設定を含む未エクスポートの状態を格納します。 New関数は、時間形式やデバッグ状態など、ロガーを作成するための初期状態を設定します。 次に、内部で指定した値を、エクスポートされていない変数timeFormatおよびdebugに格納します。 また、LoggerタイプにLogというメソッドを作成し、出力したいステートメントを受け取ります。 Logメソッド内には、ローカルメソッド変数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...")
}

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

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 startedEmail sentなどのアクションをユーザーに通知する情報タイプのイベントを表します。 これらは、プログラムの一部をデバッグおよび追跡して、予期された動作が発生しているかどうかを確認するのに役立ちます。
  • warningレベル。 これらのタイプのイベントは、Email failed to send, retryingのように、エラーではない予期しないことが発生したことを識別します。 彼らは私たちが期待したほどスムーズに進んでいない私たちのプログラムの部分を見るのを助けてくれます。
  • errorレベル。これは、プログラムでFile not foundなどの問題が発生したことを意味します。 これにより、プログラムの操作が失敗することがよくあります。

また、特定のレベルのロギングをオンまたはオフにすることもできます。特に、プログラムが期待どおりに実行されておらず、プログラムをデバッグしたい場合はそうです。 debugtrueに設定されている場合に、すべてのレベルのメッセージが出力されるようにプログラムを変更して、この機能を追加します。 それ以外の場合、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メソッドを正常に使用しました。

これで、debugfalseに切り替えることで、各メッセージの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]。