20190627のGoに関する記事は5件です。

Go 標準ライブラリ tar パッケージの使い方

tar ファイルの書き込み

書き込むには,まず, Writer.WriteHeader でヘッダを書き込みます。
ヘッダには,最低限,Name(ファイル名)とSize(ファイルサイズ)を指定します。
なぜなら,Sizeを超えた内容を書き込むとエラーとなるため,Sizeを指定しないとファイル内容を全く書き込めません。
Sizeは,int64(len(ファイル内容))で指定するのが最も確実で簡単です。

    var buf bytes.Buffer
    tw := tar.NewWriter(&buf)
    body := "ファイル内容"
    // ヘッダ
    hdr := &tar.Header{
        Name: "ファイル名",
        Size: int64(len(body)),
    }
    // ヘッダを書き込む
    if err := tw.WriteHeader(hdr); err != nil {
        log.Fatal(err)
    }
    // ファイル内容を書き込む
    if _, err := tw.Write([]byte(body)); err != nil {
        log.Fatal(err)
    }
    // 閉じる
    if err := tw.Close(); err != nil {
        log.Fatal(err)
    }

複数のファイルを書き込む場合,WriteHeader と Write を繰り返します。
最後に, Close を呼び出して閉じます。

tar ファイルの読み込み

Reader の Next() を呼び出して, tar ファイル内の複数のファイルに一つずつアクセスします。
Next() でヘッダを読み込めます。
Reader は, io.Reader インターフェースを実装しているので,通常のファイルを読み込むのと同じように Reader が使えます。

tr := tar.NewReader(&buf)
for {
    hdr, err := tr.Next() // 最初のファイルを読み込む時にも,Next()を呼び出す必要がある。
    if err == io.EOF {
        break // アーカイブの終わり
    }
    if err != nil {
        log.Fatal(err)
    }
    // hdr.Name がファイル名
    // tr が io.Reader インターフェースを満たしているので, tr をそのまま使ってファイル内容を読み込む。
}

参考

archive/tar パッケージ
Package archive/tar

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ここがムズいよ並行処理

【概要】

この記事は DOG という初心者向けのGo言語の勉強会に用いた資料をQiita版として落とし込んだものです。
なので初心者向けの内容となっておりますので、あしからず。

またDOGの時の資料はこちらになります。

【ゴルーチン】

Go言語にはゴルーチンという機能があり、ゴルーチンを用いることで関数・メソッド・クロージャを並行処理として呼び出すことが可能です。ゴルーチンを使う場合は go キーワードを用います。

  • 関数の場合
go logging("Hello, %s\n", name)
  • メソッドの場合
go logger.Printf("Hello, %s\n", name)
  • クロージャの場合
go func() {
    log.Printf("Hello, %s\n", name)
}()

【並行処理の実行順番】

- 問題

唐突ですが問題です。以下のプログラムを実行すると何が出力されるでしょうか?

func main() {
    var data int
    go func() {
        data++
    }()
    if data == 0 {
        fmt.Printf("%v\n", data)
    }
}

選択肢) A. 0  B. 1  C. 何も出力されない

答えは、「時と場合による」でした。並行処理では実行順番の保証がないためdata変数にアクセスする順番が変わることがあります。それぞれの結果の場合の実行順番は以下の通りです。

  • 0と出力される場合

    1. if data == 0
    2. fmt.Printf("%v\n", data)
    3. data++
  • 1と出力される場合

    1. if data == 0
    2. data++
    3. fmt.Printf("%v\n", data)
  • 何も出力されない場合

    1. data++
    2. if data == 0

- 競合

このように順番が保証されていない状態を競合状態、特に変数に対する競合をデータ競合と言います。
並行処理に慣れていないエンジニアからするとプログラムは上から順番に実行されるという先入観があるため、知らず知らずに競合が発生するプログラムを実装し、バグを生み出しかねません。

【並行処理における実行順番の保証】

- スリープによる順番保証

唐突ですが再び問題です。以下のプログラムを実行すると何が出力されるでしょうか?

func main() {
    var data int
    go func() {
        time.Sleep(1 * time.Second) // 1秒間スリープする
        data++
    }()
    if data == 0 {
        fmt.Printf("%v\n", data)
    }
}

選択肢) A. 0  B. 1  C. 何も出力されない

答えは、「0が出力されやすくなった」です。data++の前で1秒間スリープするのだから、絶対に0が表示されるでしょ!と思えるかもしれませんが、何らかの要因により if data == 0 の比較処理に1秒以上要してしまった場合は何も出力されません。そんなことほとんど起きませんが、確実に起きないとは言えないためスリープでは順番の保証が行えず、あくまで順番が保たれやすくなっただけです。

- アトミック性

他のスレッドには割り込ませずに、今行なっている変更操作が確実に終わった後に実行される性質、言い換えるならばこれ以上処理を分解できない性質をアトミック性と言います。アトミック性はスレッドセーフの実現に重要な要素となります。

