CLOVER🍀

That was when it all began.

Goで定数や関数定義などをパッケージ外からアクセスしたい(エクスポートしたい)場合、名前を大文字で始める必要があるという話

これは、なにをしたくて書いたもの?

プログラムを書く時に、Goのソースコードやライブラリのソースコードを見つつ、「これ使ったらいいのかな?」と使おうとすると
アクセスできないこととかがあり。

そういえば、Goのスコープ的な話、知らないですね。特に、パッケージ外からのアクセスが気になるところです。

この機会に押さえておこうかな、と。

Goとスコープ

こういう話を見るのであれば、やはり言語仕様でしょうか。

The Go Programming Language Specification / Declarations and scope

関数やブロック({})を使ったスコープは、良いかなぁと。

ポイントは、ここです。

Exported identifiers

識別子のエクスポートですね。別のパッケージからのアクセスを許可するかどうか、です。

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文字が大文字であること、のようです。

この状態を、"エクスポートした"というみたいですね。

構造体のフィールドや、インターフェースのメソッドも含まれます。

Struct types

Interface types

名前が重要だったんですね…。

というわけで、少し動作確認したいと思います。

環境

今回の環境は、こちらです。

$ 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
}

メソッド。Stringageへのアクセスを含み、エクスポートされたメソッド経由であれば問題なく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

これで、パッケージ外からアクセスできる、できない条件を把握できたかな、と思います。