279
318

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言語を始める方のための落とし穴、問題の解決方法やよくある間違い

Posted at

こちらの記事は、Kyle Quest氏により公開された『50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

初心者(Total Beginner)向けの和訳は 【初級編】Go言語を始める方のための落とし穴、問題の解決方法やよくある間違い があります。本記事は初中級者(Intermediate Beginner)向けの和訳です。


初中級者向け

HTTPレスポンスボディのクローズ

標準のhttpライブラリを使ってリクエストをすると、HTTPレスポンスの変数を取得します。レスポンスボディを読まない場合でも、レスポンスボディをクローズする必要があります。また空のレスポンスに対してもレスポンスボディをクローズする必要があることに注意してください。新米Gopherにとっては特に忘れやすい内容です。

同様に間違った場所でレスポンスボディをクローズしようとする人がいます。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()//問題があります
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

上記のコードはリクエストが成功した場合には動作しますが、HTTPリクエストが失敗した場合には resp 変数が nil になる可能性があり、実行時にpanicが起きます。

HTTPレスポンスがエラーかどうかチェックしたあとに defer を使ってレスポンスボディをクローズするのが一般的な作法です。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    if err != nil {
        fmt.Println(err)
        return
    }

    defer resp.Body.Close()//ほとんどの場合は問題ないでしょう
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

HTTPリクエストが失敗したほとんどの場合、変数 respnil になり変数 errnon-nil となるでしょう。しかし、リダイレクトに失敗した場合は両方の変数が non-nil になりえます。この場合はリソースのリークが発生する可能性があります。

HTTPレスポンスをエラーハンドリングするブロックの中で non-nil であるレスポンスボディをクローズすることで、上記のリークを防ぐことができます。もう一つの方法は、リクエストが成功したか失敗したかにかかわらず、レスポンスボディをクローズするように defer を使って呼び出すことです。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

当初の resp.Body.Close() の実装では、残りのレスポンスボディのデータを読み込んで破棄していました。これはHTTP Keep-Aliveを有効にしておけば、HTTPコネクションを別のリクエストで再利用することを保証していました。最新のHTTPクライアントでは異なります。残りのレスポンスデータを読み込んで破棄するのは、クライアントの責務です。そうしない場合、HTTPコネクションは再利用されずに、クローズされるかもしれません。この小さな落とし穴はGo 1.5でドキュメントに書かれることになっています。

アプリケーションにとってHTTPコネクションの再利用が重要な場合、HTTPレスポンスを処理するロジックの最後に以下のような実装を追加する必要があるかもしれません。

_, err = io.Copy(ioutil.Discard, resp.Body)  

以下のようなコードでJSONのAPIレスポンスを処理している場合には、すぐにレスポンスボディ全体を読まない場合があるため、上記のようなコードが必要になります。

json.NewDecoder(resp.Body).Decode(&data)  

HTTPコネクションのクローズ

いくつかのHTTPサーバはしばらくの間ネットワーク接続をオープンし続けます(HTTP 1.1 の仕様とサーバの "keep-alive" の設定に基づいています)。デフォルトでは、標準のhttpライブラリは接続先のHTTPサーバが要求した場合にのみネットワーク接続をクローズします。これは特定の状況下において、ソケットやファイルディスクリプションを使い果たす可能性があります。

リクエスト変数の Close フィールドを true にセットすることで、リクエストが完了した後に接続をクローズするようにhttpライブラリに依頼できます。

もう一つの方法は、リクエストヘッダーの Connectionclose をセットしてリクエストすることです。ターゲットのHTTPサーバもレスポンスヘッダーに Connection: close を付与してレスポンスしなければなりません。httpライブラリがこのレスポンスヘッダーを見ると、コネクションをクローズします。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    req, err := http.NewRequest("GET","http://golang.org",nil)
    if err != nil {
        fmt.Println(err)
        return
    }

    req.Close = true
    //あるいは以下のようにします:
    //req.Header.Add("Connection", "close")

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

httpライブラリのtransportの設定をカスタマイズすることで、HTTPコネクションが再利用されることをグローバルに無効化することもできます。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    tr := &http.Transport{DisableKeepAlives: true}
    client := &http.Client{Transport: tr}

    resp, err := client.Get("http://golang.org")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(resp.StatusCode)

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

