20200703のGoに関する記事は7件です。

【Go言語】contextパッケージを理解する-概要編-

Go言語でAPI開発をしていて、一番多く使う標準パッケージと言っても過言ではないcontext。

その動作はなんとなく理解はしているものの、実際のコードを読んで深く理解したことがなかったため、今回は実際にコードやGoDocを読んで中身をまとめていこうと思います。

概要

contextは、Go1.7から標準パッケージに組み込まれています。

まずはGo公式ブログのcontextについてまとめられた記事のイントロをそのまま訳してみます。

In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.

Goサーバーでは、サーバーに来たリクエストはそれぞれそれ自身のゴルーチンで処理されます。 リクエストハンドラーはよくデータベースやRPCサービスといったバックエンドにアクセスするために追加でゴルーチンを起動します。 リクエストの処理を行っているゴルーチンは、通常エンドユーザーのアイデンティティや認証トークン、リクエストの期限などリクエスト固有の値へのアクセス権が必要です。 リクエストがキャンセルされたりタイムアウトした場合には、システムがそれらのゴルーチンが使っていたリソースを再度要求することができるように、 そのリクエストの処理を行っていたすべてのゴルーチンは素早く終了すべきです。

つまりcontextパッケージが担う役割は別サーバへのリクエストのように時間のかかる処理をgoroutineで実行する場合に、処理に適切なTimeoutやDeadlineを設定して処理が停滞するのを防ぐ役割やCancel行うことで適切にネットワークのリソースを開放する役割を担います。

さらにはそのgoroutineは別のgoroutineを呼び出しそれがまた別の…と呼び出しの連鎖は深くなることが考えられます。その場合も親のTimeoutに合わせてその子は全て適切にキャンセルされリソースは解放されるべきです。
また、そのほかにも認証情報などをkvとして受け渡す機能も備わっています。キャンセルのためのシグナルをAPIの境界を超えて受け渡すための仕組みである.ある関数から別の関数へと,親から子へと,キャンセルを伝搬させることが可能になっていっます。
また、ContextはAPIの境界を越えて期限とキャンセルシグナルとリクエスト固有の値を保持します。
メソッドは複数のゴルーチンから同時に呼び出されても安全なものとなっています。

さらにはCancelのシグナルをAPI挟んで受け渡すことで、関数間でキャンセルを伝達することができるようになっています。

要するに
- Goのサーバーで入ってきた各リクエストは個別のgoroutineで処理される。
- リクエストのハンドラは、データベースやRPCのサービスにアクセスする。
- リクエスト上で動作するgoroutineはユーザー識別子や認証トークン、リクエストの期限みたいな特定のリクエストの値にアクセスする必要がある。
- リクエストがタイムアウトされた時、システムがそのリソースを再度利用できるように、すべての動作しているゴルーチンも素早く終了されるべきである。

Contextインターフェイス

ここはcontextパッケージで定義されているインターフェイスをみていきます。

golang.org/x/net/contextにある

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Deadline returns the time when work done on behalf of this context
    // should be canceled. Deadline returns ok==false when no deadline is
    // set. Successive calls to Deadline return the same results.
    Deadline() (deadline time.Time, ok bool)

    // Done returns a channel that's closed when work done on behalf of this
    // context should be canceled. Done may return nil if this context can
    // never be canceled. Successive calls to Done return the same value.
    // The close of the Done channel may happen asynchronously,
    // after the cancel function returns.
    //
    // WithCancel arranges for Done to be closed when cancel is called;
    // WithDeadline arranges for Done to be closed when the deadline
    // expires; WithTimeout arranges for Done to be closed when the timeout
    // elapses.
    //
    // Done is provided for use in select statements:
    //
    //  // Stream generates values with DoSomething and sends them to out
    //  // until DoSomething returns an error or ctx.Done is closed.
    //  func Stream(ctx context.Context, out chan<- Value) error {
    //      for {
    //          v, err := DoSomething(ctx)
    //          if err != nil {
    //              return err
    //          }
    //          select {
    //          case <-ctx.Done():
    //              return ctx.Err()
    //          case out <- v:
    //          }
    //      }
    //  }
    //
    // See https://blog.golang.org/pipelines for more examples of how to use
    // a Done channel for cancellation.
    Done() <-chan struct{}

    // If Done is not yet closed, Err returns nil.
    // If Done is closed, Err returns a non-nil error explaining why:
    // Canceled if the context was canceled
    // or DeadlineExceeded if the context's deadline passed.
    // After Err returns a non-nil error, successive calls to Err return the same error.
    Err() error

    // Value returns the value associated with this context for key, or nil
    // if no value is associated with key. Successive calls to Value with
    // the same key returns the same result.
    //
    // Use context values only for request-scoped data that transits
    // processes and API boundaries, not for passing optional parameters to
    // functions.
    //
    // A key identifies a specific value in a Context. Functions that wish
    // to store values in Context typically allocate a key in a global
    // variable then use that key as the argument to context.WithValue and
    // Context.Value. A key can be any type that supports equality;
    // packages should define keys as an unexported type to avoid
    // collisions.
    //
    // Packages that define a Context key should provide type-safe accessors
    // for the values stored using that key:
    //
    //  // Package user defines a User type that's stored in Contexts.
    //  package user
    //
    //  import "context"
    //
    //  // User is the type of value stored in the Contexts.
    //  type User struct {...}
    //
    //  // key is an unexported type for keys defined in this package.
    //  // This prevents collisions with keys defined in other packages.
    //  type key int
    //
    //  // userKey is the key for user.User values in Contexts. It is
    //  // unexported; clients use user.NewContext and user.FromContext
    //  // instead of using this key directly.
    //  var userKey key
    //
    //  // NewContext returns a new Context that carries value u.
    //  func NewContext(ctx context.Context, u *User) context.Context {
    //      return context.WithValue(ctx, userKey, u)
    //  }
    //
    //  // FromContext returns the User value stored in ctx, if any.
    //  func FromContext(ctx context.Context) (*User, bool) {
    //      u, ok := ctx.Value(userKey).(*User)
    //      return u, ok
    //  }
    Value(key interface{}) interface{}
}

}

