1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go言語: 雑多な Tips、文法など

Last updated at Posted at 2020-09-13

この記事について

忘備録として Go言語の雑多なTIPS、文法などを脈絡なくまとめていきます。

TIPS

ランダムな文字列の生成

いろんな方法があるが、長さや文字種の指定がなければこんな簡易的な方法もある(Unix時間を36進数表記の文字列に変換してる)。

s := strconv.FormatInt(time.Now().UnixNano(), 36)
// 実行結果は、例えば "c3av0t23ntsk"

ただ、時刻を用いてるだけなので複数の Goroutine から同時実行すると同じ文字列が返る可能があることは注意。

ある文字列が数字だけ含むかチェック

"20200601" のように、文字列が数字だけ含むかどうかチェックしたいときはstrings.Trim(<文字列>, "0123456789") == "" が使える。

サンプルコード:

func main() {
    params := []string {"1", "dog", "20200601", "5c6"}
    
    for _, p := range params {
        if strings.Trim(p, "0123456789") == "" {
            fmt.Println(p)
        }
    }
}

// 実行結果
// 1
// 20200601

if の代わりの switch

switch の後ろに変数を指定しないで、単に if 文の代わりにのように使うことが出来る。

type user struct {
    name    string
    age     int
    healthy bool
    hobbies []string
}

func main() {
    u := user{
        name:    "Andy",
        age:     110,
        healthy: true,
        hobbies: []string{"Game", "Music"},
    }

    // if, else の代わり
    switch {
    case u.name == "":
        fmt.Println("Unknown!")
    case u.age > 100 && u.healthy:
        fmt.Println("Fantastic!")
    case len(u.hobbies) > 0:
        fmt.Println("Have fun!")
    }
}

// 実行結果
// Fantastic!

main 関数を抜けたくない時

goroutineの開始後に main関数から抜けたくない時、select を使うことができる。

func main() {
    // goroutineを開始...

    // goroutineがずっと動いてるので、main から抜けたくない
    select {}
}

適当なサンプルだがこんな感じの使い方。goroutineが終了するケースでは all goroutines are asleep - deadlock! って怒られるので要注意。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // 複数の HTTP サーバーを起動
    addrs := []string{":8080", ":8081", ":8082"}
    for _, addr := range addrs {
        s := &http.Server{
            Addr: addr,
            Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "Hello from %s", r.Host)
            }),
        }
        go s.ListenAndServe()
    }

    select {}
}

main 関数を抜けたくない時 (ユーザーの入力待ち状態にする)

似た話だが、mainを抜けたくないため入力待ち状態にする方法。ENTER キーを打つと main を抜ける。

import (
    "bufio"
    "os"
)

func main() {
    // goroutineを開始...

    // 入力待ち状態にする
    bufio.NewScanner(os.Stdin).Scan()
}

スライスの要素を削除

よくあるやつだが、スライスの途中の要素を削除する方法。

func main() {
    s := []string{"dog", "cat", "bird", "desk", "pig", "rabbit"}
    
    n := 3 // "desk" を削除したい
    s = append(s[:n], s[n+1:]...)
    fmt.Println(s)

    // append を使わずにこうやってもよい。
    //copy(s[n:], s[n+1:])
    //s = s[:len(s)-1]
}

// 実行結果
// [dog cat bird pig rabbit]

ある変数がインターフェースを実装しているかチェック

reflect パッケージの Implements を使う。

package main

import (
    "fmt"
    "reflect"
)

type Sender interface {
    Send()
}

type Receiver interface {
    Recv()
}

// App は Sender インターフェースだけを実装
type App struct{}

func (c *App) Send() {
    fmt.Println("Send something")
}

func main() {
    a := &App{}

    at := reflect.TypeOf(a)
    st := reflect.TypeOf((*Sender)(nil)).Elem()
    rt := reflect.TypeOf((*Receiver)(nil)).Elem()

    fmt.Printf("Client implements Sender   : %v\n", at.Implements(st))
    fmt.Printf("Client implements Receiver : %v\n", at.Implements(rt))
}

// 実行結果:
// Client implements Sender   : true
// Client implements Receiver : false

文法

make してない map へのアクセス

make してない map(つまりnil) のキーにアクセスすることが出来て、値として定義した型の初期値が返ってくる。

type user struct {
    name string
    age  int
}

func main() {
    // map を宣言
    var m map[string]user

    // m は nil のまま
    if m == nil {
        fmt.Println("m is nil")
    }

    // キーにアクセスすると、値として定義した型(user)の初期値が返る
    u := m["AAA"]
    fmt.Printf("%#v\n", u)

    // ただし、Key に Value をセットしようとすると panic する
    // -> panic: assignment to entry in nil map
    //m["BBB"] = user{name: "Andy", age: 20}
}