同じHTTPサーバに多くのリクエストを送信する場合、ネットワーク接続をオープンにしたままでも問題ありません。しかし、短い間隔で異なるHTTPサーバに1、2つのリクエストを送信する場合、レスポンスを受け取った後すぐにネットワーク接続をクローズすることは良い考えです。同様にオープンできるファイルの制限数を増やしておくことも良い考えでしょう。いずれにしても、正しい解決策はアプリケーションに依存します。

JSONエンコーダーが追加する改行

JSONをエンコードするテストを実装していたときに、期待した値を取得できずにテストが失敗したことに気が付きました。何が起きたのでしょう。JSONエンコーダーを使っている場合、JSONエンコードした値の最後に改行文字が追加されます。

package main

import (
  "fmt"
  "encoding/json"
  "bytes"
)

func main() {
  data := map[string]int{"key": 1}
  
  var b bytes.Buffer
  json.NewEncoder(&b).Encode(data)

  raw,_ := json.Marshal(data)
  
  if b.String() == string(raw) {
    fmt.Println("same encoded data")
  } else {
    fmt.Printf("'%s' != '%s'\n",raw,b.String())
    //prints:
    //'{"key":1}' != '{"key":1}\n'
  }
}

JSONエンコーダーはストリーミング用に設計されています。JSONのストリーミングは通常、改行で区切られたJSONオブジェクトです。Encodeメソッドが改行文字を追加するのはこのためです。これはドキュメントに書いてある動作ですが、見落としたり忘れがちです。

JSONパッケージによるキーと文字列値の特別なHTML文字のエスケープ

これはドキュメントに書いてある動作ですが、知るためにはJSONパッケージのすべてのドキュメントを注意深く読まなくてはなりません。SetEscapeHTML メソッドの説明では &, <, > の文字のデフォルトのエンコーディング動作について説明しています。

これはGoチームによる非常に残念な設計上の決定です。いくつか理由があります。第一に、json.Marshal の呼び出しではこの動作を無効にすることはできません。第二に、すべてのWebアプリケーションをXSSの脆弱性から保護するためには、HTMLエンコーディングを実施するだけで十分であると考えているため、セキュリティ的に実装が不十分な機能です。データが利用されるコンテキストはたくさんあり、それぞれのコンテキストで独自のエンコーディング方法を必要とします。そして最後に、JSONの主なユースケースがWebページであることを想定しているため、設定ライブラリやREST/HTTP APIをデフォルトで壊してしまう、という残念な点があります。

package main

import (
  "fmt"
  "encoding/json"
  "bytes"
)

func main() {
  data := "x < y"
  
  raw,_ := json.Marshal(data)
  fmt.Println(string(raw))
  //prints: "x \u003c y" <- おそらく期待したものではないでしょう
  
  var b1 bytes.Buffer
  json.NewEncoder(&b1).Encode(data)
  fmt.Println(b1.String())
  //prints: "x \u003c y" <- おそらく期待したものではないでしょう
  
  var b2 bytes.Buffer
  enc := json.NewEncoder(&b2)
  enc.SetEscapeHTML(false)
  enc.Encode(data)
  fmt.Println(b2.String())
  //prints: "x < y" <- 良さそうですね
}

Goチームへの提案... オプトインにしてください。

JSON数値のインターフェース型へのデコード

デフォルトでは、JSONデータをインターフェース型にデコード/アンマーシャルするときに、GoはJSONの中に含まれる数値を float64 型として扱います。以下のコードはpanicを起こして失敗します。

package main

import (  
  "encoding/json"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status = result["status"].(int) //エラー
  fmt.Println("status value:",status)
}

実行時panic:

panic: interface conversion: interface is float64, not int

デコードしようとしているJSONの値が整数の場合、いくつかの選択肢があります。

選択肢1:浮動小数点の値をそのまま使う

選択肢2:浮動小数点の値を必要な整数型に変換する

package main

import (  
  "encoding/json"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status = uint64(result["status"].(float64)) //ok
  fmt.Println("status value:",status)
}

選択肢3:JSONをアンマーシャルするために Decoder 型を使い、Number インターフェース型を使ってJSON数値を表現するように指示する

package main