ここでインターフェイスの各メソッドを見ていこうと思います。

//いつこのcontextがキャンセルされるかを返す。
    Deadline() (deadline time.Time, ok bool)

//channelがクローズされていれば、その理由を返す。
    Err() error

//コンテキストがキャンセルされたりタイムアウトした場合にcloseされ、channelを返す   
    Done() <-chan struct{}

//同コンテキストでに保存した値のkeyを指定するとvalueを返す
    Value(key interface{}) interface{}

変数

//Canceled は,コンテキストがキャンセルされたときに Context.Err によって返されるエラー
var Canceled = errors.New("context canceled")

//DeadlineExceeded は,コンテキストの期限が過ぎたときに Context.Err によって返されるエラー
var DeadlineExceeded error = deadlineExceededError{}

type

CancelFunc は,作業を中止するよう指示します。 CancelFunc は,作業が停止するのを待ちません。 CancelFunc は,複数のゴルーチンから平行に呼び出すことができます。 最初の呼び出しの後, CancelFunc への後続の呼び出しは何もしません。

type CancelFunc func()

function

WithCancel ¶

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel は、新しい Done チャンネルを持つ親のコピーを返します。返されたコンテキストの Done チャンネルは、返された cancel 関数がコールされたとき、あるいは親コンテキストの Done チャンネルがクローズされたときのいずれか早いほうでクローズされます。

このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。

サンプルコード

https://play.golang.org/p/PUtYHTQbNpG

WithDeadline ¶

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

親コンテキストのデッドラインがすでに d よりも早くなっている場合は、WithDeadline(parent, d) は意味的に parent と同等です。返されたコンテキストの Done チャンネルは、デッドラインの期限が切れたとき、 返されたキャンセル関数が呼び出されたとき、または親コンテキストの Done チャンネルが閉じられたときのいずれか早いほうで閉じられます。

このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。

指定した時間が経過ではなく指定した時刻になったらcancelが走ります。

サンプルコード

https://play.golang.org/p/xdwLGigcY7U

WithTimeout ¶

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout は WithDeadline(parent, time.Now().Add(timeout)) を返します。
timeoutの指定付きのWithCancel。第2引数で指定した時間が経過するとcancelが走ります。

サンプルコード

https://play.golang.org/p/Rta-7ZzGFla

Background ¶

func Background() Context

Background は、null ではない空の Context を返します。キャンセルされることはなく、値もなく、期限もありません。これは通常、メイン関数、初期化、テストで使用され、 リクエストを受け取る際のトップレベルのコンテキストとして使用されます。

TODO ¶

func TODO() Context

TODO は、nullではないの空の Context を返します。どのコンテキストを使用するかが不明な場合や、まだ利用できない場合 (周囲の関数がまだ拡張されていないため、Context パラメータを受け付けることができません) には、コードは context.TODO を使用しなければなりません。

BackgroundとTODOの違いは?

機能的には何もありません。これらはビット単位で全く同じ値です。

違いは意図の違い。

context.TODO()で生成されるcontext.Contextは関数名のとおり、一時的なcontext.Contextになります。 GoDocにも「どのcontext.Contextを使うかわからないとき、他の関数が対応していなくてcontext.Contextが用意できないときに使ってね。」のように記載されている。

context.TODO() は、将来的にコンテキストを作成する必要がなくなることが予想される場合に使われることを意味している。

しかし、実際にはどちらも同じように動作します。

WithValue ¶

func WithValue(parent Context, key, val interface{}) Context

WithValue は、キーに関連付けられた値が val である親のコピーを返します。

context Values は、プロセスや API を通過するリクエスト・スコープされたデータにのみ使用し、パラメータを関数に渡す場合には使用しません。

提供されるキーは同等のものでなければなりません。また、コンテキストを使用するパッケージ間の衝突を避けるために、文字列型やその他の組み込み型であってはなりません。WithValue のユーザは、鍵の型を独自に定義しなければなりません。インターフェイス{}に代入する際の割り当てを避けるために、コンテキストキーはしばしば具象型の struct{} を持つことがあります。あるいは、エクスポートされたコンテキストキー変数の静的型はポインタかインターフェースでなければなりません。