// 実行結果
// m is nil
// main.user{name:"", age:0}

型エイリアス

type構文と見た目が似てるが、型のエイリアスを定義する alias 構文というものがある。違いは以下。


// type構文 : NewString と string は異なる型として扱われる
type NewString string

// alias構文 : AliasString と string 同じ型として扱われる
type AliasString = string

サンプルコード:

type NewString string
type AliasString = string

func main() {
    s := "Hello"

    // 型が違うのでコンパイルエラーになる
    // → cannot use s (type string) as type NewString in assignment
    var ns NewString = s

    // こっちは OK
    var as AliasString = s
    fmt.Printf("%v\n", as)
}

ループ変数

Go のハマりどころのド定番。
Go ではループ変数(下記コードの n)はループが回っている間ずっと単一の変数、つまり、同じメモリ領域を使っている。ループが回るごとに変数の値が変わるだけで、変数(メモリ領域)そのものは同じ。

なので、以下のようにループ変数のアドレスを取得すると意図しない動作になる。

func main() {
    iNums := []int {1, 2, 3}
    var oNums []*int
    
    // ループ変数 n のアドレスを oNums に追加していく
    for _, n :=  range iNums {
        oNums = append(oNums, &n)
    }
    
    // oNums に追加された値とアドレスを確認
    for i, pn := range oNums {
        fmt.Printf("Index [%d], Value [%d], Address [%p]\n", i, *pn, pn)
    }
}

// 実行結果: Value は 1, 2, 3 にはならない。
// Index [0], Value [3], Address [0xc00002c008]
// Index [1], Value [3], Address [0xc00002c008]
// Index [2], Value [3], Address [0xc00002c008]

この場合、こんな風に対処できる(一部のみ掲載)。

    for _, n :=  range iNums {
        N := n
        oNums = append(oNums, &N)
    }

// 実行結果:
Index [0], Value [1], Address [0xc00002c008]
Index [1], Value [2], Address [0xc00002c040]
Index [2], Value [3], Address [0xc00002c048]

ループ変数と goroutine

Go のハマりどころのド定番 その2。
以下のコードは単一のループ変数 n を複数の goroutine が参照していること、かつ、n は goroutine 実行時の値として評価されるため意図した動作にならない。goroutine の実行タイミングによるが、ループが回り終わった後に実行されるとしたら全て最終要素の 3 になる。

iNums := []int{1, 2, 3}
for _, n := range iNums {
    go func() {
        fmt.Println(n)
    }()
}

// 実行結果: 1, 2, 3 にはならない
// 3
// 3
// 3

対処方法は、goroutine 定義時の値を引数として渡してあげること。

for _, n := range iNums {
    go func(N int) {
        fmt.Println(N)
    }(n)
}

これでもよい。

for _, n := range iNums {
    N := n
    go func() {
        fmt.Println(N)
    }()
}

クロージャー

クロージャーとは通常の関数とは少し異なり、関数の定義だけでなく、それが定義された際の環境(自身の外で宣言された変数)をセットしたもの。

以下、簡単な例。

// getFuncはクロージャーを生成する関数
func getFunc() func() {
    i := 0
    // 以下の func() がクロージャー
    return func() {
        // 自身の外で宣言された i にアクセスする
        i++
        fmt.Println(i)
    }
}

func main() {
    // クロージャーの生成と呼び出し(1つ目) : 1 → 2 → 3 と増える
    fn1 := getFunc()
    fn1()
    fn1()
    fn1()
    
    // クロージャーの生成と呼び出し(2つ目): また 1 から始まる
    fn2 := getFunc()
    fn2()
}

// 実行結果
// 1
// 2
// 3
// 1

ここで1つ前の「ループ変数と goroutine」の例を見てみる。

goroutine として実行される関数は、自身の外で使われてるループ変数 n を参照するクロージャー。ループ変数(この例のn)は全て同じメモリ領域を指すので、この例の3つの goroutine は全て同じ変数 n を参照する。各 goroutine の実行タイミングによるが、すべての出力が 3 になったりする。解決方法は前述のとおり goroutine の引数として n を渡すなど。

iNums := []int{1, 2, 3}
for _, n := range iNums {
    go func() {
        fmt.Println(n)
    }()
}

// 実行結果(タイミングによる)
// 3
// 3
// 3

(参考)

コンパイラ

Heap と Stack

Go の変数はメモリ上の Heap と Stack のどちらに保存されるか?

  • コンパイラが自動で判断して、関数内からのみ参照される変数ならスタック、関数外から参照される可能性がある変数はヒープに割り当てる
  • ビルド時に go build -gcflags -m main.go というように -gcflags オプションを付けると、どちらが選択されたか見れる。

(引用) https://golang.org/doc/faq

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?