例えば、 i++ はアトミックでしょうか?
答えは、「アトミックではない」です。

i++は参照、計算、代入の3つに分解することが可能です。
参照している途中、計算している途中、代入している途中に他のスレッドからiを参照されるといったことはありませんが、参照してから計算するまでの間に他のスレッドからiを参照されることはあり得ます。

- sync.Mutex

i++ はアトミックではないと説明しましたが、syncパッケージのMutexを使うことでアトミックとすることが可能です。
Mutexはロックされたら、アンロックされるまで他の処理にロックを渡さないという機能です。
以下の例をご覧ください。

func main() {
    var data int
    var mu sync.Mutex
    go func() {
        mu.Lock()
        data++
        mu.Unlock()
    }()
    mu.Lock()
    if data == 0 {
        fmt.Printf("%v\n", data)
    }
    mu.Unlock()
}

Mutexがロックしているため、 data++ をしている途中に if data == 0 が行われるといったことは無くなりました。
これで順番を保証することができました。めでたしめでたし。
と言いたいのですが、まだ保証しきれていません。data++ が先に実行されるか、 if data == 0 が先に実行されるかは保証されていないため、出力結果は「0」と「何も出力されない」の2通りあります。

【考察】

func main() {
    var data int
    go func() {
        data++
    }()
    if data == 0 {
        fmt.Printf("%v\n", data)
    }
}

今回は紹介していませんがsyncパッケージによりMutexなどの同期処理が提供されています。が、そもそもこのプログラムを並行処理させる必要はありますでしょうか?
流石に今回の例は極端ではございますが、並行処理で実行すべき箇所、そうでない箇所を見極める能力が問われてくるのではないでしょうか?

【ここがムズいよ並行処理】

  • 副作用がある場合、順番を保証しないとバグの温床となる
  • syncパッケージを活用してアトミック性を意識する必要がある
  • 並行処理で実装すべきか見極める必要がある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語Slackライブラリ(nlopes)のRTM.SendMessageを使ってハマった話

はじめに

この記事はGo言語のSlackライブラリを用いてBotでメッセージを送信する方向けです。
使用ライブラリ nlopes/slack バージョン 0.5.0

メッセージ送信に不具合が?

Botは落ちないのにメッセージが送られていないことが今までに何度かあり、気になって調べてみたところ
日本語文字数が1334以上のメッセージを送信しようとする際に何もなかったようにメッセージが送信されなくなる事が発覚。
(ちなみにアルファベットだと4000文字でもしっかり発話してくれる?)
実際に自分のSlackアカウントで試したところ、4000文字までは日本語でも問題なく投稿できたので、Bot側に問題がありそうです。

最終的には解消できたのですが、いくつか罠があったので似たような問題で躓かないようにメモ書きとして残しておきます。

その1

ライブラリ側のメッセージ送信時の文字数判定が len(string) > 4000 という条件式になっていた
つまり、文字数ではなくバイト数で判定するというアレなことになっています

よくよく最新のライブラリを見ると修正されていた!
https://github.com/nlopes/slack/commit/9e37c90c58901e134c8bf22e97accc72eeab171e#diff-bdeefded79c795ae396d7cde5c980706
→Slackライブラリを最新(0.5.0)にアップデートにすることで解消。

ここで終わらなかった…

その2

ライブラリを最新にした後、同様にbotから日本語4000字送信させようとすると今度はログにエラーが発生
Error: websocket: close 1006 (abnormal closure): unexpected EOF

なんだこれ
調べてみたところ、未解決状態のIssueに辿りつきました。

そして、Issue上で議論された解決策は

rtmのwebsocket関連は、標準のSendMessageがサポートしていることすべてをサポートしているわけではない
slack.RTM.SendMessageではなく標準のSendMessageであるslack.Client.PostMessageに変更してはどうか

という内容。

確かに今は RTM.SendMessage を使って送信している…
実際に Client.PostMessage に変更したところ、4000文字の日本語を送信することに成功

変更前(RTM.SendMessage)
func SendMessage(rtm *slack.RTM, channel, text string) {
        msg := rtm.NewOutgoingMessage(text, channel)
        rtm.SendMessage(msg)
}
変更後(Client.SendMessage)
func SendMessage(client *slack.Client, channel, text string) {
        params := slack.PostMessageParameters {
                Markdown:    true,
                EscapeText:  true,
                AsUser:      true,
                UnfurlMedia: true,
                UnfurlLinks: true,
        }
        opt1 := slack.MsgOptionPostMessageParameters(params)
        opt2 := slack.MsgOptionText(text, false)

        client.PostMessage(channel, opt1, opt2)
}

まとめ

Go言語のライブラリを使用してSlackにメッセージを送る手段として
slack.Client.PostMessage
slack.RTM.SendMessage
の2通りありますが