サンプルコード

https://play.golang.org/p/ePfJSs9v14Z

以上がGoのcontextパッケージの概要になります。
Go playgroundの簡単なコードも置いているので、ぜひ動かしてみてください。

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

【Go言語】contextパッケージを理解する-実践編-

実際にContextを使ってみる

キャンセルの伝播

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // contextを生成
    ctx := context.Background()

    ctxParent, cancel := context.WithCancel(ctx)
    go parent(ctxParent, "Hello-parent")
    time.Sleep(5 * time.Second)

    cancel()
        time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

func parent(ctx context.Context, str string) {
    childCtx, cancel := context.WithCancel(ctx)
    go child(childCtx, "Hello-child")
    defer cancel()
    // 無限ループ
    for i := 1; i <= 1000; i++ {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err(), str)
            return

        case <-time.After(1 * time.Second):
            fmt.Printf("%s:%d sec..\n", str, i)
        }
    }
}

func child(ctx context.Context, str string) {
    // 無限ループ
    for i := 1; i <= 1000; i++ {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err(), str)
            return
        case <-time.After(1 * time.Second):
            fmt.Printf("%s:%d sec..\n", str, i)
        }
    }
}

https://play.golang.org/p/88dYnmE92yH

もちろんWithDeadlineやwithTimeoutを使うと明示的に指定した時間以上にかかっている場合にその処理をキャンセルすることが可能。

context導入によるコード比較

context利用なし

func handler(w http.ResponseWriter, r *http.Request) {
    doneCh := make(chan struct{}, 1)

    errCh := make(chan error, 1)
    go func() {
        errCh <- request(doneCh)
    }()

    // 別途goroutineを準備してTimeoutを設定する
    go func() {
        <-time.After(2 * time.Second)
        // Timeout後にdoneChをクローズする
        // 参考: https://blog.golang.org/pipelines
        close(doneCh)
    }()

    select {
    case err := <-errCh:
        if err != nil {
            log.Println("failed:", err)
            return
        }
    }

    log.Println("success")
}


func request(doneCh chan struct{}) error {
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}

    req, err := http.NewRequest("POST", backendService, nil)
    if err != nil {
        return err
    }
  
   errCh := make(chan error, 1)
    go func() {
        _, err := client.Do(req)
        errCh <- err
    }()

    select {
    case err := <-errCh:
        if err != nil {
            return err
        }


    case <-doneCh:
        // キャンセルが実行されたら適切にリクエストを停止して
        // エラーを返す.
        tr.CancelRequest(req)
        <-errCh
        return fmt.Errorf("canceled")
    }

    return nil
}

context利用

func handler(w http.ResponseWriter, r *http.Request) {

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    errCh := make(chan error, 1)
    go func() {
        errCh <- request3(ctx)
    }()

    select {
    case err := <-errCh:
        if err != nil {
            log.Println("failed:", err)
            return
        }
    }

    log.Println("success")
}

func request(ctx context.Context) error {
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}

    req, err := http.NewRequest("POST", backendService, nil)
    if err != nil {
        return err
    }

    // 新たにgoroutineを生成して実際のリクエストを行う
    // 結果はerror channelに投げる
    errCh := make(chan error, 1)
    go func() {
        _, err := client.Do(req)
        errCh <- err
    }()

    select {
    case err := <-errCh:
        if err != nil {
            return err
        }

    // Timeoutが発生する,もしくはCancelが実行されるとChannelが返る
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-errCh
        return ctx.Err()
    }

    return nil
}

valueにはどんな値を格納すべきか?

・map[interface{}]interface{}であり、完全に型安全ではなく、コンパイラでチェックできないということです。
・文字列やデータ構造体のような値を格納するためにコンテキストを使用し、ポインタやハンドルのような参照を格納するためにコンテキストを使用するのは避けたほうが良い
・リクエストのライフサイクルが始まる前に、そこに入れた情報がミドルウェアチェーンで利用可能かどうかも判断基準に

https://peter.bourgon.org/blog/2016/07/11/context.html

context使用上の注意とまとめ

  • 構造体の中には、Contextを保持してはならない。Contextが必要な関数には明示的に渡す事。また、Contextは第1引数であるべきで、だいたいはctxと名付ける

→contextを含んだpublicな構造体を定義すると、いろんなところからcontexを使えてしまうため、意図しないcancelや親子関係が複雑になってしまうから

  • 関数側が許容するとしても、nilのContextを渡してはいけない。どのコンテキスト渡して良いか確証が持てない時は、Context.TODOを使って空のContextを渡す。

  • 同じContextは別々に実行されているgoroutineで関数渡しても良い。Contextは複数のgoroutineから同時に使われても安全。

  • Context leakを避ける。WithCancelやWithTimeout,WithDeadlineで返されるcancelが呼ばれないと,その親Contextがcancelされるまでその子ContextがLeakする。→go vetで指摘してくれる

  • contextをもつ関数は適切なキャンセル処理を実装するべきである.この関数を使う側は呼び出し側(つまり親context)でTimeoutが発生した,もしくはCancelを実行した場合に適切にキャンセル処理・リソースの解放が実行されることを期待する。

  • ContextのValueはAPIやプロセスをまたぐリクエストスコープな値だけに使う。オプショナルな値を関数に渡すためではない。

  • contextはos,net,net/httpなど他のパッケージでも使われている

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

Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとした話

