さすがに本の内容を書くのはアカンので、5節区切りくらいずつ、本を読み進めるにあたって必要なGo言語の知識を説明していきたいと思います。
※ 思ったより1.3節がしんどかったので、該当節で記事を分割することにしました
同じような勉強の仕方の人に役立てば良いなと思います。
書籍の要約
「900ページにも及ぶコンパイラについての書籍」と、「50行のRubyコードでLispインタプリタを実装する方法に関するブログ記事」との間にあるような書籍
↑著者前書きより。
言い得て妙だと思います。
種々あるインタプリタの中でも、tree-walking型のインタプリタを実装していく書籍です。
僕は現時点でまだ1章を読み終わっただけですが、REPLができて普通に感動しました。
だってまだ400行もコード書いてない。
まだまだたくさんコードを書くことになりますが、これは楽しい本です。
普段書いてるプログラミング言語が、裏側でどんな実装をされているのか、どういう解釈をしているのか。
それが少しでも分かるんじゃないかと思ってワクワクしてます。
少しでも気になったら、ぜひ皆様Amazonでポチるなりなんなりしてください。
では、ここからはただのGo言語の記事になります。
基本的に1節ごとに、新出の言語的仕様等々を羅列していくつもりです。
皆さんが勉強するときの一助になれば嬉しいです。
1.1
いきなりですがここではGoコードが登場しないので、とりあえず環境構築手順を載せておきます。
なお、僕が使用しているGoのバージョンは下記のとおりです。
$ go version
go version go1.13.7 darwin/amd64
この記事シリーズではGo1.13以降を使用している前提で進めていきます。
まずはGoをインストールしましょう。
このページからインストールできるはずです。
完了したらターミナルを開き、go
とだけタイプ。
下記のような出力がされればインストールOK
$ go
Go is a tool for managing Go source code.
Usage:
go <command> [arguments]
The commands are:
bug start a bug report
build compile packages and dependencies
clean remove object files and cached files
......
後はお好みのテキストエディタがあれば良いです。
VSCodeなりAtomなりSublimeなり、何でもよいです。
僕はVSCodeを使ってます。
VSCodeならとりあえずms-vscode.go
エクステンションをインストールして、settingsでGo: Use Language Server
をOFFにしておけば良いと思います。
Language Serverをオフにするのは、どうやらgolintと相性が悪いっぽいため。
ファイルを保存してもlintエラーが消えないという。。。
1.2
本書ではほんとにさらっとGoでのコーディングが始まります笑
Go初めて触る方はちょっとでも予習しておくと入りやすいと思われます。
英語ドキュメントでおkな方はこちらで。
用語の整理から始めましょう。 (ほぼ翻訳ですが。。)
package
Goのプログラムは複数のpackage
で構成されます。
package
とは、同じディレクトリ内にあり、まとめてコンパイルされるソースファイルの一群のことです。
例えば下記で言うならtoken
が一つのpackageです。
project-root
├── go.mod
└── token
└── token.go
package内の特定のソースファイルで宣言された関数、型、変数、定数などは、同じpackage内にあるソースファイルから「見える」状態になります。
module
module
は、協調して動くGoのpackage群のまとまりを表します。
「協調して動くGoのpackage群のまとまり」 = 一つのアプリケーションと言ってしまって良いと思います。
プロジェクトルートにあるgo.mod
ファイルにはモジュールのパスが宣言されており、これは、同じmodule内に存在する全てのpackageをimport
(次節で説明します)するためのprefixとなります。
moduleは、go.mod
と同階層に存在するpackageに加え、さらにその子階層、また別のgo.mod
ファイルを持つ更なる子階層をも包含します。
また、moduleのパスはimportのためのprefixとなるだけではなく、go
コマンドでそのmoduleをダウンロードする際にどこを探しに行けばいいかを示すものでもあります。
例えばgolang.org/x/tools
というmoduleをダウンロードするにあたって、go
コマンドはhttps://golang.org/x/tools
を探しに行くことになります。
import
import
は、あるソースファイルから別のpackageを参照し、そのpackage内のコードを再利用するための記述です。
packageのimportパスは、「そのpackageが含まれるmoduleのパス」と、「moduleの子階層のパス」をつなげた文字列となります。
例えばgithub.com/google/go-cmp
というmoduleは、その子階層であるcmp/
にpackageを持っています。
その場合、そのpackageのimportパスはgithub.com/google/go-cmp/cmp
となります。
なお、Go本体に備わっている標準ライブラリはmoduleパスprefixは必要ありません。
Goでコードを書く
では実際にコードを書くにあたってどうするのか。
moduleを作る
まずは自分のmoduleを作るところからです。
ターミナルでローカルマシンの適当な場所にcd
して、プロジェクト用のディレクトリを作成しましょう。
$ mkdir go-interpreter
で、さらに
$ cd go-interpreter
$ go mod init <YOUR_MODULE_PATH>
を行います。
moduleのパスは、githubアカウントをお持ちの方であればgithub.com/<username>/go-interpreter
で良いと思います。
プロジェクトルートにgo.mod
が出来上がりましたか?
中身は下記のようになっているはずです。
module <YOUR_MODULE_PATH>
go 1.13
これでmoduleが出来上がりました!
packageを作る
では最初のpackageを作ります。
書籍に沿って、token
packageを作成しましょう。
プロジェクトルートで下記コマンドを実行。
$ mkdir token && cd token && touch token.go
token.goができたら自分のテキストエディタで開き、下記を入力します。
package token
これでパッケージも出来上がり!
簡単ですね
ここからは具体的なコードは書籍を買っていただくとして、Goの文法的な話をしていきます。
type (型)
Goは「静的型付き言語」に分類されます。
Wikipediaの説明がわかりやすいので引用します。
静的型付け(せいてきかたづけ、英: static typing)とは、プログラミング言語で書かれたプログラムにおいて、変数や、サブルーチンの引数や返り値などの値について、その型が、コンパイル時など、そのプログラムの実行よりも前にあらかじめ決められている、という型システムの性質のことである。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
普段JSを書いている人などであれば、型違いによるランタイムエラーに悩まされたことが幾度もあると思います。
これはJSが「動的型付け」な言語であり、プログラム実行時に変数や関数の返り値の型がコロコロ変わりうるためです。
静的型付け言語の場合は、プログラム実行以前に型が確定しているため、上記のような理由でのランタイムエラーは起こりづらくなります。
さて、Go言語におけるtype
キーワードは、「型名」を「型」に紐付けることができます。
具体例で見たほうが良いでしょう。
type MyString string
これは、string
と同じ性質を持ったMyString
という名前の型を宣言しています。
MyString
は、その元となったstring
と中身は一緒ですが、型としては全く別物として生成されます。
なお、別物として生成しない「エイリアス宣言」という方法もありますが、冗長になるのでここでは割愛します。
struct (構造体)
Go言語におけるstruct
とは、フィールド
を寄せ集めた「type(型)」です。
ここでいうフィールドとは、「型を持った名前付きの要素」です。
structはtypeの一種なので、上述のstring
と同様に型宣言ができます。
type MyStruct struct {
Field1 int
Field2 string
}
これは、int
型のField1とstring
型のField2を持ったMyStruct
という名前の型を宣言しています。
const (定数)
const
は定数を宣言するためのキーワードです。
const
宣言は、任意の識別子(名前)を特定の値に束縛することができます。
例えば、下記ではMyConst
という名前で、中身が"This is const"
の定数が宣言されています。
const MyConst = "This is const"
この時、明確に型が宣言されていないため、MyConst
という定数の方は"This is const"
からstring
であると自動的に推論されます。
もちろん明示的に宣言することも可能です。
const MyConst string = "This is an explicitly declared string constant."
さらに、同時に複数の定数を宣言したい場合は下記のように書くことができます。
const (
MyConst1 string = "String value"
MyConst2 int = 1
MyConst3 = 2
MyConst4 string = "String value again"
)
このように、ある定数にはstring
、別の定数にはint
を明示的に強制しつつ、さらに型を明示しない定数を含めてまとめて宣言することもできます。
コメント
ここは界隈で宗派が分かれるところではあるのですが、Goではpackageのドキュメンテーションのためにコメントを書くことが推奨されています。
個人的には、考え抜かれたコメントはコードよりもモノを言うと思っているので、非常に良いと思います。
さて、なぜコメントの話をしているかと言うと、
皆さんの環境次第ではあると思いますが、ここまでコードを書いてきた中でコード内に黄色ないしその他の色の下線が出ている部分がありませんでしょうか。
おそらくですが、Goのlinterに下記のように怒られていませんか?
exported type MyStruct should have comment or be unexported
(エクスポートされる型には)commentをつけろよデコ助野郎
要は外部から利用されうるモノには、分かりやすいようコメントを入れましょうね、ということですね。
いや、そもそもなぜMyStruct
がexportされてるんだ?
というのが次節です。
export
実はGo言語では、「packageのトップレベルで宣言」されており、かつ「識別子が大文字アルファベットで始まる」モノについては自動的にexportされるようになっています。
(地味にもう少し条件ありますが)
ちゃんと言語仕様にも書いてありますね。
An identifier may be exported to permit access to it from another package. An identifier is exported if both:
- 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.
All other identifiers are not exported.
出典: The Go Programming Language Specification
ということで、type MyStruct
のように書いていると、Golintは「MyStructは他のパッケージからも使われるためのものなんだな」と判断し、その結果「コメント書いてないけど?書けよ!」となるわけですね。
1.3 (前半)
だいぶGo言語が分かってきた気がしますね。
ここからがもうちょっとヘビーになります笑
* と & (ポインタ)
学生時代にC言語をちょっと履修していた僕にとっての最大の心理的壁がこいつでした。
まあまずは言語仕様を見てみましょう。
Pointer types
A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. The value of an uninitialized pointer is nil.
出典: The Go Programming Language Specification
「ポインタ型は、ベースとなる特定の型を持つ変数に対応するポインタを表す。初期化されていないポインタの値はnil
である。」
うーん、わからんwww
ただ、色々と混乱しやすいのは「そもそもポインタとはなにか」が分かっていないからなのでは?という気持ちなので、まずはポインタについて考えます。
In computer science, a pointer is a programming language object that stores a memory address. (中略) A pointer references a location in memory, and obtaining the value stored at that location is known as dereferencing the pointer. As an analogy, a page number in a book's index could be considered a pointer to the corresponding page; dereferencing such a pointer would be done by flipping to the page with the given page number and reading the text found on that page.
出典: From Wikipedia, the free encyclopedia
日本語記事は冒頭のざっくり説明部分が無かったので英語版から引用。
ざっくり訳すと、
コンピュータサイエンスにおいて、ポインタとはメモリ上のアドレスを保持するモノである。ポインタが表す場所から値を取得することを「(ポインタを)デリファレンスする」という。
(中略)
例えるなら、書籍の目次に記載されている「ページ数」は、それに対応するページへのポインタであると類推できる。
「ページ数」ポインタをデリファレンスするという作業は、そのページ数分本をめくり、ページ内の文章を読むことだと類推できる。
ということで、「ポインタ」を考えるにあたっては
- ポインタそのもの (ポインタとはコンテンツの場所を指し示す(pointする)もの)
- ポインタが指し示す先にあるモノ (ポインタをデリファレンスしたもの)
の2つのコンセプトがあることがわかりますね。
現実世界ならこれで十分なんですが、プログラムを書くとなった場合には実はもう一段階の抽象化が必要です。
ここが多分難しいんですね。
それは、「ある値がポインタであるかどうかを識別できるようにする必要がある」ということです。
これが「ポインタ型」ですね。
ポインタと名前はついていますが、扱いは他の型と同様です。
ポインタ型は*<BaseType>
という記法で表現されるので、
type MyString string
type MyStringPointer *string
MyString
はstring
型の変数で、
MyStringPointer
はstring型の値へのポインタが入るポインタ
型
となります。
さらに事態をややこしくするのが、「ポインタ型」とひとくくりに呼ばれるのですが、実は上記のように「string型の値へのポインタが入るポインタ型」があれば「int型の値へのポインタが入るポインタ型」もあるという事実です。
ただこれは考えてみれば当然で、「任意の型の値へのポインタ」をデリファレンスするとどんな値が得られるのか分からないですよね。
その結果、その値をそのまま使えるのか、もしくは他の型にキャストしないと使えないのか、処理の仕方が分からなくなります。
ということで、一般的に「ポインタ」と呼ばれている概念には実は以下の意味が含まれていることが分かりました。
- ポインタが格納される変数の型
- ポインタそのもの
- ポインタが指し示す先にあるモノ
ではそれぞれをコードでどのように表すのかを簡単に見ておきましょう。
下記をプロジェクトルートのmain.go
に書いてみます。
package main
import (
"fmt"
)
type MyIntPointer *int // 1. ポインタが格納される変数の型
func main() {
// int型の変数 myInteger を初期化
var myInteger int = 10
/**
* myIntegerの中身(= 10)が格納されているメモリアドレス(= ポインタ)を取得し、
* 上で宣言しておいた MyIntPointer型 の変数に格納
*/
var myIntegerPointer MyIntPointer = &myInteger // 2. ポインタそのもの
// myIntegerPointerをデリファレンス
var dereferencedMyInteger = *myIntegerPointer // 3. ポインタが指し示す先にあるモノ
fmt.Printf("MyInteger: %d\n", myInteger)
fmt.Printf("MyIntegerPointer: %v\n", myIntegerPointer)
fmt.Printf("DereferencedMyInteger: %d\n", dereferencedMyInteger)
}
ここでわかりにくいのは、
「ポインタが格納される変数の型」と「ポインタが指し示す先にあるモノ」それぞれを記述する際に使う記号(*)が全く同じということですね。
こればっかりは覚えるしかありません。
また、任意の変数からその値が格納されているメモリアドレスを取得する場合は&
を使います。
*<型名> -> ポインタ型
*<ポインタ変数名> -> デリファレンス
&<変数名> -> ポインタ取得
です。
書き終わったらターミナルでプログラムを走らせてみます。
うまく動けば下記のように出力されるはずです。
$ go run main.go
MyInteger: 10
MyIntegerPointer: 0xc0000b6038
DereferencedMyInteger: 10
ポインタの値はプログラムを動かす度に変わりますが、ポインタ周りのコードがちゃんと動いたのが分かりますね!
関数とメソッド
さて、一旦ポインタの話から離れましょう。
既に上のコード例にも出てきていますが、func main()
はメイン関数と呼ばれる、Go言語の関数の中でも特殊な関数です。
特殊なやつは置いておいて、まずは通常の関数から見ていきましょう。
関数
関数は、宣言することによって任意の識別子を関数処理に束縛します。
具体的には下記のように書くことで、MyFunction
という関数を宣言できます。
func MyFunction() {}
上記関数は文字通り何も行っていないため引数も返り値もありませんが、それらを持つ関数は下記のように定義できます。
func AddArgs(num1, num2 int) int {
return num1 + num2
}
メソッド
メソッドは関数の特殊系です。
Go言語においてはreceiver
(レシーバ)と同時に定義された関数 = メソッドです。
レシーバは「そのメソッドが何に紐づいたメソッドなのか」を表現したものです。
誤解を招く可能性もありますが、「どの型に紐付いたメソッドなのか」を表すと言い換えても良いかもしれません。
メソッドは下記のように宣言できます。
func (m MyType) MyFunc(num int) int {
return num
}
上記に於いては、m
がレシーバで、MyType
がレシーバのbase type
と呼ばれます。
base type
にはポインタ型、またはインターフェース型(本記事未出)は利用できません。
また、base type
はそのメソッドと同じパッケージ内に定義されていないといけません。
レシーバがメソッド処理内で利用されていない場合、メソッド定義を下記のように省略できます。
func (MyType) MyFunc(num int) int {
return num
}
メソッドを呼ぶ際には下記のようになります。
m := MyType{} // MyType型の変数を初期化
m.Func1(1) // メソッドはメソッドセレクタ(.でつなぐ形式)で呼び出せる
なお、下記のように宣言された関数はメソッドではありません。
そのため、メソッドセレクタで呼び出すこともできません。
func MyFunc(m MyType, num int) int {
return num
}
ポインタレシーバ
さて、実はレシーバはポインタを指定することも可能です。
上記ではレシーバはポインタではなく値そのものでした。
これはValue receiver
と呼ばれています。
本節ではそれに対してPointer receiver
を紹介します。
といっても、定義の仕方はバリューレシーバとほぼ一緒です。
func (m *MyType) MyFunc(num int) int {
return int
}
MyTypeに*がついているので、m
は「MyType型へのポインタが入るポインタ型」ですね。
ただ、上記のコード例ではレシーバがポインタになったからと言って何が嬉しいのかよくわかりません。
そこで検証のため、新しくコードを書いてみます。
mystring
パッケージを作成し、string
を拡張したMyString
型を作ってみます。
そして、自分自身に格納された文字列をひっくり返すメソッドを実装します。
念の為、ひっくり返す処理が終わったら結果をPrintf
で表示しておきましょう。
package mystring
import "fmt"
type MyString string
func (str MyString) ReverseSelf1() {
runes := []rune(str)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
str = MyString(runes)
fmt.Printf("mystring: %v\n", str)
}
そして、このメソッドをmain関数から呼び出してやります。
package main
import (
"<MODULE_PATH>/mystring"
)
func main() {
m := mystring.MyString("this is my string.")
m.ReverseSelf1()
}
そして実行してみると
$ go run main.go
mystring: .gnirts ym si siht
ちゃんとひっくり返っていますね!
でも念の為、main関数の方でもm
の中身がひっくり返っているかをチェックしてみましょう。
func main() {
m := mystring.MyString("this is my string.")
m.ReverseSelf1()
fmt.Printf("mystring: %v\n", m)
}
もう一度実行します。
$ go run main.go
mystring: .gnirts ym si siht
mystring: this is my string.
すると、mystringパッケージの方ではうまくいっているのに、呼び出し元ではひっくり返っていません。。
実はバリューレシーバで実装されたメソッドの場合のレシーバには、そのメソッドが呼び出された時点の値のコピーが渡されることになります。
つまり、ReverseSelf1
の中のstr = MyString(runes)
という処理は、コピーに対して結果を代入しており、呼び出し元の変数には全く影響を及ぼさない、ということになります。
もちろん処理の内容によってはその方が望ましい場合もあるでしょうが、今回はどうしても呼び出し元をひっくり返したいものとします。
そこでポインタレシーバを活用します。
mystring
パッケージに下記を追記します。
func (str *MyString) ReverseSelf2() {
strV := *str
runes := []rune(strV)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
*str = MyString(runes)
}
そしてmainから呼び出しましょう。
func main() {
m := mystring.MyString("this is my string.")
m.ReverseSelf1()
fmt.Printf("mystring: %v\n", m)
m.ReverseSelf2()
fmt.Printf("mystring: %v\n", m)
}
そして実行してみると、
$ go run main.go
mystring: .gnirts ym si siht <- ReverseSelf1の内部
mystring: this is my string. <- ReverseSelf1を実行した後の呼び出し元
mystring: .gnirts ym si siht <- ReverseSelf2を実行した後の呼び出し元
見事にひっくり返ってますね!
ポインタレシーバメソッドを実行した場合、レシーバには呼び出し元のポインタが渡されます。
そのため、メソッド内で呼び出し元の値を直接参照したり、編集したりできるわけです。
さらに、ポインタレシーバの場合は値をコピーしないため、その分メモリ効率が良いという特徴もあります。
一旦ここまで
ちょっと思ってたより1.3節で必要な知識が多すぎたので、ここで1記事目を区切りたいと思います笑
次回はArray, Sliceなどを見ていきます。
できるだけ早めに投稿できるようにがんばります!