Golangの外部パッケージを使いこなそう!アクセス制限のポイントを解説
はじめに
こんにちは、Golang プログラマの皆さん。
今回の記事では、Golang の外部パッケージの少し複雑な例について、サンプルコードを交えて説明します。
Golang のパッケージに関する知識を深めることで、より一層効率的でシンプルなコードを書けるようになると思います。
本記事の対象読者
Golang の基本的な構文とパッケージシステムについては理解していることを前提としています。
本記事を読む前に以下の事柄については把握していることをお勧めします。
- 外部・内部パッケージの違い
- アクセス制御の仕組み
- import 文の使い方
- 基本的なデータ型と関数・メソッドの使い方
これらの前提知識に加え、次のエントリでは、特に外部パッケージに焦点を当てて詳細に説明しています。
検証環境
$ 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.go
が sub.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 文の使い方、アクセス制御、データ型や関数、メソッド、ポインタの扱い方、コードの再利用に関する理解が必要です。
これらの知識に加えて、今回の記事では、外部パッケージに焦点を当て、サンプルコードを交えて説明しました。
パッケージの正しい扱い方を理解することで、効率的でシンプルなコードを書くことができるようになるでしょう。
ディスカッション
コメント一覧
まだ、コメントがありません