記事概要

Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。

筆者について

  • Golang初見。
  • linebot、herokuはPHPとRailsで触った事がある。
  • Dockerは便利すぎてハマりだしてる。

リポジトリ

https://github.com/YanaPIIDXer/QiitaLineBot
実際に動作させるherokuとLINEのアカウントは非公開で。。。
「どんなものを作るか」「設計はどうするか」はここのREADMEに。

採用したもの

  • gin
    • Webサーバライブラリ。
  • Realize
    • ホットリロード。
    • 後述。

Golangのホットリロード

「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。

導入

Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。

legacyの有効化

どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を

.realize.yaml
settings:
  legacy:
   force: true
(省略)

として再起動したら無事動作。

herokuへのdeploy

ポート

ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。

プログラム側の対応

環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。

Dockerfile
# 以下の記述を追加。
ENV PORT=${PORT}
main.go
// サーバ起動部分。
var port = os.Getenv("PORT")        // heroku上で動かす場合は指定されたPortじゃないと死なますよ。
if port == "" {
    port = "3000"
}
fmt.Println("Server Port:" + port)
router.Run(":" + port)

これでdeployしたら動・・・かなかった。

コンテナ構成の変更

色々調査しながら、ふとdocker-compose.ymlを眺めると・・・

docker-compose.yml
 (省略)
    ports:
      - "3000:3000"
    command: realize start --name='server' --run
 (省略)

・・・

docker-compose.yml
    ports:
      - "3000:3000"

お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?

色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイする

この記事にある通りにコンテナ構成を変更。
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。

herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。

SSL対応

「あれ?」と思っておもむろに
heroku logs
を実行。
アクセス時にエラーになってた。
で、そのログをよく見ると・・・

protocol=https

・・・FxxK!!
こりゃまた頭の痛い問題だ・・・。
(今ここ。進展あれば更新予定。)

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

Golang+Docker+herokuで何か作ろうとした話

記事概要

Golang+Docker+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。

筆者について

  • Golang初見。
  • herokuはPHPとRailsで触った事がある。
  • Dockerは便利すぎてハマりだしてる。

採用したもの

  • gin
    • Webサーバライブラリ。
  • Realize
    • ホットリロード。
    • 後述。

リポジトリ

https://github.com/YanaPIIDXer/QiitaLineBot
試行錯誤の経緯とかはここのコミットログを見れば大体分かる・・・かも。
ちなみに今回ブランチ分岐は無しの方向。

Golangのホットリロード

「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。

導入

Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。

legacyの有効化

どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を

.realize.yaml
settings:
  legacy:
   force: true
(省略)

として再起動したら無事動作。

herokuへのdeploy

ポート

ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。

プログラム側の対応

環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。

Dockerfile
# 以下の記述を追加。
ENV PORT=${PORT}
main.go
// サーバ起動部分。
var port = os.Getenv("PORT")        // heroku上で動かす場合は指定されたPortじゃないと死なますよ。
if port == "" {
    port = "3000"
}
fmt.Println("Server Port:" + port)
router.Run(":" + port)

これでdeployしたら動・・・かなかった。

コンテナ構成の変更

色々調査しながら、ふとdocker-compose.ymlを眺めると・・・

docker-compose.yml
 (省略)
    ports:
      - "3000:3000"
    command: realize start --name='server' --run
 (省略)

・・・

docker-compose.yml
    ports:
      - "3000:3000"

お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?

色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイする

この記事にある通りにコンテナ構成を変更。(Realizeはローカルでのみ動作させるように。)
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。

herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。
SSL絡みかと思ったが、普通にhttpでアクセスしても同じ結果だった。

No web processes running

ログをみた所、「No web processes running」と言うエラーメッセージが。
これを調べた所、以下の記事に行き着いた。
【Heroku】デプロイ後にcode=H14 desc="No web processes running"

記事にある通り、heroku ps:scale web=1を叩いたところ、動作はしなかったもののエラーメッセージが変わった。
(記事では「このコマンドを叩いただけでは動作せず、heroku.ymlが必要」との事だったが、自分はRealizeをローカルでのみ動作させるようにする過程で作成済みなのでこのコマンドだけでOKだった。)

App crashed

何かクラッシュしたらしい。
このエラーメッセージで調べたところ、以下の記事がヒット。
HerokuでApplicationErrorが発生したときの対処法

この記事に書かれていたコマンドで再起動。しかし変わらず。

そこからログを少し遡ると、.env Load Failed.
「・・・これってさ、.envの読み込み失敗時にそのままエラーとしてreturnしてた時のログだよね?herokuへのdeploy作業の過程でそのエラー処理は撤去したんだけど・・・」
「何かしらの原因で過去のやつが動作しているのでは?」と言う疑いが。

