Golangの外部パッケージを使いこなそう!アクセス制限のポイントを解説

はじめに

こんにちは、Golang プログラマの皆さん。
今回の記事では、Golang の外部パッケージの少し複雑な例について、サンプルコードを交えて説明します。

Golang のパッケージに関する知識を深めることで、より一層効率的でシンプルなコードを書けるようになると思います。

本記事の対象読者

Golang の基本的な構文とパッケージシステムについては理解していることを前提としています。
本記事を読む前に以下の事柄については把握していることをお勧めします。

  1. 外部・内部パッケージの違い
  2. アクセス制御の仕組み
  3. import 文の使い方
  4. 基本的なデータ型と関数・メソッドの使い方

これらの前提知識に加え、次のエントリでは、特に外部パッケージに焦点を当てて詳細に説明しています。

検証環境

$ go version
go version go1.20.2 darwin/arm64

今回のエントリをまとめようと思ったきっかけ

私が今回のブログ記事を書くきっかけとなったのは、次のリンクページでした。

このページには、Golang の外部/内部パッケージに関する非常に興味深い情報がありました。
特に自分が誤解していた点を修正し、パッケ ージのインポートやアクセスに関する詳細な説明が記載されていたため、更に理解が深まりました。

ただ実際に動かしてみると、外部パッケージのリソース参照に関して、記事に書かれている内容と異なる挙動をしている点がありました。
そのため、その点を取り入れた新しいブログ記事を書くことに決めました。

今回のエントリで取り扱うソースコードとディレクトリ構造

早速サンプルコードを交えて、外部パッケージのリソース呼び出しの挙動を確認していきます。
エントリ内では 2 つのソースコードが登場します。

  • sub.go ( package = dir )
  • main.go ( package = main )

フォルダ構成は以下のようになっています。
go.mod ファイルは go mod init hello というコマンドで作成しました。

$ tree
.
├── dir
│   └── sub.go
├── go.mod
└── main.go

main.gosub.go の様々なリソースを呼び出すようになっています。

sub.go のコード

構造体、インタフェース、変数、関数、メソッドを定義しています。
Go では名前が大文字で始まるリソースはパッケージ外から参照可能であり、小文字やアンダースコアで始まるリソースはパッケージ内でしか参照できません

package dir

type (
    Msg struct {
        Member string
        member string
    }
    msg struct {
        Member string
        member string
    }
    Sender interface {
        Say() string
    }
    sender interface {
        Say() string
    }
)

var A string = "AAA"
var a string = "aaa"

func NewInternalMsg() msg {
    return msg{
        Member: "HELLO",
        member: "world",
    }
}

func NewInternalSender() sender {
    var s sender
    s = msg{
        Member: "HELLO",
        member: "world",
    }
    return s
}

func (m Msg) InternalMember() string {
    return "Msg's " + m.member
}

func (m msg) InternalMember() string {
    return "msg's " + m.member
}

func (m Msg) Say() string {
    return m.Member + ":" + m.member
}

func (m msg) Say() string {
    return m.Member + ":" + m.member
}

main.go のコード

sub.go 内のリソースを参照するコードとなります。

package main

import (
    "fmt"
    . "hello/dir"
)

func main() {
    x1 := Msg{
        Member: "XXX1",
        // member: "xxx1",        => NG
    }
    fmt.Printf("%#v\n", x1.Member) // => "XXX1"
    // fmt.Printf("%#v\n", x1.member)             => NG
    fmt.Printf("%#v\n", x1.InternalMember()) // => "Msg's "

    // x2 := msg{}              => NG

    x3 := NewInternalMsg()
    fmt.Printf("%#v\n", x3.Member) // => "HELLO"
    // fmt.Printf("%#v\n", x3.member) => NG
    fmt.Printf("%#v\n", x3.InternalMember()) // => "msg's world"

    var x4 Sender
    x4 = Msg{
        Member: "XXX4",
        // member: "xxx3",        => NG
    }
    fmt.Printf("%#v\n", x4.Say()) // => "XXX4:"

    // var x5 sender            => NG

    x6 := NewInternalSender()
    fmt.Printf("%#v\n", x6.Say()) // => "HELLO:world"

    fmt.Printf("%#v\n", A) // => "AAA"

    // fmt.Printf("%#v\n", a)         => NG
}

実行方法

コードが用意できていれば、以下のコマンドで実行可能です。