import (  
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  var decoder = json.NewDecoder(bytes.NewReader(data))
  decoder.UseNumber()

  if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status,_ = result["status"].(json.Number).Int64() //ok
  fmt.Println("status value:",status)
}

Number型の文字列表現を使って、別の数値型にアンマーシャルすることができます。

package main

import (  
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  var decoder = json.NewDecoder(bytes.NewReader(data))
  decoder.UseNumber()

  if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status uint64
  if err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status); err != nil {
    fmt.Println("error:", err)
    return
  }

  fmt.Println("status value:",status)
}

選択肢4:struct 型のフィールドに数値の値をマッピングしたい数値型を使う

package main

import (  
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result struct {
    Status uint64 `json:"status"`
  }

  if err := json.NewDecoder(bytes.NewReader(data)).Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  fmt.Printf("result => %+v",result)
  //prints: result => {Status:200}
}

選択肢5:値のデコードを後で実施する必要がある場合は、数値をマッピングする型として json.RawMessage 型を struct 型のフィールドに使う

この選択肢は条件によって型が変わったり構造が変わるJSONのフィールドをデコードをする必要があるときに便利です。

package main

import (  
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {  
  records := [][]byte{
    []byte(`{"status": 200, "tag":"one"}`),
    []byte(`{"status":"ok", "tag":"two"}`),
  }

  for idx, record := range records {
    var result struct {
      StatusCode uint64
      StatusName string
      Status json.RawMessage `json:"status"`
      Tag string             `json:"tag"`
    }

    if err := json.NewDecoder(bytes.NewReader(record)).Decode(&result); err != nil {
      fmt.Println("error:", err)
      return
    }

    var sstatus string
    if err := json.Unmarshal(result.Status, &sstatus); err == nil {
      result.StatusName = sstatus
    }

    var nstatus uint64
    if err := json.Unmarshal(result.Status, &nstatus); err == nil {
      result.StatusCode = nstatus
    }

    fmt.Printf("[%v] result => %+v\n",idx,result)
  }
}

16進数や非UTF8のエスケープシーケンスがJSON文字列値として使えない

Goは文字列の値がUTF8でエンコードされていることを期待しています。つまりJSON文字列に16進数でエスケープされた任意のバイナリデータを含めることができません。(バックスラッシュ文字もエスケープしなければなりません)。実際にはGoが受け継いだJSONの問題ですが、本記事で言及するに値するほど、Goのアプリケーションでよく起こる問題です。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\xc2"}`)
  var decoded config

  if err := json.Unmarshal(raw, &decoded); err != nil {
        fmt.Println(err)
    //prints: invalid character 'x' in string escape code
    }
  
}

16進数のエスケープシーケンスが含まれる場合、Unmarshal/Decodeの呼び出しは失敗します。文字列にバックスラッシュが必要な場合は、必ず別のバックスラッシュでエスケープしてください。16進数でエンコードされたバイナリデータを使いたい場合、バックスラッシュをエスケープしてから、JSON文字列内のデコードされたデータを使用して独自の16進数エスケープを行うことができます。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\\xc2"}`)
  
  var decoded config
  
  json.Unmarshal(raw, &decoded)
  
  fmt.Printf("%#v",decoded) //prints: main.config{Data:"\\xc2"}
  //todo: decoded.Dataに適した16進数エスケープをデコードします
}

別の選択肢として、JSONオブジェクトにバイト配列/スライスのデータ型を使用することもできますが、バイナリデータはbase64エンコードされている必要があります。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data []byte `json:"data"`
}

func main() {
  raw := []byte(`{"data":"wg=="}`)
  var decoded config
  
  if err := json.Unmarshal(raw, &decoded); err != nil {
          fmt.Println(err)
      }
  
  fmt.Printf("%#v",decoded) //prints: main.config{Data:[]uint8{0xc2}}
}

その他に気をつけなければならないのは、Unicode置換文字(U+FFFD)です。Goは無効なUTF8の代わりに置換文字を使用するので、Unmarshal/Decodeの呼び出しは失敗しませんが、得られる文字列の値は期待したものとは異なるかもしれません。

構造体、配列、スライス、マップの比較

構造体の変数を比較する場合、それぞれの構造体のフィールドが等価演算子で比較可能な場合は等価演算子 == を使用することができます。

package main

import "fmt"

