Go(プログラミング言語):Dev Basics/Keyword
GoはGoogleによって開発されているオープンソースなプログラミング言語であり、静的言語と動的言語のよいとこどり、並列処理などの特徴を持っている。
Goは、Googleによって開発されているオープンソースなプログラミング言語だ。シンプルな言語であることに加えて、高速なコンパイル、静的型付けと型推論、強力な並列処理機構、ガベージコレクション、オブジェクト指向的な機構(の一部)や例外処理がサポートされないなどの特徴を持っている。
Goの特徴
Goは、コンピュータ(ハードウェア)の進化に合わせて、従来はC++やJavaなどが使われてきた大規模なシステムソフトウェア開発でも使える(それらの言語よりも)高速、効率的なプログラミング言語を目指して作られたものだ。
その特徴は、高速なコンパイル、(CPUのマルチコア化に適合した)並列処理機構、ガベージコレクション機構、静的型付け、フラットな型システム(型の継承をサポートしない)である。特にネイティブコードへのコンパイルは高速であり、Goの公式サイトのFAQページによれば「1台のコンピュータ上で大規模なGoプログラムを数秒でコンパイル可能」である。
Goはコンパイル型言語ではあるが、型推論などの機構を活用することで、スクリプト言語(や動的型付け言語)などでよく見られる簡潔なコード記述も可能となっている。
以下にシンプルなGoプログラムの例を示す。
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
簡単に説明しておこう。まず、Goプログラムではプログラマーがセミコロンを明示的に記述する必要はない(内部では自動的に挿入されている)。また、単体として完結するプログラムは「main」パッケージという名前になり、引数と戻り値を持たないmain関数を含む。「import "fmt"」はCのprintf関数などに似た書式指定付きの入出力を行うためのパッケージfmtをプログラムに取り込むことを宣言している。
以下では、Goの特徴を幾つか掘り下げて見ていく。
簡潔な変数宣言
以下は標準入出力に対して、読み書きを行うHello Worldプログラムだ。
package main
import(
"fmt"
"bufio"
"os"
)
func main() {
fmt.Print("what's your name: ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
fmt.Println("Hello " + scanner.Text())
}
ここでは先ほど見たfmtパッケージに加えて、バッファリング付きの入出力を行うbufioパッケージと標準入出力(os.Stdin)を扱うためのosパッケージを利用して、標準入力からのユーザー入力を受け取っている。コードを見れば分かる通り、その値はscanner.Textメソッドを呼び出すことで取得できる。注目してほしいのは変数scannerの宣言部分だ。ここでは、型推論が行われ、scanner変数の型が自動的に決定されている。
この部分は以下のようにも記述できる。
var scanner = bufio.NewScanner(os.Stdin)
その違いはvarキーワードの有無と、「=」と「:=」にある。基本は「変数名 := ……」形式の記述を使えばタイプ量も減り、余計な字句が減り、コードを読みやすくなる。
上のコードでは、fmtパッケージのみをimportしている点にも注意されたい。不要なパッケージをここに指定してみよう。
…… 省略 ……
import(
"fmt"
"bufio"
"os"
)
…… 省略 ……
すると、次のようなエラーが発生する。このようにすることで、パッケージの依存関係の不要な分析が避けられる。
$ go run hello.go
# command-line-arguments
./hello.go:5: imported and not used: "bufio"
./hello.go:6: imported and not used: "os"
メソッドとインタフェース
冒頭で「オブジェクト指向的な機構(の一部)がサポートされない」ことをGoの特徴として挙げた。大ざっぱにいえば、これは「Goでは型の継承(型階層)がサポートされない」ことを意味する(実際にはできないわけではない)。その一方で、特定の型と結び付いた関数、すなわちメソッドを使用することは可能だ。例えば、前述の「scanner.Textメソッド」がそうだ。これはbufioパッケージが提供するScanner型の変数scannerをレシーバーとする関数だ。オブジェクト指向プログラミングのメッセージ/レシーバーの関係を思い出せば、その意味は容易に分かるはずだ。
詳細は割愛するが、簡単な型宣言と、その型のオブジェクトをレシーバーとするメソッド宣言の例を以下に示す。
type person struct {
name string
tel string
}
func (p *person) printData() {
fmt.Println(p.name + ": " + p.tel)
}
ここではtypeキーワードを使用して、person型を宣言している。printDataはperson型のオブジェクトをレシーバーとするメソッドだ。メソッドを定義する際には、上のコードにあるように関数名の前に、そのレシーバーとなる対象を「(p *person)」のように指定する(「*」はCプログラマーにはおなじみのポインタだ)。
また、多態性を実現するために、Goではインタフェースと呼ばれる機構も用意されている。簡単な例を示す。上で見たperson型に加えて、company型とそのメソッドも宣言している。
…… 省略 ……
type person struct {
name string
tel string
}
func (p *person) printData() {
fmt.Println(p.name + ": " + p.tel)
}
type company struct {
name string
address string
tel string
}
func (c *company) printData() {
fmt.Println(c.name + ": " + c.address + ", " + c.tel)
}
type phoneBook interface {
printData()
}
…… 省略 ……
ここではpersonとcompanyの2つの型とそれらをレシーバーとするメソッドに加えて、phoneBookインタフェースを宣言している。
インタフェースとは、そのインタフェースに対して呼び出しが行われるメソッド(群)を列挙するものだ。そのインタフェースで規定されているメソッドを持つ型(構造体)は全てそのインタフェース型として扱える。ここでは、phoneBookインタフェースのメンバにはprintDataメソッドだけであり、person型とcompany型はそのメソッドを持っているため、これらをphoneBook型として扱える。
そのため、main関数では次のように、phoneBook型の配列を定義して(「array := []phoneBook {...}」)、そこにperson型のオブジェクトとcompany型のオブジェクトを含め、それぞれの要素に対してprintDataメソッドを呼び出し可能だ。呼び出されるメソッドはもちろん型によって異なる。
func main() {
p1 := new(person)
p1.name, p1.tel = "insider.net", "1111-1111"
c1 := new(company)
c1.name, c1.address, c1.tel = "atmarkit", "japan", "2222-2222"
array := []phoneBook {
p1, c1,
}
array[0].printData()
array[1].printData()
}
以下に実行結果を示す。person型のデータとcompany型のデータで出力が異なっているのが分かる。
insider.net: 1111-1111
atmarkit: japan, 2222-2222
このように、Goでは型の継承こそサポートしていないものの、柔軟な形でさまざまな型を扱えるようになっている。
goroutine
最後に、並列処理を行うための機構、goroutineを簡単に紹介しておこう。以下に例を示す。
package main
import(
"fmt"
"time"
)
func await1(message string, ch chan <- string) {
fmt.Println(message + "1")
time.Sleep(1000 * time.Millisecond)
fmt.Println(message + "2")
time.Sleep(1000 * time.Millisecond)
fmt.Println(message + "3")
time.Sleep(1000 * time.Millisecond)
ch <- message + " finished"
}
func await2(message string, ch chan <- string) {
fmt.Println(message + "1")
time.Sleep(300 * time.Millisecond)
fmt.Println(message + "2")
time.Sleep(300 * time.Millisecond)
fmt.Println(message + "3")
time.Sleep(300 * time.Millisecond)
ch <- message + " finished"
}
func main() {
ch := make(chan string)
go await1("foo", ch)
go await2("bar", ch)
fmt.Println(<- ch)
fmt.Println(<- ch)
}
2つの関数await1とawait2を宣言し、それぞれをgo文で並列に実行している。await1関数はメッセージを表示してから1秒スリープし、await2関数はメッセージを表示してから0.3秒スリープする。chは「チャネル」と呼ばれるデータ構造であり、goroutineとの通信を行うために使用している。2つの関数は、実行が終わるとチャネルchにメッセージを書き込み、main関数では書き込まれたデータをコンソールに出力している。実行結果は次のようになる。興味のある方はgo文での関数の実行を通常の関数呼び出しに変更するなどして、動作を確認してほしい。
bar1
foo1
bar2
bar3
bar finished
foo2
foo3
foo finished
本稿では、Go言語の基本要素を見てきた。型推論を利用した簡潔なコード記述、不要なパッケージのimportの禁止などはコードの可読性やコンパイル時間の高速化に役立つと思われる。また、型の継承をサポートしない一方で、インタフェースやメソッドなどの機構を用意することで、型の柔軟な扱いも可能となっている。本稿では詳しく説明できなかったが並列処理を行うための機構であるgoroutineなどについては以下の資料を参考にされたい。
参考資料
- 「The Go Programming Language」: Go公式サイト
- 「golang.jp」: 公式サイトの情報を日本語に訳して提供してくれている
- 「新世代の並列処理言語Google Goをひもとく」: @ITでのGo解説記事
Copyright© Digital Advantage Corp. All Rights Reserved.