再度deployしてアクセス→router.Runメソッドが走るところまでは確認したがApp crashは解消されず→再度上記記事のコマンドで再起動→ .env Load Failed.

気になる事としては、「herokuへのdeploy時、releaseから処理が返ってこない」と言う事。(毎回Ctrl+Cしてる。)
「コマンドがフォアグラウンドで走ってるとか・・・?」とか考えて、適当にENTRYPOINTをlsに変えてみても結果は同じ。
で、ENTRYPOINTがlsのものが動いてる(・・・と思い込んでいる)状態で heroku logs
普通にGoのプログラムが動いていた。
・・・まぁ、「処理が返ってこない」と言いながらCtrl+Cで強制的に抜けてるんだからそりゃそうだわな。

アプリケーションの作り直し

とりあえず heroku destroy で一旦破棄。
heroku create で作り直す。
再度pushしてアクセスすると無事動作。
app crashedの原因はよく分からないが、一旦作り直してみるのも手ではある。(今回は「過去のプログラムが動作した」と言う事から、「ゴミが残っている」と言う事が明白だったので。)

ちなみにここでdeploy方法を git push heroku master から heroku container:push webheroku container:release web にも変更している。
(自分はdocker-composeを使ったビルドしかした事が無く、docker-composeを使わずにコンテナを単体で動かす時にどうやってコンテナ名を付けるのかが分からなかった為。。。これもここで覚えた。)

最後に

「GolangをDockerでインストールし、heroku上で動作させる」まででハマったところを色々まとめてみた。
冒頭に書いたリポジトリでのアプリケーション開発活動自体はまだまだ継続するものの、今回の表題である「Golang+Docker+heroku」での動作までは進み、「後はGolang内で色々と実装するだけ」と言う段階に至ったので、ひとまずこの記事は完結とする。
(それに伴い、余計な記述も撤去。)

ここまで来るのに数日を要したが、その分多くのことをインプットできたように思える。
そこでブチ当たった様々な問題とその解決策をなるべくリアルタイムで更新してきた。
「誰かに伝える事」は正直苦手だから色々と拙い所はあると思うが、こういった記事でも役に立ってくれれば幸いである。

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

GO言語学習メモ ~ Embeddedについて~ GOには継承がない

はじめに

完全初心者で、GO言語の学習を進めています。
学んだ事のメモとし記事を投稿します。

理解できていない部分が、多くありますが随時編集しますのでお手柔らかにお願いします。

継承について再確認

あるクラスに定義されたメソッドを、別の新規クラスで利用できるようにした上でクラスを定義することを継承と言う。

クラスの継承には、親クラスと子クラスの関係があり、元となるクラスを親クラス、親クラスのメソッドを引き継ぎ新しく作成するクラスを子クラスと呼ぶ。

簡単なイメージ図
image.png

ずっとrubyを使っていので、rubyでイメージ式

ruby
class 子クラス名 < 親クラス名

end

Goで継承みたいな事する

GO言語にはクラスがないので、上に書いた様な事ができないって事。

Goで継承で継承みたいな事をしたい時は、Embeddedの仕組みを使用できるみたいです。

実際に色を見てみる

package main

import "fmt"

//最初に定義=========
type Vertex struct {
    x,y int
}
func (v Vertex) Area() int{
    return v.x * v.y
}
func (v *Vertex) Scale(i int)  {
    v.x = v.x * i
    v.y = v.y * i
}
//===============

//注目する式①
type Vertex3D struct {
    Vertex
    z int
}
//注目する式②
func (v Vertex3D) Area3D() int{
    return v.x * v.y * v.z
}
//注目する式③
func (v *Vertex3D) Scale3D(i int)  {
    v.x = v.x * i
    v.y = v.y * i
    v.z = v.z * i

}
//注目する式④
func New (x,y,z int) *Vertex3D{
    return &Vertex3D{Vertex{x,y}, z}
}
//注目する式⑤
func main() {

    v := New(3,4, 5)
   //注目する式⑤
    v.Scale(10)
    fmt.Println(v.Area())
    fmt.Println(v.Area3D())

}

最初の定義した、x,yのint型を継承をみたいな事させたいと時、注目する式①の様な書き方をする事で元の定義したx,yのint型に、新たにz int型を加える事ができる。

Vertex3D定義し、中身に最初に定義したVertexを加える事で、Vertex3Dの中にx,y,z int型が定義された状態になる

//注目する式①
type Vertex3D struct {
    Vertex
    z int
}

あとは、func main()で呼び出す値を変えていけば、継承の様な事ができる。

終わりに

はじめに定義したものを、新しく定義するもの中に書いてあげれば上手くイメージなのかな。。

まだまだ、勉強が足りないですね、、、

内容は随時編集します!

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

go修行15日目 サードパーティ製パッケージを使う

サードパーティパッケージのインストール

  • 株価分析のためのパッケージ

https://github.com/markcheno/go-talib

インストール

go get github.com/markcheno/go-talib
go get github.com/markcheno/go-quote

インストールされる場所

