LoginSignup
8
15

More than 3 years have passed since last update.

Goを50時間勉強して分かったこと

Posted at

Goを50時間ほど勉強して分かったことを整理します。

勉強時間の内訳は、

です。

学習開始時のレベルは、Goを触るのは初めてで、Ruby や JavaScript は普通に書ける、といった感じでした。

.goファイルと基本コマンド

Go の型や文法に触れる前に、.goファイルとその基本コマンドについて整理します。

.goファイル

.goファイルのシンプルなコード例は次のとおりです。

main.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 ifelse でつなぐこともできます。

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{} で定義します。それぞれの例は次のとおり。

Arrayの例
[2]string{"apple", "orange"}
Sliceの例
[]string{"apple", "orange"}

Array と Slice の違いは、データの長さを変更できるかどうかです。Array は長さが固定されていますが、Slice は長さを自由に変えられます。上の例だと、Array の長さは「2」で、後から変更できません。一方 Slice の長さは、次のように変更できます。

fruits := []string{"apple", "orange"}
fruits = append(fruits, "banana") // [apple orange banana]

空の Array と 空の Slice は、次のように定義します。

Arrayの例
var fruits [2]string
Sliceの例
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

反復処理は、forrange を使って次のように書きます。

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

反復処理は、forrange を使って次のように書きます。

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())
}

rectcircle は、どちらも 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() を持たせて、レスポンスを返してみます。

main.go
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 のテスト):

xxx_test.go
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

References

8
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
15