ゼロから学ぶGo言語プログラミング(3) Hello World

プログラミング言語Goをのんびり一から学ぶ続き。 appengineのVMにGoで書かれたDockerが採用されたりで、モチベーションさらに上がりました。

今回はようやくHello World

Go Playground

きっちりGoの開発環境を用意するのは後回しにして、まずはコードに触れていきたいので、ブラウザ上で手軽にGoが試せる「Go Playground」を利用します。

Go Playgroundは公式サイトの一部なので、最新のバージョンも追いかけてくれる筈。 当面はこの環境で言語仕様を確認したりします。

Hello World

Go Playgroundにアクセスすると、以下のHello World("World"でなく"playground"ですが)に当たるコードがデフォルトで入力されています。

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

Go Playgroundでこのまま「Run」すれば、画面下部の出力先に"Hello, playground"と出力されます。 favicongopherなので、gopherくんに挨拶されてる気分になりますね。

まずはこのコードをきちんと理解できるようになってから、あれこれ試します。

余談ですが、はてなブログシンタックスハイライトがGoに対応していて嬉しかったです。

package

1行目には、

package main

とあります。 packageという名前からして、プログラミングに使用するライブラリなど、何らかの塊を指定しているのだと思います。 そしてmainは、そのパッケージの名前でしょう。

Goのパッケージという概念についてきちんと確認するには、公開されている言語の仕様書を読む必要があります。

しかし、不満足な英語でこの手の文書を読むのは苦痛で、とっかかりとして良くありません。 幸い株式会社 エイベルが日本語訳を公開してくれているので、それを利用します。

まず、パッケージの概要。

Go言語のプログラムは、複数のパッケージをひとつにリンクすることによって作られます。さらに各パッケージは、そのパッケージに所属する定数、型、変数、関数を宣言しているひとつ以上のソースファイルから構成されます。これらパッケージ内の要素は、同一パッケージ内であれば別ファイルからアクセスすることができます。また要素がエクスポートされていれば、他パッケージからアクセスすることができます。

それから基本中の基本らしき記述。

各ソースファイルはパッケージ節で始まり、そのファイルが所属するパッケージを定めます。

リンクというのは、コンパイルする言語では耳にしますが、スクリプト言語でのimportのようなものでしょうか。 ここだけ読んでもまだ分かりません。

"package foo"という記述は、「パッケージ節」という扱いのようです。 節や文といった言語的な概念によって、具体的にプログラミング上何が変わってくるのか、これも確認が必要そうです。

重要っぽいのは、変数や関数などが「そのパッケージに所属」となっている点。 つまりパッケージは、名前空間であり、pythonでいうmoduleのようなものでもあるようです。 要素のエクスポートとある辺りも、pythonっぽい。

pythonは単一のファイルで書けばmodule意識しない作りも可能でしたが、"各ソースファイルはパッケージ節で始まり"とあるので、Goではpackage必須なんでしょう。 ということは、ワンライナーで何か書くのには向かない?…というか、スクリプトじゃないからワンライナーは考慮しなくていいのか。

他に、「ひとつ以上のソース・ファイル」となっているので、複数のファイルで単一のパッケージを構成できるようです。 パッケージが基本的な単位で、pythonのmoduleのような扱いなら、手軽に複数ファイルをまたげるのは楽で良さそう。

更にこんな記述がありました。

パッケージの実装は、同じパッケージ名を共有するファイル群によって構成されます。実装上、同一パッケージ内のすべてのソースファイルが、同一ディレクトリ内に置かれている必要があります。

同一とは、単一の階層を指しているんでしょうか。 であれば、foo/にfooパッケージを構成する全ファイルが入るとして、サブディレクトリの扱いはどうなるんでしょうか。 サブディレクトリはサブパッケージ? サブパッケージがfoo.barみたいにネストすると、更にpythonっぽくて分かりやすいんですが。

同一ディレクトリに異なるパッケージのファイルを置いた場合も、いまいちこの仕様書だけではわかりません。

package についてわかったこと

