go言語の名前空間について

package宣言における名前付けのアプローチが、goとJavaでは考え方が随分違っていて面食らったので忘れないうちに書いておきます。

go言語のテンプレートAPI

go言語にはテンプレーティングするAPIが標準で二種類あります。

Templateというstructがそこらじゅうで出てくるのですけども、何か混ざったような印象を受けるのです。

html/templateのAPIはtext/templateを利用して実装しているので、ある種の重複は当然あってしかるべきなのですけども、何か違和感がありました。

コードを引っ張りだしてみます。

package template

import (
    "fmt"
    "reflect"
    "text/template/parse"
)

// Template is the representation of a parsed template. The *parse.Tree
// field is exported only for use by html/template and should be treated
// as unexported by all other clients.
type Template struct {
    name string
    *parse.Tree
    *common
    leftDelim  string
    rightDelim string
}
package template

import (
    "fmt"
    "io"
    "io/ioutil"
    "path/filepath"
    "sync"
    "text/template"
    "text/template/parse"
)

// Template is a specialized Template from "text/template" that produces a safe
// HTML document fragment.
type Template struct {
    escaped bool
    // We could embed the text/template field, but it's safer not to because
    // we need to keep our version of the name space and the underlying
    // template's in sync.
    text *template.Template
    // The underlying template's parse tree, updated to be HTML-safe.
    Tree       *parse.Tree
    *nameSpace // common to all associated templates
}

何とディレクトリパスが違うのにpackageで宣言されている名前は同じなのです。

多分、きっとjavaならディレクトリ階層を名前空間に反映させる必要がありますのでtext.templateとかhtml.template等と言う名前になるでしょう。ちなみに、go言語では識別子に.(ドット)は使えません。

go言語のpackage宣言は絶対的なものではない

go言語におけるpackage宣言は、それをimportする側にとっては単にデフォルトの名前であって何かあれば好きな様に変えられるものを宣言するのです。

これは、それぞれのパッケージをimportするコードを書くと分かります。

まずは、シンプルにhtml/templateだけをimportするコード。

package main

import (
    "html/template"
    "os"
)

func main() {
    type Hoge struct {
        Name string
    }
    v := &Hoge{"Joe<script>alert('hoge')</script>"}
    const letter = "Dear {{.Name}}\n"
    handle(template.Must(template.New("hoge").Parse(letter)).Execute(os.Stdout, v))
}

func handle(e error) {
    if e != nil {
        panic(e)
    }
}

ここで、登場するtemplateが肝ですね。html/templateには、package templateとあるのでimportするとデフォルトで、templateが対象リソースをmain.goに取り込んだ際に使える名前空間になります。

では、text/templatehtml/templateを同時に使う場合、どうなるのでしょうか。

import する際に利用する名前を利用者側が指定します。つまり、こういうコードになります。

package main

import (
    ht "html/template"
    "os"
    tt "text/template"
)

func main() {
    type Hoge struct {
        Name string
    }
    v := &Hoge{"Joe<script>alert('hoge')</script>"}
    const letter = "Dear {{.Name}}\n"
    handle(tt.Must(tt.New("hoge").Parse(letter)).Execute(os.Stdout, v))
    handle(ht.Must(ht.New("moge").Parse(letter)).Execute(os.Stdout, v))
}

func handle(e error) {
    if e != nil {
        panic(e)
    }
}

名前空間に対する考え方の違いについて

Javaでは慣習としてドメイン名を反転してパッケージ名の一部に含める様になっています。FQDNで表すとcom.example.system.HogeControllerの様にウンザリする程長い名前のクラスがそこらじゅうに転がっています。

今回の様に、複数のパッケージに同じ名前のクラスが存在する場合、Javaでは少々面倒な事になります。

javaで継承の仕組みを使わずに似た様なコードを書くと、こんな風になるでしょう。

public class Main {
    public static void main(String[] args) {
        class Hoge {
            String name;
        }

        Hoge v = new Hoge();
        v.name = "Joe<script>alert('hoge')</script>";
        String letter = "Dear {{.Name}}\n";
        new html.template.Template("hoge").parse(letter).Execute(System.out, v);
        new text.template.Template("moge").parse(letter).Execute(System.out, v);
    }
}

ここで問題にしたいのは、名前空間を指定するやり方です。javaではクラス名が重複する際、FQDNを使って厳密にどちらを利用するのか指定します。

これは、最初に宣言されたパッケージ名が出来る限り尊重されるのであるから、パッケージ名を決める人は慎重に名前を選択して下さい、という事です。

つまり、Javaではpackage宣言は大きな慎重さを必要とされる宣言なのです。

それに対してgo言語では名前が衝突するならimport時に、そのソースコード内でユニークになる様に参照する名前を変えて下さい、というやり方です。

これは、最初に宣言されたパッケージ名は、それ程尊重されないという事です。例え不都合な名前が付けられていたとしても、importする側がより妥当な名前を付ければいいのです。

つまり、goのpackage宣言は大胆にそれぞれが分かり易いシンプルな名前を付けておけば良いのです。

最後に、Effective Goの当該部分を引用しておきます。

The package name is only the default name for imports; it need not be unique across all source code, and in the rare case of a collision the importing package can choose a different name to use locally. In any case, confusion is rare because the file name in the import determines just which package is being used.

javaっぽい名前付けについては明確に否定しています。

Another convention is that the package name is the base name of its source directory; the package in src/pkg/encoding/base64 is imported as “encoding/base64” but has name base64, not encoding_base64 and not encodingBase64.

現状のコードに対する疑問メモ

今回の話題とは直接関係ないのですけども、text/template/helper.goの中身が、ごそっとhtml/template/template.go#L278辺りにコピペされている様に見えるんですけども、ちょっとダサくないですか?

参考資料