🎃

GoQuiz: importを書いてよい場所はどこ?

2020/12/11に公開

Goクイズ Advent Calendar 2020 の 11日目です

下記のGoコードはコンパイルできるでしょうか?

できる/できないの理由も考えてみてください。

package main

import "fmt"

func main() {
	fmt.Println(strconv.Itoa(123))
}

import "strconv"

ちなみに、他の言語 (C/Perl/PHP) ではこのようなコードは実行できます。

C

#include <stdio.h>

int main() {
    printf("hello\n");
}

#include <stdlib.h>

PHP

<?php
use Foo\Bar;

echo "hello\n";

use Buz\Buz;

Perl

use HTTP::Request;

print("hello\n");

use HTTP::Response;

正解は下の方で






正解

コンパイルできない

PlayGround で実行してみる → https://play.golang.org/p/IoB40WI4vpq

解説

Go言語仕様の "Source file organization" という項で、ファイルの文法的構成が定められています。

https://golang.org/ref/spec#Source_file_organization

Each source file consists of a package clause defining the package to which it belongs, followed by a possibly empty set of import declarations that declare packages whose contents it wishes to use, followed by a possibly empty set of declarations of functions, types, variables, and constants.

「import宣言の後にfunctions, types, variables, and constantsの宣言が来る」と明示的に書かれています。

EBNFでもこのように定義されています。

SourceFile       = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

(TopLevelDecl は functions, types, variables, constants宣言の総称です)

「importが下にあるのは見たことないからダメだと思っていた」という方も多いかと思いますが、「言語仕様のここでこのように定められてるから」ときっぱり言えるようになると気持ちいいかもしれませんね。

パーサの実装を見てみる

公式パッケージの go/parser を見てみると、このようになっています。

https://github.com/golang/go/blob/e012d0dc34e0c182aed605347fb19c6980b3f8bd/src/go/parser/parser.go#L2554-L2563

		for p.tok == token.IMPORT {
			decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
		}

		if p.mode&ImportsOnly == 0 {
			// rest of package body
			for p.tok != token.EOF {
				decls = append(decls, p.parseDecl(declStart))
			}
		}
  • ファイルの中身を上から順に読んでいく
  • 行頭の単語が "import" であれば、import 宣言であるとみなして中身を解析
  • 次の行頭も "import" であれば、上記を繰り返す
  • 行頭が "import" でなかったら、「トップレベル宣言解析フェーズ」に突入してトップレベル宣言(functions, types, variables, constants)を読んでいく

のような処理になっています。

また、ImportsOnly モードというものがあって、そのモードの場合は「トップレベル宣言解析フェーズ」に入らずに構文解析を終えていることが見てとれます。 (パッケージ依存だけを調べたいときに使う)

なぜこのような仕様になっているのか

もし、他の言語のようにファイルの途中や下方で import が宣言できるようにしてしまうと、
ファイルの末尾まで全部構文解析をしないと依存先のパッケージリストを収集できなくなります。
import をファイル上部だけと決めておけば、ファイルの冒頭部分のみを読めばよいので処理時間が短くてすみます。

他にもパーサ実装者の視点としては、ファイルの冒頭に import "x" がある場合は x.Yパッケージ名.識別子 であってオブジェクト.フィールド (つまりSelector)でないことが明確なので、デバグなどがしやすいです。

つまり構文解析が簡単になるようにこのような仕様にしたのだと思われます。

Go言語の仕様は、このように随所でパーサにやさしくなるような工夫が散りばめられているので、とても良い言語ですね。

Discussion