とりあえず今の時点では、packageに関して以下を理解したものとして、先に進みます。

  • Goではpackageという単位でプログラムを作成する
  • ソースファイルはpackage節で始まる
  • package節は"package パッケージ名"と書く
  • packageは他のパッケージからリンクされる
  • その際リンクした側に何を公開するかは選べる
  • packageは複数のファイルにまたがれる
  • 同じパッケージのソースファイルは同一ディレクトリに置く

package についてわからなかったこと

一方、次の点はこれからおいおい調べます。

  • リンクとは? スクリプト言語のimportとの違いは?
  • package節のpackage名は文字列型ではない?
  • 名前空間とpackageの関係
  • packageは常に必須か
  • packageでのエクスポート指定方法
  • 同一ディレクトリに異なるパッケージのソースファイルは混在可能か
  • packageのネストは可能か

packageに限らず、言語仕様はEBNF - Wikipediaという記法のメタ言語で色々書かれているようですが、Hello Worldには必須でも無し、細かい文法が問題になった場合のみ確認します。

main

前段でpackageという基本的な単位があることは理解できました。 次に、Hello Worldでのpackage名らしき、"main"。

このmainパッケージについて単独ではっきり取り上げているリソースが見つからなかったので、プログラムの初期化と実行 - Goプログラミング言語仕様 - golang.jpの後半部分の言及を参考にします。

mainパッケージと呼ばれる、他からインポートされることがないパッケージをリンクすることでプログラムは完成します。このmainパッケージから全てのパッケージが直接・間接的にインポートされます。mainパッケージは、パッケージ名はmainであり、かつ引数と戻り値を持たないmain関数を宣言していなければなりません。

ということで、mainパッケージは、プログラムに必ず必要な、その名の通り主となるパッケージを表す予約語のようです。 そしてmainパッケージは、同じくmainという名の関数も宣言必須。

おぼろげな記憶の中のCとかにおけるmain()に似てますが、Goのmainはパッケージと関数の2段階で必要です。

main についてわかったこと

  • Goのプログラムにはmainパッケージが必須
  • mainパッケージにはmain関数が必須
  • main関数は引数も戻り値も持ってはいけない

main についてわからなかったこと

  • mainパッケージまたはmain関数を宣言しないと何が起こる?
  • mainを異なる文字で置き換えることは可能か
  • mainが必須で面倒な点は?

import

よやく3行目、ただし空白行をはさんで実質2行目です。

import "fmt"

引き続き、言語仕様の日本語訳を参照します。

インポート宣言は、ソースファイル内に含まれている宣言がインポートしたパッケージ(§プログラムの初期化と実行)が持つ機能に依存していることを表し、かつそのパッケージでエクスポートされている識別子へのアクセスを可能にします。インポート名はアクセスに使用する識別子(PackageName)であり、ImportPathにはインポートされるパッケージを指定します。

そのプログラムが"依存"している他のパッケージを宣言し、インポートした側でどのような表現でアクセスするかを既定する。 これがimportのようです。

importの対象はパッケージ名ではなく、パスを指定するようです。 package節ではパッケージ名を指定していたため文字列型ではなかったんですが、importはパスなので文字列型になるっぽい。 相対パスになるようですが、その起点だとかコンパイルのことだとか、この手のパスの解決は鬼門が多いので、きちんと調べて抑えたほうが良さそう。

いわゆる as も用意されていて、これが"インポート名"として3種類紹介されています。

import   "lib/math"         math.Sin
import M "lib/math"         M.Sin
import . "lib/math"         Sin

当然一番上が基本ですが、2番めはインポートしたいパッケージ名によって、簡略化した名称などを使うかもしれません。 3番目の表記は、インポート対象のパッケージ名が失われてしまい、名前の衝突なども起きそうで、現時点ではあまりメリットを感じられません。 恐らく特定の用途には欠かせない、あるいはかなり楽の出来るケースがあるのでしょう。

こうやってimportされたパッケージは、何をimportした側に公開するのか、何らかの方法で決めているようです。 そして例えばfooパッケージをimportした際、fooがbar()を公開していれば、インポートした側では foo.bar() 記述して別パッケージの要素にアクセスできる。 ドット区切りである点も含め、こういった挙動はpythonに近く、すんなり入っていきます。 しかし、パッケージ内のサブパッケージなんていう考え方はまだ出てきません。