$ go run main.go

解説

main.go のコードについて解説します。

5 行目で少々ずるというか横着しています。
本来、 dir パッケージ内のリソースはすべて dir.Msg のように参照する必要がありますが、 . で別名を付けて import するとプレフィックス dir を省略できます。

9 行目は Msg 構造体を使ってインスタンス生成しています。
ここで Member フィールドは設定できるのですが、 member フィールドは小文字で定義されているため初期化のタイミングでも設定ができません。
このあとでもMember フィールドは読み書きできますが、 member フィールドは読み書きできません。

15 行目ですが、構造体で定義された InternalMember というメソッドを呼んでいます。
内部では member フィールドの内容を出力していますが、これは問題ありません。
サンプルコードでは member フィールドの値が空なので出力が欠落していますが、 func (m *Msg) SetInternalMember(m string) といったようなメソッドを用意し、member フィールドに値をセットすることも可能です。
ポイントは「自分ではフィールドにアクセスはできませんが、外部パッケージでアクセスしてもらうことはできる」という点です。

17 行目は msg 構造体のインスタンスを生成しようとしていますが、こちらは小文字で始まっている名称のためコンパイル時にエラーとなってしまいます。

19 行目は面白い例となっています。
NewInternalMsg 関数を呼び出しています。
NewInternalMsg 関数の中では msg 構造体のインスタンスを生成し返却していますが、これはコンパイルが通ります。
ポイントは「自分ではインスタンス生成はできませんが、外部パッケージで生成してもらうことはできる」という点です。
インスタンスさえ取得できればこっちのものです。
Member フィールドに読み書き可能となります。
ただし、やはり member フィールドに対しては読み書きできません。
構造体で定義された InternalMember というメソッドを呼んでやれば、 member フィールドの値を使うは可能です。
ポイントは Member 構造体のときと同様、「自分ではフィールドにアクセスはできませんが、外部パッケージでアクセスしてもらうことはできる」という点です。

24 行目はインタフェース変数を使った例です。
インタフェース Sender は大文字で始まっているので変数宣言時に型として指定が可能です。
当然変数内に登録した実体(Msg 構造体)メソッドも呼び出せます。

31 行目では sender というインタフェースの変数を定義しようとしていますが、こちらはコンパイルエラーとなります。
名前が小文字で始まっているため、 dir パッケージ外からは利用できません。

ただし、 33 行目のように関数経由で sender インタフェースの値を返却してもらうことはできます。
しかし、Go 言語のプラクティスとして NewInternalSender() のような関数はあまりよろしくないそうです。
Go のプラクティスとしては「インタフェースをもらい、構造体を返す」ような関数やメソッドを設計するべきだと書籍で読んだことがあります。

36 行目、 39 行目は改めて最もシンプルなケースを振り返ります。
パッケージ内で生成した変数のうち、大文字で始まるものはパッケージ外から参照可能です。
小文字で始まるものはパッケージ内からしか参照できません。
ただし、ここでは例を上げていませんが、 変数 a にアクセスする関数を用意すれば、読み書き可能となります。
ポイントはやはり、「自分では小文字で始まる変数にアクセスはできませんが、外部パッケージの関数、メソッドを経由しアクセスしてもらうことはできる」という点です。

ひとこと

Golang の外部パッケージについて理解を深めるコツは、パッケージ内のリソース参照に関して、外部パッケージのリソースにどのようなアクセス制限があるかを注意深く観察することです。
名前が大文字で始まるリソースはパッケージ外から参照可能である一方、小文字やアンダースコアで始まるリソースはパッケージ内でしか参照できないというルールがあります。
パッケージ内で定義されたリソースにアクセスするためには、関数やメソッドを用意して、それらを介してアクセスすることができます。

外部パッケージを利用する場合には、それらのパッケージが提供するインタフェースを使って、プラグインのように適宜実装することができます。
Golang でのパッケージの利用には、基本的な構文やパッケージシステム、外部パッケージと内部パッケージの違い、import 文の使い方、アクセス制御、データ型や関数、メソッド、ポインタの扱い方、コードの再利用に関する理解が必要です。
これらの知識に加えて、今回の記事では、外部パッケージに焦点を当て、サンプルコードを交えて説明しました。
パッケージの正しい扱い方を理解することで、効率的でシンプルなコードを書くことができるようになるでしょう。

Posted by genzouw