PS C:\Users\yuta\go\src\github.com> ls .\markcheno\


    ディレクトリ: C:\Users\yuta\go\src\github.com\markcheno  


Mode                 LastWriteTime         Length Name      
----                 -------------         ------ ----       
d-----        2020/07/03      8:45                go-talib  

サンプルコード

package main

import (
    "fmt"
    "github.com/markcheno/go-quote"
    "github.com/markcheno/go-talib"
)

func main() {
    spy, _ := quote.NewQuoteFromYahoo("spy", "2016-01-01", "2016-04-01", quote.Daily, true)
    fmt.Print(spy.CSV())
    rsi2 := talib.Rsi(spy.Close, 2)
    fmt.Println(rsi2)
}

出力

datetime,open,high,low,close,volume
2016-01-04 00:00,200.49,201.03,198.59,184.03,222353500.00
2016-01-05 00:00,201.40,201.90,200.05,184.34,110845800.00
2016-01-06 00:00,198.34,200.06,197.60,182.01,152112600.00
2016-01-07 00:00,195.33,197.44,193.59,177.64,213436100.00

パッケージ自体を呼ばせない場合

  • importしたパッケージが呼ばれないとビルド時にエラーになるため
package main

import (
    "fmt"
    "github.com/markcheno/go-quote"
    _ "github.com/markcheno/go-talib"
)

func main() {
    spy, _ := quote.NewQuoteFromYahoo("spy", "2016-01-01", "2016-04-01", quote.Daily, true)
    fmt.Print(spy.CSV())
    // rsi2 := talib.Rsi(spy.Close, 2)
    // fmt.Println(rsi2)
}

godoc

  • ローカル環境でgodocを見ることができる
  • Example関数などで自分で書いたコードをExampleとしてみることができる

インストール

go get golang.org/x/tools/cmd/godoc

利用方法

godoc -http=:6060

アクセス

image.png

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

fluentdでdockerコンテナのログをファイルごとに出し分けてみたお話

fluentdとは

fluentdとはログ収集ツールです。
様々な形式のinputからログを収集でき、それを必要に応じて整形・加工してログ出力できます。

環境

  • OS:debian 10.3
  • docker:19.03.11
  • docker-compose:1.16.1
  • fluentd:1.3.2

今回の目標

今回の目標はサーバーのログを標準出力からファイル出力へ切り替えることです。
さらに、ファイル出力も全てのログを出力するログファイルと、エラーログのみを出力するエラーログファイルの2種類作成していきます。

fluentdの設定ファイルについて

デフォルトでは、 /fluentd/etc/fluent.conf が読み込まれます。

tag

fluentdではログをtagというもので管理していきます。
このtagを使って設定を任意のログのみに適用できたりします。

ディレクティブ

fluentdの設定はディレクティブと呼ばれるもので設定していきます。
ディレクティブには下記のようなものがあります。

  • source: ログの入力を設定するディレクティブ。
  • match: ログの出力を設定するディレクティブ。引数にtagを指定。
  • filter: ログに対して行う処理を設定するディレクティブ。引数にtagを指定。
  • label: @label でラベル名を設定することで、<label ラベル名> の設定にルーティングできる。

下記はmatchディレクティブの例です。

<match myapp.access>
  @type file
  path /var/log/fluent/access
</match>

myapp.access の部分がtagになります。
@type の部分が次に説明する、プラグインを指定しています。

設定ファイルの主なディレクティブの流れとしては、
< source > → < filter > や < label >など → ...... → < match >
となることを頭に入れておくと設定ファイルの読み書きがしやすくなると思います。
参考:ディレクティブ 公式ドキュメント

プラグイン

fluentdには標準のプラグインが豊富にあります。
プラグインは先ほどのディレクティブ内に @type で指定することで利用できます。
このプラグインを使うことで様々な形式でログを入出力できたり、ログを加工したりできるようになります。

下記は filterディレクティブ のプラグインの一部です。

  • record_transformer:ログにフィールドを追加したり削除したりできる。
  • grep:正規表現でマッチするログだけを通したり、マッチするログを出力しないようにしたりできる。
  • parser:ログ内のフィールドをjsonなどの形式を指定してパースすることができる。

参考:filterプラグイン 公式ドキュメント

今回使う各ファイル

ファイル階層

├── app
│   └── main.go
├── docker
│   ├── app
│   │   ├── app
│   │   └── Dockerfile
│   ├── docker-compose.yml
│   └── fluentd
│       ├── config
│       │   └── fluent.conf
│       ├── Dockerfile
│       └── log
│           ├── alert.buf
│           └── log.buf
└── Makefile

app/main.go

今回はこちらの超簡易サーバーを使っていきたいと思います。

main.go
package main

import (
    "fmt"
    "net/http"

    "github.com/sirupsen/logrus"
)

func main() {
    // server起動
    http.HandleFunc("/sample", outputQueryString)
    http.ListenAndServe(":8080", nil)
}

