これは、なにをしたくて書いたもの?
プログラムを書く時に、Goのソースコードやライブラリのソースコードを見つつ、「これ使ったらいいのかな?」と使おうとすると
アクセスできないこととかがあり。
そういえば、Goのスコープ的な話、知らないですね。特に、パッケージ外からのアクセスが気になるところです。
この機会に押さえておこうかな、と。
Goとスコープ
こういう話を見るのであれば、やはり言語仕様でしょうか。
The Go Programming Language Specification / Declarations and scope
関数やブロック({}
)を使ったスコープは、良いかなぁと。
ポイントは、ここです。
識別子のエクスポートですね。別のパッケージからのアクセスを許可するかどうか、です。
Goでパッケージ内で定義したものが以下の条件の両方を満たす場合、パッケージ外からアクセス可能です。
- the first character of the identifier's name is a Unicode upper case letter (Unicode class "Lu"); and
the identifier is declared in the package block or it is a field name or method name.
識別子の最初の文字が大文字であること
- 識別子がパッケージブロック、またはフィールド、メソッドであること
要するに、トップレベルで宣言したものであり、その識別子の名前の最初の1文字が大文字であること、のようです。
この状態を、"エクスポートした"というみたいですね。
構造体のフィールドや、インターフェースのメソッドも含まれます。
名前が重要だったんですね…。
というわけで、少し動作確認したいと思います。
環境
今回の環境は、こちらです。
$ go version go version go1.15.6 linux/amd64
モジュール作成。
$ go mod init export-identifiers go: creating new go.mod: module export-identifiers
go.mod
module export-identifiers go 1.15 require github.com/stretchr/testify v1.7.0
確認は、テストコードで行います。
確認対象のコード
ややわざとしいですが、アクセス制御確認のためのコードとして、以下を用意。
mydata/types.go
package mydata import ( "fmt" ) const ( INT_CONST_VAL = 100 STR_CONST_VAL = "Hello World as Constant" private_int_const_val = 5 private_string_const_val = "wow" ) var ( IntVar = 1000 StrVar = "Hello World as Variable" privateIntVar = 2000 privateStrVar = "wow" ) func GetMessage() string { return "Hello World" } func privateFunc() string { return "wow" } type Person struct { FirstName string LastName string age int } func (p *Person) String() string { return fmt.Sprintf("name: %s %s, age: %d", p.FirstName, p.LastName, p.age) } func (p *Person) GetName() string { return p.FirstName + " " + p.LastName } func (p *Person) SetAge(age int) { p.age = age } func (p *Person) getAge() int { return p.age } type privatePerson struct { FirstName string LastName string age int }
定数と変数。大文字で始まるものはパッケージ外からもアクセスでき、そうでないものはアクセスできない想定です。
const ( INT_CONST_VAL = 100 STR_CONST_VAL = "Hello World as Constant" private_int_const_val = 5 private_string_const_val = "wow" ) var ( IntVar = 1000 StrVar = "Hello World as Variable" privateIntVar = 2000 privateStrVar = "wow" )
関数。
func GetMessage() string { return "Hello World" } func privateFunc() string { return "wow" }
構造体。Person
はパッケージ外からもアクセスできる構造体ですが、フィールドage
はパッケージ外からはアクセスできない
はずです。
type Person struct { FirstName string LastName string age int } type privatePerson struct { FirstName string LastName string age int }
メソッド。String
はage
へのアクセスを含み、エクスポートされたメソッド経由であれば問題なくage
にアクセスできることを
確認します。
func (p *Person) String() string { return fmt.Sprintf("name: %s %s, age: %d", p.FirstName, p.LastName, p.age) } func (p *Person) GetName() string { return p.FirstName + " " + p.LastName } func (p *Person) SetAge(age int) { p.age = age } func (p *Person) getAge() int { return p.age }
こちらに対して、アクセスを行うテストコードを書いていきます。
このコードはmydata
パッケージとして用意しましたが、
package mydata
テストコードはmain
パッケージに配置します。
エクスポートされた定数や関数などにアクセスする
それでは、テストコードを書きましょう。こんな感じで用意。
main_test.go
package main import ( "export-identifiers/mydata" "github.com/stretchr/testify/assert" "testing" ) func TestAccessExportedIConstantsVars(t *testing.T) { assert.Equal(t, 100, mydata.INT_CONST_VAL) assert.Equal(t, "Hello World as Constant", mydata.STR_CONST_VAL) assert.Equal(t, 1000, mydata.IntVar) assert.Equal(t, "Hello World as Variable", mydata.StrVar) } func TestAccessExportedFunctionStructure(t *testing.T) { assert.Equal(t, "Hello World", mydata.GetMessage()) p := &mydata.Person{FirstName: "カツオ", LastName: "磯野"} p.SetAge(11) assert.Equal(t, "カツオ", p.FirstName) assert.Equal(t, "磯野", p.LastName) assert.Equal(t, "カツオ 磯野", p.GetName()) assert.Equal(t, "name: カツオ 磯野, age: 11", p.String()) }
いずれも、問題なくアクセスできます。
いずれも、大文字から始まっている定義にはアクセスできています。
こちらの初期化時点では、(エクスポートされていないから)age
を含められないことに注意です。
p := &mydata.Person{FirstName: "カツオ", LastName: "磯野"}
エクスポートされていない定数や関数などにアクセスしてみる
続いて、エクスポートされていないものにアクセスするコードを書いてみましょう。
main_failure_test.go
package main import ( "export-identifiers/mydata" "github.com/stretchr/testify/assert" "testing" ) func TestAccessPrivateConstantsVars(t *testing.T) { assert.Equal(t, 5, mydata.private_int_const_val) assert.Equal(t, "Hello World as Constant", mydata.private_string_const_val) assert.Equal(t, 2000, mydata.privateIntVar) assert.Equal(t, "wow", mydata.privateStrVar) } func TestAccessPrivateFunctionStructure(t *testing.T) { assert.Equal(t, "wow", mydata.privateFunc()) p := &mydata.Person{FirstName: "カツオ", LastName: "磯野", age: 11} p.age = 11 assert.Equal(t, 11, p.getAge()) ip := &mydata.privatePerson{FirstName: "カツオ", LastName: "磯野"} }
実行。
$ go test -gcflags=-e main_failure_test.go # command-line-arguments [command-line-arguments.test] ./main_failure_test.go:10:21: cannot refer to unexported name mydata.private_int_const_val ./main_failure_test.go:10:21: undefined: mydata.private_int_const_val ./main_failure_test.go:11:45: cannot refer to unexported name mydata.private_string_const_val ./main_failure_test.go:11:45: undefined: mydata.private_string_const_val ./main_failure_test.go:13:24: cannot refer to unexported name mydata.privateIntVar ./main_failure_test.go:13:24: undefined: mydata.privateIntVar ./main_failure_test.go:14:25: cannot refer to unexported name mydata.privateStrVar ./main_failure_test.go:14:25: undefined: mydata.privateStrVar ./main_failure_test.go:18:25: cannot refer to unexported name mydata.privateFunc ./main_failure_test.go:18:25: undefined: mydata.privateFunc ./main_failure_test.go:20:66: cannot refer to unexported field 'age' in struct literal of type mydata.Person ./main_failure_test.go:21:3: p.age undefined (cannot refer to unexported field or method age) ./main_failure_test.go:23:23: p.getAge undefined (cannot refer to unexported field or method mydata.(*Person).getAge) ./main_failure_test.go:25:9: cannot refer to unexported name mydata.privatePerson ./main_failure_test.go:25:9: undefined: mydata.privatePerson FAIL command-line-arguments [build failed] FAIL
いずれも、エクスポートされていない名前を参照できない、と言われています。
cannot refer to unexported name
アクセスしようとしたもの、すべて怒られていますね。
こちらのコードはビルド自体が通らず、またエラーが多くて省略されるので-gcflags=-e
オプションを付与しています。
-gcflags=-e
をつけない場合、too many errors
と言われて途中でエラーが省略されます。
$ go test main_failure_test.go # command-line-arguments [command-line-arguments.test] ./main_failure_test.go:10:21: cannot refer to unexported name mydata.private_int_const_val ./main_failure_test.go:10:21: undefined: mydata.private_int_const_val ./main_failure_test.go:11:45: cannot refer to unexported name mydata.private_string_const_val ./main_failure_test.go:11:45: undefined: mydata.private_string_const_val ./main_failure_test.go:13:24: cannot refer to unexported name mydata.privateIntVar ./main_failure_test.go:13:24: undefined: mydata.privateIntVar ./main_failure_test.go:14:25: cannot refer to unexported name mydata.privateStrVar ./main_failure_test.go:14:25: undefined: mydata.privateStrVar ./main_failure_test.go:18:25: cannot refer to unexported name mydata.privateFunc ./main_failure_test.go:25:9: cannot refer to unexported name mydata.privatePerson ./main_failure_test.go:14:25: too many errors FAIL command-line-arguments [build failed] FAIL
これで、パッケージ外からアクセスできる、できない条件を把握できたかな、と思います。