import についてわかったこと

  • importは依存するパッケージの宣言
  • 依存していないなら書かない
  • importしたいパッケージはパスで指定する
  • import時にインポートした側で使用するパッケージ名を指定できる
  • importしたパッケージの要素にアクセスするには foo.bar() のようにドットを用いる

import についてわからなかったこと

  • importのパスはどういう規則で解決されるか
  • import時の代替パッケージ名に"."を使用スべきケース
  • packageのネストはあるのかないのか

fmt

まだ3行目を抜けられません。

import "fmt"

今度はimportされるパッケージのパスを表すであろう、"fmt"。

パスに階層らしき記述が無い以上、これはfmtが直接パッケージ名を表すんでしょうが、一体どこからインポートしているのか。 Hello Worldという最初の最初のコードに出てくるのだし、一般的にいう組み込みライブラリのようなものだとは思いますが、Goでは「組み込みパッケージ」とでも呼ぶんでしょうか。

以下に標準の組み込みパッケージらしき一覧があります。

しかし、標準パッケージのインポート元だとか、パスについての仕様が見つけられませんでした。 この辺りは環境のセットアップに関わってくる部分であり、今はまだ触れていないので、一旦スルーしておきましょう。

fmt についてわかったこと

  • fmtは組み込みパッケージらしい
  • 具体的なパスを指定せずにimportできる

fmt についてわからなかったこと

  • 組み込みパッケージらしきもののパスの解決はどう行われるのか

func main()

やっと5行目。

func main() {

これは先程のmainで出てきました。 Goのプログラムは、mainパッケージとmain関数の実装が必須、となっていたあのmain関数ですね。

面白いのは、関数の宣言に"func"というキーワードを使用している点。 "function"でも"def"でもなく、"func"。 pythonの"def"は短くて好きですが、初見で「define…? default…?」とわずかに迷ったので、これで全然問題ないです。 シンプルにしようという思想の現れで、分かりやすい。

func main() についてわかったこと

  • 関数の宣言は"func"で

この時点では、わからなかったことはありません。

fmt.Println()

インポートしたらしいfmtは、6行目で以下のように使用されています。

   fmt.Println("Hello, playground")

fmt.Println()となっているので、fmtパッケージが持つPrintln()という関数を使用しているのでしょう。 引数は単純な文字列のようで、関数名から察するに、標準出力に改行付きで出力している、ということでしょうか。 なぜか関数名の先頭が大文字ですが、多分何かの規則によるものなので、後で確認し忘れないようにします。

fmtパッケージについては、以下に詳細な仕様がありました。

fmtパッケージは、フォーマットI/Oを実装しており、C言語のprintfおよびscanfと似た関数を持ちます。フォーマットの「書式」はC言語から派生していますが、より単純化されています。

"フォーマットI/O"とは、何らかのフォーマットに基づいたインプット・アウトプットということでしょうか。 推測した用途より、fmtパッケージ自体の用途はもう少し幅広いようです。

Println関数の部分を見ると、概ね予想した通りですが、気になる部分があります。

この関数は、書き込んだバイト数と発生した書き込みエラーを返します。

Hello Worldコードでは、この戻り値は無視されているようですが、バイト数とエラーの両方を返す関数とはどういうことでしょうか。 pythonのように、関数から気軽に複数の戻り値を返せるということかも知れません。 あまりこの複数の戻り値という仕組みに慣れていないので、うまく慣れていく必要がありそうです。 しかし、エラーも戻り値で返すというのは、エラー処理をどうやれば良いのか、ぱっとイメージできません。

fmt.Println() についてわかったこと

  • fmtは何らかのフォーマットのI/Oを担うパッケージ
  • Goの関数は複数の戻り値を持つ
  • 戻り値にエラーが含まれることがある

fmt.Println() についてわからなかったこと

  • 複数の戻り値の受け方
  • 複数の戻り値の返し方
  • 戻り値のエラー情報を使ったエラー制御

Hello Worldまとめ

たった7行のコードですが、ひとつひとつ理解して進めようとすると、なかなか時間がかかります。 でも、これまでこういう進め方をしてこなかったロスを取り戻せているようで、かなり楽しいです。

次は、もう少し基本的な文法などを見ていきます。