type data struct {  
    num int
    fp float32
    complex complex64
    str string
    char rune
    yes bool
    events <-chan string
    handler interface{}
    ref *byte
    raw [10]byte
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}

構造体のフィールドのいずれかが等価演算子を用いて比較できない場合は、等価演算子を使用するとコンパイル時にエラーが発生します。配列が比較可能であるのは、配列のデータ項目が比較可能である場合のみであることに注意してください。

package main

import "fmt"

type data struct {  
    num int                //ok
    checks [10]func() bool //比較できません
    doit func() bool       //比較できません
    m map[string] string   //比較できません
    bytes []byte           //比較できません
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2)
}

Goは等価演算子を用いて比較できない変数を比較可能するためにいくつかのヘルパー関数を提供しています。

最も一般的な方法は reflect パッケージにある DeepEqual() を使うことです。

package main

import (  
    "fmt"
    "reflect"
)

type data struct {  
    num int                //ok
    checks [10]func() bool //比較できません
    doit func() bool       //比較できません
    m map[string] string   //比較できません
    bytes []byte           //比較できません
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}

しかし DeepEqual() が(アプリケーションにとって問題かどうかにかかわらず)遅いことは別にして、それ自体に問題点もあります。

package main

import (  
    "fmt"
    "reflect"
)

func main() {  
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}

DeepEqual() は空のスライスが "nil" スライスと等価であるとみなしません。bytes.Equal() 関数を使って得られる動作とは異なります。bytes.Equal() は "nil" スライスと空スライスは等価であるとみなします。

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}

また DeepEqual() はスライスの比較を常に完璧に行うとは限りません。

package main

import (  
    "fmt"
    "reflect"
    "encoding/json"
)

func main() {  
    var str string = "one"
    var in interface{} = "one"
    fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in)) 
    //prints: str == in: true true

    v1 := []string{"one","two"}
    v2 := []interface{}{"one","two"}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2)) 
    //prints: v1 == v2: false (okではありません)

    data := map[string]interface{}{
        "code": 200,
        "value": []string{"one","two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded)) 
    //prints: data == decoded: false (okではありません)
}

バイトスライス(や文字列)がテキストデータを含んでいる場合で、大文字小文字を区別せずに値を比較する必要があるとき、(==bytes.Equal()bytes.Compare() を使う前に)"bytes" パッケージや "strings" パッケージの ToUpper()ToLower() 関数を使いたくなるかもしれません。英語のテキストでは機能しますが、他の言語の場合は機能しません。strings.EqualFold()bytes.EqualFold() を代わりに使うべきです。

バイトスライスに(暗号化ハッシュ、トークンといった)シークレットが含まれており、ユーザが提供したデータに対して照合する必要があるときは reflect.DeepEqual()bytes.Equal()bytes.Compare() を使わないでください。アプリケーションが タイミング攻撃 に対して脆弱になるためです。タイミング情報の漏洩を避けるには、"crypto/subtle" パッケージの関数を使用してください(例: subtle.ConstantTimeCompare() など)。

panicの回復

panicをキャッチ/インターセプトするために recover() 関数が使われます。recover() の呼び出しは、遅延関数の中で行われた場合のみ、呼び出されます。

誤り:

package main

import "fmt"

func main() {  
    recover() //何もしません
    panic("not good")
    recover() //実行されません
    fmt.Println("ok")
}

修正後:

package main

import "fmt"

func main() {  
    defer func() {
        fmt.Println("recovered:",recover())
    }()

    panic("not good")
}

recover() の呼び出しは、遅延関数の中で直接呼び出された場合にのみ動作します。

失敗例:

package main

import "fmt"

func doRecover() {  
    fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}

func main() {  
    defer func() {
        doRecover() //panicは回復されません
    }()

    panic("not good")
}

スライス、配列、マップの"for-range"句での項目の更新と参照

"range" 句の中で生成されたデータ値は、実際のコレクション要素のコピーです。元の項目を参照しているわけではありません。つまり値を更新しても元のデータは変更されません。同様に、値のアドレスを取得しても元のデータへのポインタは与えられていません。

package main

import "fmt"

func main() {  
    data := []int{1,2,3}
    for _,v := range data {
        v *= 10 //元の項目は変更されていません。
    }

    fmt.Println("data:",data) //prints data: [1 2 3]
}

元のコレクションレコードの値を更新する必要がある場合は、インデックス演算子を使用してデータにアクセスします。

package main

import "fmt"

func main() {  
    data := []int{1,2,3}
    for i,_ := range data {
        data[i] *= 10
    }

    fmt.Println("data:",data) //prints data: [10 20 30]
}

コレクションがポインタ値を保持している場合、ルールは少し異なります。元のレコードが別の値を指すようにしたい場合は引き続きインデックス演算子を使う必要がありますが、ターゲットの場所が格納されている値を更新したい場合は、"for range" 句で取得できる2番目の値を使って更新できます。

package main

import "fmt"

func main() {  
    data := []*struct{num int} {{1},{2},{3}}

    for _,v := range data {
        v.num *= 10
    }

    fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}
}