大量の文字列送信を想定する場合は slack.RTM.SendMessage ではなく slack.Client.PostMessage を使うのが無難のようです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

machineryのDelayed Taskをテストで即時実行する

問題

Goのジョブキューであるmachineryを使っていて、
Delayed Taskによりタイマーで発動するような処理があるとき、テストでは即時に実行させたい。

machineryにはGetPendingTasksというインターフェースがあり、これでTaskを取得することができると思ったら、以下のIssueにあるように現状でDelayed Taskは実装的にGetPendingTasksで取得できないらしい。

GetPendingTasks does not return tasks having ETA · Issue #342 · RichardKnop/machinery
https://github.com/RichardKnop/machinery/issues/342

しょうがないのでgoのテストでタイムトラベルするときと同様のパターンで、
テストのときはETAがnilになるようにする
(もっとスジの良いやり方があったら教えてください!)

対処

1. ETAをフィルターする関数を定義

import (
    "time"
)

var FilterETAFunc = FilterETA

func FilterETA(eta *time.Time) *time.Time {
    return eta
}

func DisableETA() {
    FilterETAFunc = func(eta *time.Time) *time.Time {
        return nil
    }
}

2. TaskにETAを指定するときに必ずフィルタをかます

import (
  "github.com/RichardKnop/machinery/v1/tasks"
)

eta := time.Now().UTC().Add(time.Second * 5)
signature := &tasks.Signature{
  Name: "add",
  Args: []tasks.Arg{
    {
      Type:  "int64",
      Value: 1,
    },
    {
      Type:  "int64",
      Value: 1,
    },
  },
  ETA: FilterETAFunc(&eta)
}

asyncResult, err := server.SendTask(signature)

3. テストからはDisableETAを呼ぶ

some_test.go
func init() {
    utils.DisableETA()
}

4. Taskを実行する

GetPendingTasksしてworker.Processで実行する

func ProcessJobTasks(server *machinery.Server) error {
    worker := server.NewWorker("for_test", 1)
    tasks, err := server.GetBroker().GetPendingTasks(server.GetConfig().DefaultQueue)
    if err != nil {
        return err
    }
    for _, task := range tasks {
        err := worker.Process(task)
        if err != nil {
            return err
        }
    }
    return nil
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goのモジュール管理(vgo)

久しぶりにメンテしようと思ってみたものの、モジュール管理方法を忘れてググるところから始めないといけない...。覚えているからドキュメントはいらないと思っていた事もありました。そんな過去の自分にイラっとしたついでに調べてみたらモジュール管理が便利になっていたのでメモしておきます。

0.はじめに(苦労)

えっと、Godepsってディレクトリーがあるな...ということはgodepってやつか。 go get github.com/kr/godep っと。godep get...あれ、goなんてPC入れ替えてからコンパイルしてないけど、どこにインストールされたかな?go env。あー、ここか。/User/hoge/go/bin/dep getっと。

godep: [WARNING]: godep should only be used inside a valid go package directory and
godep: [WARNING]: may not function correctly. You are probably outside of your $GOPATH.
godep: [WARNING]:   Current Directory: /Users/hoge/dev/***
godep: [WARNING]:   $GOPATH: 
godep: Unable to find SrcRoot for package .

プッチーン。

「めんどくさいから一つづつgo getすりゃいいんだろ!」

1. Modules(vgo)を使ってみる

Go 1.11よりModules(vgo)という仕組みが標準装備になっているようです。

ということでやってみます。

go mod init
go: creating new go.mod: module hoge
go: copying requirements from Godeps/Godeps.json

なんと、秘伝のGodepsもちゃんと移行してくれたようです。

go.modとgo.sumというファイルが増えています。

使い方は?...そのまま実行するだけ?

go run hoge.go             
go: finding github.com/julienschmidt/httprouter v0.0.0-20160219154026-77366a47451a
go: downloading github.com/julienschmidt/httprouter v0.0.0-20160219154026-77366a47451a
go: extracting github.com/julienschmidt/httprouter v0.0.0-20160219154026-77366a47451a

これはなかなか便利です。go getでインストールするだけで、バージョン管理もきちんとされるようです。開発環境をdockerなどに展開していても、普通にbuildできるので非常に便利です。

2.モジュールのアップデートをする

使っているhttprouterは2016年のファイルっぽいですね...。動いた後の放置されっぷりがよくわかります。アップデートはgo get -uのようです。

go get -u
go: finding github.com/julienschmidt/httprouter v1.2.0
go: downloading github.com/julienschmidt/httprouter v1.2.0
go: extracting github.com/julienschmidt/httprouter v1.2.0

これで最新のものにアップデートすることができました。go.modファイルもきちんと書き変わっています。

その他

しばらくGo使っていなかったので、久しぶりの進化にびっくりしました。

参考

詳細については他の

https://qiita.com/propella/items/e49bccc88f3cc2407745
https://www.wantedly.com/companies/wantedly/post_articles/132270

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む