Goを50時間ほど勉強して分かったことを整理します。
勉強時間の内訳は、
- Udemyの「Go: The Complete Developer's Guide (Golang) - Stephen Grider」を全て完了(16時間)
- Udemyの「Web Development w/ Google’s Go (golang) Programming Language - Todd McLeod」を3/4ぐらい完了(16時間)
- 「Go by Example - Mark McGranaghan(無料)」 をざっと読む(2時間)
- 実際に自分でコードを書いてみた → コードはこちら(16時間)
です。
学習開始時のレベルは、Goを触るのは初めてで、Ruby や JavaScript は普通に書ける、といった感じでした。
.goファイルと基本コマンド
Go の型や文法に触れる前に、.goファイルとその基本コマンドについて整理します。
.goファイル
.goファイルのシンプルなコード例は次のとおりです。
package main
import "fmt"
func main() {
fmt.Println("Hello, playground")
}
この例の処理結果は、Go Playground で確認できます(引用元)。
package
package
は「ワークスペース名」のようなもので、実行可能パッケージと再利用パッケージの2つがあります。実行可能パッケージは、必ず main
と命名します。
名前 | 種類 |
---|---|
main | 実行可能(executable) |
その他 | 再利用(reuseable) |
import
import
で標準ライブラリや外部ライブラリなど、必要な依存ファイルをインポートします。
func main()
func main()
は、package main
ファイルの実行時に呼び出される関数です。package main
では、必ず定義しないといけません。
Go CLI
Go の基本的なコマンドラインは次のとおりです。
go run
goファイルをコンパイルして処理を実行します。
$ go run main.go
冒頭のコード例を実行すると、次のとおり。
Hello, playground
go build
goファイルをコンパイルして、実行可能な状態にします。
$ go build main.go
コンパイルされた main
ファイルの実行。
$ ./main
go get
外部ライブラリのインストールに使います。
$ go get github.com/gorilla/mux
go test
テストファイル(*_test.go
)を実行します。
$ go test
データ型と基本文法
それでは本題に入り、Go のデータ型と基本的な文法について整理します。
基本のデータ型
基本のデータ型は次のとおりです。
種類 | 例 |
---|---|
bool | true/false |
string | "string" |
int | 10 |
float64 | 3.14 |
変数の定義
静的型付け言語なので、変数の定義時にデータ型を指定しないといけません。
var fruit string = "apple"
:=
を使って、次のように書くこともできます。この場合、Goが自動でデータ型を認識します。
fruit := "apple"
一度変数を定義すれば、それ以降の操作で var
や :=
は要りません。
fruit = "orange"
複数の変数を同時に定義することもできます。
a, b := "apple", "orange"
関数の定義
関数を定義する場合、関数名の後に返り値の型を指定します。
func newFruit() string {
return "apple"
}
引数を持たせる
引数を持たせる時は、その型も指定します。
func newFruit(f string) string {
return f
}
複数の値を返す
複数の値を返す時は、それぞれの型を指定します。
func newFruits() (string, string) {
return "apple", "orange"
}
for
for ループは次のように for {}
で書きます。
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
break
で処理を中断します。
for {
fmt.Println("loop")
break
}
continue
で後続の処理をスキップします。
for n := 0; n <= 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
if
条件分岐は、if {}
で次のように書きます。
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
else if
や else
でつなぐこともできます。
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
例外処理
Go の例外処理は if
を使って実装します。
if err != nil {
fmt.Println("Error:", err)
}
利用する関数が error
を返す場合は、その直後に例外処理を書くのが一般的です。
bs, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println("Error:", err)
}
Array と Slice
Array と Slice は、どちらもいわゆる「配列」で、単一のデータ型を持つことができます。
例えば、文字列を持つ Array は「string Array」、文字列を持つ Slice は「string Slice」と言い、どちらも []string{}
で定義します。それぞれの例は次のとおり。
[2]string{"apple", "orange"}
[]string{"apple", "orange"}
Array と Slice の違いは、データの長さを変更できるかどうかです。Array は長さが固定されていますが、Slice は長さを自由に変えられます。上の例だと、Array の長さは「2」で、後から変更できません。一方 Slice の長さは、次のように変更できます。
fruits := []string{"apple", "orange"}
fruits = append(fruits, "banana") // [apple orange banana]
空の Array と 空の Slice は、次のように定義します。
var fruits [2]string
fruits := make([]string, 2)
Slice の基本操作
値取得
0から始まる n 番目の値を取ってきます。
fruits[0]
範囲指定して値を取り出すこともできます。
fruits[0:2] // [0] から [1] まで
fruits[:2] // [0:2] と同義
fruits[2:] // [2] 以降
len
len
で Slice の長さを調べます。
len(fruits)
append
append
で値を追加します。
fruits = append(fruits, "lemon")
for, range
反復処理は、for
と range
を使って次のように書きます。
for i, fruit := range fruits {
fmt.Println(i, fruit)
}
インデックスを使わない場合は、その変数を _
にしないといけません。
for _, fruit := range fruits {
fmt.Println(fruit)
}
Slice の操作をもっと知りたい場合 → こちら
Map
Map は Ruby でいうハッシュ、JavaScript でいうオブジェクトと似たような感じで、キー・バリューのペアでデータを保持します。全てのキーと、全てのバリューは、それぞれ同じデータ型でないといけません。例えば、文字列キー・文字列バリューの Map は次のように定義します。
colors := map[string]string{
"red": "#ff0000",
"green": "#4bf745",
}
空の Map は次のように定義します。
var colors map[string]string
// 上と同義
colors := make(map[string]string)
Map の基本操作
値取得
キーを指定して値を取り出します。
colors["red"] // #ff0000
追加
新たなキーを指定することで、キー・バリューを追加できます。
colors["white"] = "#ffffff"
delete
delete
でキー・バリューを削除します。
delete(colors, "white")
for, range
反復処理は、for
と range
を使って次のように書きます。
for color, hex := range c {
fmt.Println("Hex code for", color, "is", hex)
}
Struct
Struct で基本データ型のデータ構造を定義すれば、それを Map(ハッシュ)のように使えます。
type person struct {
firstName string
lastName string
}
Structは、フィールドに値を渡して定義します。
ken := person{firstName: "Ken", lastName: "Thompson"}
フィールドを指定しない場合は、定義した順番にしたがって値が割り当てられます。
ken := person{"Ken", "Thompson"} // 上と同義
Struct のデフォルト値
Struct のフィールドに値を入れない場合、デフォルト値が適用されます。それぞれのデータ型のデフォルト値は次のとおりです。
種類 | デフォルト |
---|---|
bool | false |
string | "" |
int | 0 |
float64 | 0 |
Struct の基本操作
各フィールドの値は、.
メソッドのようにして取り出せます。
fmt.Println(ken) // {Ken Thompson}
fmt.Println(ken.firstName) // Ken
fmt.Println(ken.lastName) // Thompson
値の更新も .
を使って感覚的におこなえます。
ken.lastName = "Anderson"
fmt.Println(ken) // {Ken Anderson}
Embedded structs(ネスト)
Struct のフィールドに別の Struct をネストさせることができます。
type contactInfo struct {
email string
zipCode int
}
type person struct {
firstName string
lastName string
contact contactInfo
}
フィールド名とネストさせる Struct 名を統一する場合は、次のように省略して定義できます。
type person struct {
firstName string
lastName string
contactInfo // contactInfo contactInfo と同義
}
Map と Struct の違い
Map と Struct は似ていますが、次のような違いがあります:
- Map
- キーは全て同じ型
- バリューも全て同じ型
- キーはインデックスされている(反復処理できる)
- Reference Type(後述)
- Struct
- 各バリューの型は自由
- キーはインデックスされていない(反復処理できない)
- Value Type (更新にはポインターが必要・後述)
メソッド
関数にレシーバを持たせることで、メソッドを定義できます。
type rect struct {
width, height int
}
func (r rect) area() int {
return r.width * r.height
}
上の例だと、type rect
であれば、area()
メソッドを呼び出せます。
r := rect{width: 10, height: 5}
fmt.Println("area: ", r.area()) // area: 50
Struct 更新できない問題
例えば、上のStruct type rect
の値を更新するメソッドを書いてみます。
// widthを更新するメソッド
func (r rect) updateWidth(w int) {
r.width = w
}
しかし、これを実行しても元の width
は更新されません!
r := rect{width: 10, height: 5}
r.updateWidth(20)
fmt.Println("area: ", r.area()) // area: 50
fmt.Println(r) // {10 5}
Value Type の更新
これは Struct が Value Type であることが原因です。Value Type をレシーバにすると、そのコピーされた値がメソッド関数に渡ってしまうからです。
これを解決するには、&
を使って Value Type のポインターのメモリアドレスを引数に渡し、*
を使ってそのポインター(実際の値)をレシーバに指定します。
// ポインター(実際の値)をレシーバに指定
func (r *rect) updateWidth(w int) {
r.width = w
}
r := rect{width: 10, height: 5}
// r のポインターでメソッドを呼ぶ
rPointer := &r
rPointer.updateWidth(20)
fmt.Println("area: ", r.area()) // area: 100
fmt.Println(r) // {20 5}
Value Type の更新(省略ver.)
上のようにいちいちポインターを定義しなくても、メソッド引数に *
が使われていれば、Value Type を渡すだけで Go がそれを自動変換してくれます。なので、普通はポインターの定義を省略して書きます。
r := rect{width: 10, height: 5}
r.updateWidth(20) // Go がポインターに自動変換してくれる
fmt.Println("area: ", r.area()) // area: 100
fmt.Println(r) // {20 5}
Reference Type
一方の Reference Type (例えば、Slice)は、もともと別のデータメモリを参照している型なので、ポインターを使わなくても別の関数内で更新できます。
func updateFruits(fruits []string) {
fruits[0] = "lemon"
}
fruits := []string{"apple", "orange", "banana"}
updateFruits(fruits)
fmt.Println(fruits) // [lemon orange banana]
Value Type と Reference Type、それぞれの一覧は次のとおりです:
Value Type 一覧
- int
- float64
- string
- bool
- structs
Reference Type 一覧
- slices
- maps
- channels
- pointers
- functions
Interface
Interface は、同じ振る舞い(メソッド)をもつ複数の型を1つの型にまとめるのに使います。Interface に対して共通したメソッドを書くことができます。
例えば、長方形 rect
と、円 circle
は、両方とも面積を計算する area()
メソッドを持っていたとします。
type rect struct {
width, height float64
}
type circle struct {
radius float64
}
func (r rect) area() float64 {
return r.width * r.height
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
この area()
メソッドを持っている型を geometry interface
とし、measure()
メソッドを定義します。
type geometry interface {
area() float64
}
func measure(g geometry) {
fmt.Println("area:", g.area())
}
rect
と circle
は、どちらも geometry interface
の定義を満たしているので、measure()
の引数として利用できます。
r := rect{width: 3, height: 4}
c := circle{radius: 5}
measure(r) // area: 12
measure(c) // area: 78.53981633974483
Interface の例(net/http)
Webサーバを作る時に使われる標準ライブラ、net/http パッケージを使って、Interface を実際に使ってみます。
import "net/http"
Handler
まず、http.Handler は、ServeHTTP(ResponseWriter, *Request)
メソッドを持つ Interface です。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ListenAndServe
そして、Webサーバの役割を成す http.ListenAndServe は、第2引数に Handler
を取っています。
func ListenAndServe(addr string, handler Handler) error
Handler を作ってサーバを立ち上げる
つまり、ServeHTTP()
メソッドを持つ型を作れば、それを Handler
として扱うことができ、さらにその型を ListenAndServe
の引数に渡せば、独自のレスポンスを返すことができます。
試しに anything
タイプに ServeHTTP()
を持たせて、レスポンスを返してみます。
package main
import (
"fmt"
"net/http"
)
type anything int
func (a anything) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Any code you want in this func")
}
func main() {
var a anything
http.ListenAndServe(":8080", a)
}
go run
コマンドでウェブサーバを起動します。
$ go run main.go
http://localhost:8080
を叩くと、ちゃんとレスポンスが返ってきますね。
$ curl http://localhost:8080
Any code you want in this func
Goroutines と Channels
Go はデフォルトでは Main Routine(シングルスレッド)で処理していますが、HTTPリクエストを連続して投げる時など、関数呼び出しの前に go
をつけると、新しい Child Goroutine を作成して Concurrency 処理(マルチスレッド)にできます。
go checkLink(link) // Child Goroutine で実行
Child Goroutine 待たれない問題
注意点としては、ただ go
で Child Goroutine を利用するだけだと、Main Routine は Child Goroutine の処理を待たずに終了してしまうことです。
func checkLink(link string, c chan string) {
_, err := http.Get(link)
if err != nil {
c <- link + " might be down!"
return
}
c <- link + " is up!"
}
links := []string{"http://google.com", "http://github.com"}
c := make(chan string)
for _, link := range links {
go checkLink(link, c)
}
for i := 0; i < len(links); i++ {
fmt.Println(<-c)
}
この問題を防ぐのに使われるのが Channels です。
Channels
Channels は Routines 間のコミュニケーションに使い、次のように定義します。
c := make(chan string)
テキストメッセージサービスのようなイメージで、次のようにデータを送受信します。
c <- "Some string" // 送信
<- c // 受信
Channels で Child Goroutine を待つ
この Channels を使って、Main Routine が Child Goroutine の処理を待つようにします。具体的には、Child Goroutine に Channel を渡し、Child Goroutine の処理終了時にその Channel へデータを送信させます。Main Routine に受信したデータを使う処理を書いておけば、ちゃんと Child Goroutine の処理を待つことができます。
// Child Goroutine に Channel を渡す
// 処理終了時にその Channel へデータを送信
func checkLink(link string, c chan string) {
_, err := http.Get(link)
if err != nil {
c <- link + " might be down!"
return
}
c <- link + " is up!"
}
links := []string{"http://google.com", "http://github.com"}
c := make(chan string)
for _, link := range links {
go checkLink(link, c)
}
// 注意: links の数だけで Channel でデータを受信する
for i := 0; i < len(links); i++ {
fmt.Println(<-c) // 受信したデータを使う処理
}
ここでの注意点は、Child Goroutine の数だけ、Main Routine にて Channel でデータを受信しないといけません。この数が足りないと、全ての Child Goroutine の処理が終了する前に Main Routine の処理が終了してしまうからです。したがって、上の例では links
の数だけ「受信したデータを使う処理」を反復させています。
Goのテスト
Go でのテストは、標準ライブラリの testing を使って、*_test.go
ファイルにシンプルなテストコードを書いていくのが一般的です。
*testing.T
テストコードの例は、次のとおりです(Abs
のテスト):
func TestAbs(t *testing.T) {
got := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %d; want 1", got)
}
}
テスト関数の引数に *testing.T
を渡し、if
でアサーションを検証します。エラーがある場合は、t.Errorf
で結果を失敗にします。
TestMain(m *testing.M)
テストファイルの実行前後にセットアップなどの処理が必要なときは、TestMain(m *testing.M)
にその処理を書きます。
func TestMain(m *testing.M) {
// setup
code := m.Run() // テストの実行
// teardown
os.Exit(code) // 処理の終了
}
go test
go test
コマンドで *_test.go
ファイルのテストを実行します。
$ go test