Edited at

Goの構造体をpackageに外出ししてハマッたこと

More than 1 year has passed since last update.


まえがき

Go(golang)勉強中の身ゆえ、学習中にハマッたり自己解決できたり気がついた点をメモしていきます。


概要

Goで構造体(struct)を使ったプログラムを書いていたときに、ある部分をパッケージ(package)に外出しにしようと再実装したら、なぜかコンパイルエラーが出てしまいました。

理由は単純でしたが、案外ハマりどころかと思い、メモを書いてみます。

さらに、実は構造体(だけ)の話だけではなかったという基本的なコトでした。キーワードは「識別子のスコープ」です。


要約

読んでる時間のない方に向けて以下の記事を要約すると、以下のようになります。


  • 別のpackageから参照させたい変数名や関数名、フィールド名(構造体内の変数名)などの識別子名は、最初の文字を大文字で始める必要がある。

  • 小文字で始まる識別子名は、定義されたpackage内でのみ使用可能である。

以下で簡単に確認をしていきます。


構造体を使う

私の場合は構造体を他のpackageで使うところで躓いたので、構造体を例に確認してみます。

最初はごく普通に、同じソースファイル内にstructを書いていて、C/C++などと同様の概念で当然のように動くのだと感じていました。


main.go

package main

import "fmt"

type mydata struct {
num int
str string
}

func main() {
var x mydata
x.num = 10
x.str = "something"
fmt.Printf("x.num=%d, x.str=%s\n", x.num, x.str)
}


これを実行しますと、想像通りの出力が行われました。


インタプリタで実行

% go run main.go

x.num=10, x.str=something


別packageの構造体を使う

構造体を、dataというpackageにしてから、main.goでその構造体を使うことにしました。

dataディレクトリを作成し、data/data.goというファイルに構造体を移行します。


dataディレクトリの作成

% mkdir data


data/data.goの実装は以下のようになります。構造体自体は、さきほどmain.goで書いていた内容とまったく変わりません。


data/data.go

package data

type mydata struct {
num int
str string
}


packageに合わせて、main.goも書き換えます。


main.go

package main

import (
"fmt"
"./data"
)

func main() {
var x data.mydata
x.num = 20
x.str = "anything"
fmt.Printf("x.num=%d, x.str=%s\n", x.num, x.str)
}


実行すると、以下のようなエラーになります。

data.mydataが参照できないと言われており、したがってdataパッケージ内のmydata構造体が未定義(undefined)と処理されました。


実行エラーになる

% go run main.go

# command-line-arguments
./main.go:9: cannot refer to unexported name data.mydata
./main.go:9: undefined: data.mydata

なぜ正しく動かないのかがしばらくわからず、ウェブでいろいろ調べていたら「スコープ」という言葉がひっかかり、ハタと気がついてGo言語仕様の公式ドキュメントにあるExported Identifiersの項目を見て合点がいきました。


Exported identifiers

An identifier may be exported to permit access to it from another package. An identifier is exported if both:


  1. the first character of the identifier's name is a Unicode upper case letter (Unicode class "Lu"); and

  2. the identifier is declared in the package block or it is a field name or method name.

All other identifiers are not exported.


英語は得意じゃないですが、いちおう翻訳努力を…。なおexported identifiersは、import宣言子でインポートするパッケージとして外出しされた識別子、と考えればいいと思います。


エクスポートされた識別子

識別子は、他のパッケージからのアクセスを許可するエクスポートが可能である。以下の両方を満たしていれば、識別子はエクスポートされる:


  1. 識別子名の最初の文字がユニコード大文字(Unicodeクラスの"Lu")であり、

  2. 識別子がpackageブロック内で宣言されているか、フィールド名もしくはメソッド名であること。

これ以外のすべての識別子はエクスポートされない。


ここで「識別子名」とは、定数名、変数名、関数名、型名(構造体名)などを指します。

Goの言語仕様における基本事項だったので、しっかり言語仕様を把握しなければと反省しつつ、さきほどのプログラムを改修します。


大文字フィールド名で構造体を再定義

構造体名とフィールド名の最初の文字を大文字にしただけのプログラムが以下になります。大文字にする以外は何も変更はありません。


main.go

package main

import (
"fmt"
"./data"
)

func main() {
var x data.Mydata
x.Num = 10
x.Str = "something"
fmt.Printf("x.Num=%d, x.Str=%s\n", x.Num, x.Str)
}


構造体名だけでなく、フィールド名もスコープの対象であることに注意します。


data/data.go

package data

type Mydata struct {
Num int
Str string
}


さて、実行しましょう。


改修したコードで実行

go run main.go

x.Num=10, x.Str=something

無事、構造体をpackageに外出し(エクスポート)することができました。


まとめと感想

「要約」でも書きましたが、あらためてまとめておきます。


  • 識別子(定数名、変数名、関数名、型名(構造体名)など)にはスコープ(参照可能範囲)がある。

  • あるpackageで定義された識別子を外部参照させる場合は、大文字で始める必要がある。

  • それ以外の識別子のスコープは他のpackageから参照できない。

どのプログラミング言語でも、言語仕様の把握は大切だと改めて感じた一件でした。

package以外のスコープについても再確認をしようと思います。


感想追記

Goにはクラスの概念がないですが、packageと構造体とメソッドで似たような実装が可能だとは思います。

識別子名の最初の文字を大文字と小文字でのスコープ分けは、他の言語でいうところのアクセス指定子(publicかprivate)の意味合いなのだという理解をしています。