スライスの"隠れた"データ

スライスから再スライスすると、新しいスライスは元のスライスの配列を参照します。この動作を忘れると、アプリケーションが一時的に大きなスライスを新規に生成して、そこから元のデータの小さな一部を参照する場合に、予期せぬメモリ使用量になる場合があります。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    return raw[:3]
}

func main() {  
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 <byte_addr_x>
}

この罠を避けるためには、(再スライスする代わりに)一時的に作成したスライスから必要なデータをコピーするようにしてください。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    res := make([]byte,3)
    copy(res,raw[:3])
    return res
}

func main() {  
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 <byte_addr_y>
}

スライスデータの"破損"

(スライスに格納されている)パスを書き換える必要があるとしましょう。それぞれのディレクトリを参照するようにパスを再スライスし、最初のフォルダ名を変更します。そして、新しいパスを生成するためにそれぞれのディレクトリ名を組み合わせます。

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (okではありません)

    fmt.Println("new path =>",string(path))
}

期待通りには動きませんでした。"AAAAsuffix/BBBBBBBBB"の代わりに"AAAAsuffix/uffixBBBB"になってしまいました。これは両方のディレクトリのスライスが、元のパスのスライスから同じ背後にある配列データを参照していたために起こりました。つまり元のパスも変更されています。アプリケーションによっては、これも問題になるかもしれません。

この問題は新しいスライスを割り当てて、必要なデータをコピーすることで解決できます。もう一つの選択肢は、フルスライス式を使用することです。

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex:sepIndex] //フルスライス式
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

    fmt.Println("new path =>",string(path))
}

フルスライス式の追加パラメータは、新しいスライスの容量を制御します。これにより、そのスライスに値を追加すると、2つ目のスライスのデータを上書きする代わりに、新しいバッファを割り当てるようになります。

"古く"なるスライス

複数のスライスは同じデータを参照できます。これは例えば既存のスライスから新しいスライスを生成する場合に起こります。この動作に依存するアプリケーションが適切に機能するためには、"古く"なるスライスを気にする必要があります。

元の配列が新しいデータを保持できないときにスライスにデータを追加すると、新しい配列が割り当てられます。このとき、元のスライスを参照している他のスライスは(古いデータがある)古い配列を指すようになります。

import "fmt"

func main() {  
    s1 := []int{1,2,3}
    fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]

    s2 := s1[1:]
    fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]

    for i := range s2 { s2[i] += 20 }

    //まだ同じ配列を参照しています
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [22 23]

    s2 = append(s2,4)

    for i := range s2 { s2[i] += 10 }

    //ここではs1は"古く"なっています
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [32 33 14]
}

型宣言とメソッド

既存の(非インターフェースの)型から新しい型を定義して型宣言するとき、既存の型に定義されているメソッドは継承されません。

失敗例:

package main

import "sync"

type myMutex sync.Mutex

func main() {  
    var mtx myMutex
    mtx.Lock() //エラー
    mtx.Unlock() //エラー  
}

コンパイルエラー:

/tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex has no field or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex has no field or method Unlock)

元の型のメソッドを必要とする場合、元の型を匿名フィールドとして埋め込んで、新しい struct 型を定義することができます。

修正後:

package main

import "sync"

type myLocker struct {  
    sync.Mutex
}

func main() {  
    var lock myLocker
    lock.Lock() //ok
    lock.Unlock() //ok
}