func outputQueryString(w http.ResponseWriter, r *http.Request) {
    log := logrus.New()
    // 処理開始のログ
    log.Infoln("Start!")

    // クエリストリングからキーが name の値を取得
    qs := r.URL.Query().Get("name")
    // キーが name の値がない場合のエラー処理
    if qs == "" {
        log.Errorln("Query string is nothing.")
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    // name があった場合の出力
    fmt.Fprintf(w, "Your name is %s", qs)
    log.Infoln("Complete!")
}

このサーバーのログをfluentdで収集します。
こちらのサーバーは、

  1. (IPアドレス):8080/sample?name=XXX にアクセスすると、「Your name is XXX」を返す
  2. (IPアドレス):8080/sample?key=XXX のようにクエリストリングのキーに name の値がない場合、エラーログを出力し、400エラーを返す

という処理内容になってます。

出力されるログは、

  1. アクセスされたときに処理開始のログ Start! を出力
  2. key が name のクエリストリングがなかった場合、エラーログ Query string is nothing. を出力
  3. 処理が正常終了した場合、処理完了のログ Complete! を出力

となっています。
なので、ログファイルには1, 2, 3のログが、エラーログファイルには2のログのみが出力されることになります。

docker/app/app

app/main.go の実行ファイル。
こちらをappのコンテナ内にコピーして実行します。

docker/app/Dockerfile

Dockerfile
FROM alpine:latest

COPY app /bin

CMD "app"

今回、appのdockerイメージはalpineを使用しています。
alpineを選択した理由はdockerイメージの容量が小さくなるからです。
詳しくはこちらの記事を参考にしてみてください。
Alpine Linux で Docker イメージを劇的に小さくする

Dockerfileの内容としては、先ほどのappの実行ファイルをコンテナ内の /bin ディレクトリにコピーして実行しているだけです。

docker/fluentd/config/fluent.conf (fluentd設定ファイル)

いよいよ、本命のfluentdの設定ファイルです。
ちょっと長いかもしれませんが、ブラウザバックするのは待ってください。
わかります。わたしも設定ファイルアレルギーの重症者ですから。
ですが、安心してください。
部分に分けて説明をしているので、どうか我慢して少し読み進めてみてください。
わからないところがあればコメントいただければできるだけ(アレルギー重症者レベルで)回答します。

fluent.conf
<source>
  @type  forward
  @label @mainstream
  port  24224
</source>

<label @mainstream>
  <match docker.**>
    @type copy
    <store>
      @type relabel
      @label @all_log
    </store>
    <store>
      @type relabel
      @label @err_log
    </store>
  </match>
</label>

<label @all_log>
  <match **>
    @type file
    path /var/log/fluent/log_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/log.buf
    symlink_path /var/log/fluent/log_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

<label @err_log>
  <filter **>
    @type grep
    <regexp>
      key log
      pattern /level=(warn|error)/
    </regexp>
  </filter>

  <match **>
    @type file
    path /var/log/fluent/alert_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/alert.buf
    symlink_path /var/log/fluent/alert_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

設定ファイルの内容説明

設定ファイルの内容を上から分けて見ていきます。

<source>
  @type  forward
  @label @mainstream
  port  24224
</source>

この sourceディレクティブでは、inputの設定を定義しています。
取得したログに対して @mainstream というラベルを設定しているので、この次は <label @mainstream> の設定に飛びます。

<label @mainstream>
  <match docker.**>
    @type copy
    <store>
      @type relabel
      @label @all_log
    </store>
    <store>
      @type relabel
      @label @err_log
    </store>
  </match>
</label>

この <label @mainstream> では、ログの複製を行っています。
@type copy でログの複製ができます。
なぜ複製しているのかというと、この後ログを、全てのログが出力されるログファイルと、エラーログだけが出力されるエラーログファイルに分けたいからです。
複製したログには @all_log@err_log というラベルを再設定しているので、
それぞれ <label @all_log><label @err_log> のディレクティブに飛びます。

<label @all_log>
  <match **>
    @type file
    path /var/log/fluent/log_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/log.buf
    symlink_path /var/log/fluent/log_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

この <label @all_log> では、ログファイルの出力設定を行っています。

  • path ではファイルの出力先を指定しています。* は後述の time_slice_format で指定したフォーマットで日付や日時が入ります。
  • buffer_type ではファイルに出力する前のバッファリングをfileに出力するか、メモリに溜めておくかを設定できます。メモリの方が処理は速いですが、fluentdが途中で落ちるとログが消失する可能性があります。
  • buffer_path ではバッファをfile出力にした際の出力先を指定しています。
  • symlink_path ではファイル出力しているバッファへのシンボリックリンクを設定しています。これにより、バッファが出力されているファイル名がどんなものであれ、同じファイル名でアクセスできるようになります。
  • time_slice_format ここで設定したフォーマットでファイル名の * が置き換えられます。デフォルトでは %Y%m%d なので日付単位でファイルが生成されますが、ここの設定を時間単位にすればファイルが1時間ごとに生成されるようになるので、ファイルの生成単位を設定しているとも言えます。
  • flush_at_shutdown では、fluentdがシャットダウンされた際に最後に1度だけバッファの内容を出力するかどうかを設定しています。trueなら出力を試します。
  • append では、既にファイル出力先ファイルがある場合、追記するか別ファイルに出力するかを設定しています。trueなら追記します。
<label @err_log>
  <filter **>
    @type grep
    <regexp>
      key log
      pattern /level=(warn|error)/
    </regexp>
  </filter>

  <match **>
    @type file
    path /var/log/fluent/alert_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/alert.buf
    symlink_path /var/log/fluent/alert_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

この <label @err_log> では、エラーログファイルの出力設定を行っています。
filterディレクティブでは、grepプラグインを使い、ログのフィルタリングを行っています。
grepプラグインでは、フィールドのキーを設定して、それに対して正規表現で通すログ通さないログを設定できます。
この設定では、 log というキーに対して level=warnlevel=error という値を持つログのみを通します。
matchディレクティブについては、上記ログファイル出力設定と重複するので割愛します。

以上が、fluent.confの設定内容です。
labelのおかげで流れを掴みやすく、他の設定ファイルより読みやすいのではと思います。

fluentdのDockerfile

Dockerfile
FROM fluent/fluentd:latest

dockerイメージで、fluentdを指定しているだけです。

アプリのcompose.yml

docker-compose.yml
version: "3.3"

services:
  app:
    build: ./app
    container_name: app
    restart: always
    ports:
      - "8080:8080"
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.{{.Name}}"
    depends_on:
      - fluentd

  fluentd:
    build: ./fluentd
    container_name: fluentd
    volumes:
      - ./fluentd/config:/fluentd/etc
      - ./fluentd/log:/var/log/fluent
    restart: always
    ports:
      - "24224:24224"

appの logging というところでログ出力先の設定を行っています。
こちらのように driver に fluentd を指定することで、fluentdでログを収集できるようになります。
optionsではfluentdのコンテナへの接続とログのtagの設定をしています。

fluentdのvolumesでは、2つのディレクトリをマウントしています。
1つめの ./fluentd/config では、デフォルトのfluent.confが格納してあるディレクトリをマウントすることで、設定ファイルを上書きしています。
2つめの ./fluentd/log では、fluentdのログファイルが出力されている /var/log/fluent をマウントすることで、ログファイルをローカルの /docker/fluentd/log のディレクトリから取得できるようにしています。

実際に起動してみる

それでは、実際に起動してみます。

$cd docker/; docker-compose up -d --build
Creating network "docker_default" with the default driver
Building fluentd
Step 1/1 : FROM fluent/fluentd:latest
 ---> 9406ff63f205

Successfully built 9406ff63f205
Successfully tagged docker_fluentd:latest
Building app
Step 1/3 : FROM alpine:latest
 ---> a24bb4013296
Step 2/3 : COPY app /bin
 ---> a57975cbc6b8
Step 3/3 : CMD "app"
 ---> Running in fa1f4446976e
Removing intermediate container fa1f4446976e
 ---> 515558e9745f

Successfully built 515558e9745f
Successfully tagged docker_app:latest
Creating fluentd ...
Creating fluentd ... done
Creating app ...
Creating app ... done

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
b76f4a1c2bdd        docker_app          "/bin/sh -c \"app\""     57 seconds ago      Up 56 seconds       0.0.0.0:8080->8080/tcp               app
b732ea83c8e7        docker_fluentd      "/bin/entrypoint.sh …"   58 seconds ago      Up 57 seconds       5140/tcp, 0.0.0.0:24224->24224/tcp   fluentd

fluentdとサーバーのコンテナが起動できました。
では、アクセスしてみましょう。
まずは、 (IPアドレス):8080/sample?name=Qiita でアクセスしてみます。
image.png
クエリストリングで指定した Qiita がちゃんと表示されてますね。

続いて、 (IPアドレス):8080/sample?key=Qiita でアクセスしてみます。
image.png

こちらもちゃんとエラーが返されてます。サーバーは意図通り動いてそうですね。

続いて、ログについて見ていきます。
log_yyyymmdd.log には全てのログが出力されます。
alert_yyyymmdd.log にはエラーログのみが出力されます。
実施日が2020/06/29なので yyyymmdd は 20200629 になります。

log_20200629.log
source:stderr   log:time="2020-06-29T14:18:06Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:06Z" level=info msg="Complete!"
source:stderr   log:time="2020-06-29T14:18:09Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."
alert_20200629.log
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."

log_20200629.log を見てみるとの1行目と3行目には処理開始のログが出力され、2行目と4行目にはそれぞれ正常終了時のログとエラー時のログがすべて出力されていますね。
一方、alert_20200629.log の方には log_20200629.log の4行目と同じログ(エラーログ)が出力されています。
これでエラー時のログのみ見たい場合は alert_20200629.log を見ればよくなりましたね。

おわりに

今回はfluentdの設定ファイルの概要と実際に使ってみた実例を紹介しました。
ここまで読んでいただいた方には、fluentdの設定ファイルがそこまで難解ではないというのが少しは伝わったのではないでしょうか?
設定ファイルアレルギーの重症者であるわたしでも簡単な設定ファイルについては記述できるようになりました。
みなさんもぜひこの記事をきっかけにfluentdデビューをしてスマートなログ収集ライフを!

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