インターフェース型宣言でも同様にメソッドセットを保持します。

修正後:

package main

import "sync"

type myLocker sync.Locker

func main() {  
    var lock myLocker = new(sync.Mutex)
    lock.Lock() //ok
    lock.Unlock() //ok
}

"for-switch"や"for-select"のコードブロックにおけるbreak

ラベルがない "break" ステートメントは内部のswitchやselectブロックから抜けるだけです。"return"ステートメントが使えない場合、ループの外でラベルを定義することが次善の策です。

package main

import "fmt"

func main() {  
    loop:
        for {
            switch {
            case true:
                fmt.Println("breaking out...")
                break loop
            }
        }

    fmt.Println("out!")
}

"goto" ステートメントも上記のように動きます...

"for"文の中のイテレーション変数とクロージャ

これはとてもよくあるGoの落とし穴です。for ステートメントのイテレーション変数はそれぞれのイテレーションで再利用されます。つまり for ループ内のクロージャは同じ変数を参照しているということです(そしてgoroutineは実行され始めた時点でその変数の値を取得します)。

誤り:

package main

import (  
    "fmt"
    "time"
)

func main() {  
    data := []string{"one","two","three"}

    for _,v := range data {
        go func() {
            fmt.Println(v)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

(goroutineに対して変更がいらない)最も簡単な解決策は for ループのブロック内でローカル変数として現在のイテレーション変数の値を保存することです。

修正後:

package main

import (  
    "fmt"
    "time"
)

func main() {  
    data := []string{"one","two","three"}

    for _,v := range data {
        vcopy := v //
        go func() {
            fmt.Println(vcopy)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

別の解決策はパラメータとして現在のイテレーション変数を匿名のgoroutineに渡すことです。

修正後:

package main

import (  
    "fmt"
    "time"
)

func main() {  
    data := []string{"one","two","three"}

    for _,v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

もう少し複雑な罠を紹介します。

誤り:

package main

import (  
    "fmt"
    "time"
)

type field struct {  
    name string
}

func (p *field) print() {  
    fmt.Println(p.name)
}

func main() {  
    data := []field{{"one"},{"two"},{"three"}}

    for _,v := range data {
        go v.print()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

修正後:

package main

import (  
    "fmt"
    "time"
)

type field struct {  
    name string
}

func (p *field) print() {  
    fmt.Println(p.name)
}

func main() {  
    data := []field{{"one"},{"two"},{"three"}}

    for _,v := range data {
        v := v
        go v.print()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

このコードを実行すると何が表示されるか(理由も含めて)考えてみてください。

package main

import (  
    "fmt"
    "time"
)

type field struct {  
    name string
}

func (p *field) print() {  
    fmt.Println(p.name)
}

func main() {  
    data := []*field{{"one"},{"two"},{"three"}}

    for _,v := range data {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

遅延関数呼び出しの引数評価

遅延関数呼び出しの引数は defer ステートメントが評価されたときに評価されます(関数が実際に実行されるときではありません)。メソッド呼び出しを遅延する場合も、同様のルールが適用されます。構造体の値は明示的なメソッドのパラメータに加えて、非公開の変数も一緒に保存されます。

package main

import "fmt"

func main() {  
    var i int = 1

    defer fmt.Println("result =>",func() int { return i * 2 }())
    i++
    //prints: result => 2 (4を期待している場合は問題です)
}

ポインタのパラメータの場合、defer ステートメントが評価されたときにポインタのみが保存されるため、ポインタが示す値を変更することができます。

package main

import (
  "fmt"
)

func main() {
  i := 1
  defer func (in *int) { fmt.Println("result =>", *in) }(&i)
  
  i = 2
  //prints: result => 2
}

遅延関数呼び出しの実行

遅延呼び出しは格納されている関数の最後で実行されます(逆の順番で)。格納されているコードブロックの最後で実行されるわけではありません。新米Gopherが犯しやすい誤りとして、遅延呼び出しコードの実行ルールと変数スコープのルールを混同することがあります。これは長く実行される関数において、forループ内の各イテレーションでリソースのクリーンアップを遅延呼び出しで実行しようとする場合に、問題になる可能性があります。

package main

import (  
    "fmt"
    "os"
    "path/filepath"
)

func main() {  
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
            break
        }
        defer f.Close() //このコードブロックの最後では閉じられません。
        //ファイルを使った何かの処理...
    }
}

この問題を解決する一つの方法は、コードブロックを関数でラップすることです。

package main

import (  
    "fmt"
    "os"
    "path/filepath"
)

func main() {  
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:",target,"error:",err)
                return
            }
            defer f.Close() //ok
            //ファイルを使った何かの処理...
        }()
    }
}

もう一つの方法は defer ステートメントをなくすことです。

型アサーションの失敗

型アサーションに失敗すると、アサーションのステートメントが使われている対象の型の"ゼロ値"がリターンされます。変数のシャドーイングが混ざっている場合に、予期せぬ動作になる可能性があります。

誤り:

package main

import "fmt"

func main() {  
    var data interface{} = "great"

    if data, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",data)
    } else {
        fmt.Println("[not an int] value =>",data) 
        //prints: [not an int] value => 0 ("great" ではありません)
    }
}

修正後:

package main

import "fmt"

func main() {  
    var data interface{} = "great"

    if res, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",res)
    } else {
        fmt.Println("[not an int] value =>",data) 
        //prints: [not an int] value => great (期待したとおりです)
    }
}

ブロックされるgoroutineとリソースリーク

Rob Pike氏は2012年にあったGoogle I/Oで行われた "Go Concurrency Patterns" のプレゼンテーションの中で、いくつかの基本的な並行処理のパターンの話をしました。複数のターゲットから最初の結果を取得する話もその一つです。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

上記の関数はそれぞれの検索レプリカに対してgoroutineを開始します。それぞれのgoroutineはresultチャネルに検索結果を送信します。resultチャネルからは最初に送信された値が返ってきます。

その他のgoroutineからの結果はどうなるでしょうか?goroutine自体はどうなりますか?

First() 関数の中にあるresultチャネルはバッファがありません。つまり最初のgoroutineだけが結果を返します。その他のgoroutineは結果を送信しようとしてスタックします。検索レプリカが1よりも多い場合、その他のレプリカの呼び出しによってリソースがリークします。

リークを避けるためには、すべてのgoroutineが必ず終了するようにする必要があります。考えられる解決策の一つはすべての結果を保持できる十分な大きさがあるバッファ付きのresultチャネルを使うことです。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

その他の解決先は default 句がある select ステートメントと1つの値を保持できるバッファ付きのresultチャネルを使うことです。default 句はresultチャネルがメッセージを受信できない場合においてもgoroutineがスタックしないことを保証します。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,1)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        default:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

またワーカーを中断するためにキャンセル専用のチャネルを使うことができます。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }

    return <-c
}

なぜプレゼンテーションではこれらのバグを含んでいたのでしょうか?Rob Pike氏は単にスライドを複雑にしたくなかっただけです。これは理にかなっていますが、問題があるかもしれないと考えずにこのコードを使ってしまうような新米Gopherにとっては問題になる可能性があります。

異なるサイズ0の変数が同じアドレスになる

2つの異なる変数がある場合に、それらのアドレスは異なるのでしょうか?Goではそうでない場合もあります。変数のサイズが0の場合、メモリ上は全く同じアドレスを共有しているかもしれません。

package main

import (
  "fmt"
)

type data struct {
}

func main() {
  a := &data{}
  b := &data{}
  
  if a == b {
    fmt.Printf("same address - a=%p b=%p\n",a,b)
    //prints: same address - a=0x1953e4 b=0x1953e4
  }
}

iotaの始まりが常に0になるわけではない

iota 識別子はインクリメント演算子のようなものだと思うかもしれません。新しい定数を宣言して iota を使うと、最初は0が得られ、2回目に使うと1が得られる、といったものです。しかし必ずしもそうとは限りません。

package main

import (
  "fmt"
)

const (
  azero = iota
  aone  = iota
)

const (
  info  = "processing"
  bzero = iota
  bone  = iota
)

func main() {
  fmt.Println(azero,aone) //prints: 0 1
  fmt.Println(bzero,bone) //prints: 1 2
}

iota は実際には定数宣言のブロックに対して現在の行へのインデックス演算子なので、iota を最初に使うときに、定数宣言ブロックの最初の行でない場合は、初期値は0ではありません。

279
318
1

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
279
318

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?