20201022のGoに関する記事は10件です。

Goでローカルサーバーを立てた~ファイルダウンロード~

初めに

Androidアプリからリクエストを投げて、GoでWebServerを立てたローカルサーバー上のファイルをダウンロードする

環境

PC Windows10
Android Studio 4.0
Kotlin 1.3.72
Android端末 Emulator(API Level 29)
Go 1.11.1

構成図

go_server.png
Androidアプリからファイル名を指定して、サーバー側にリクエスト、サーバー側に保存されているファイルとステータスコードを返却

実装

ローカルサーバー

Server.go
package controllers

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "strconv"
    "strings"
)

func apiSampleHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "POST":
        fmt.Println("POST")
    case "GET":                                        // GETのリクエストでファイルDL
        fmt.Println("GET")
        name := fmt.Sprint(r.URL)                      // 「/ファイル名」を取得
        fileName := strings.SplitAfter(name, "/")[1]   // 「ファイル名」を取得
        // ファイルがサーバー側に存在するかのチェック
        if fileinfo, err := os.Stat("./" + fileName); os.IsNotExist(err) || fileinfo.IsDir() {
            fmt.Println("file is not exist")
            w.WriteHeader(http.StatusNotFound)
            return
        }
        buf, err := ioutil.ReadFile("./" + fileName)   // ファイル読み出し 
        if err != nil {
            fmt.Println("file could not be read")
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.Write(buf)                                   // レスポンスに書き込む
    }
    w.WriteHeader(http.StatusOK)
}

// main.goから呼び出してサーバーを起動
func StartWebServer() error {
    http.HandleFunc("/", apiSampleHandler)
    return http.ListenAndServe(":8080", nil)
}

main.goを実行し、サーバーを起動
http://localhost:8080 にアクセスして、サーバーが起動していることを確認

ファイルを準備

image.png
ダウンロードする用のファイルをmain.goと同じディレクトリに配置

Android アプリ

1.Androidmanifest.xml

インターネットにアクセスするためのPermissionを定義

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

Android 9.0からhttpの通信の設定がOFFになるのでusesCleartextTraficをtrueに設定

AndroidManifest.xml
<application
     省略
    android:usesCleartextTraffic="true">
    <activity android:name=".MainActivity">
        省略
    </activity>
</application>

2.サーバーアクセス

http通信でサーバーにGETでアクセスする

Net.kt
   fun requestFileDownload(context: Context, requestUrl: String, fileName: String): Int {
        val url = URL("$requestUrl/$fileName.txt")     // URLオブジェクト生成
        // UrlConnectionオブジェクト生成
        val urlConnection = url.openConnection() as HttpURLConnection 
        var result = ""
        var responseCode = 0
        try {
            urlConnection.requestMethod = "GET"         // POST, GETなど
            urlConnection.doInput = true                // レスポンスボディの受信
            urlConnection.useCaches = false             // キャッシュの利用
            urlConnection.connect()                     // コネクションを確立

            responseCode = urlConnection.responseCode   // レスポンスコードを取得
            when (responseCode) {
                HttpURLConnection.HTTP_OK -> {          // レスポンスコードが200:OKの時だけ処理
                    val path = context.filesDir.toString() + "/" + fileName + FILE_EXPAND
                    // レスポンスの内容を読み出す
                    DataInputStream(urlConnection.inputStream).use { fileInputStream ->
                        // ファイルを作成し書き込む
                        DataOutputStream(BufferedOutputStream(FileOutputStream(path))).use {
                            val buffer = ByteArray(BUFFER_SIZE)
                            var byteRead: Int
                            do {
                                byteRead = fileInputStream.read(buffer)
                                if (byteRead == -1) {
                                    break
                                }
                                it.write(buffer, 0, byteRead)
                            } while (true)
                        }
                    }
                }
            }
        } catch (e: Exception) {
            Log.e("Net", "#startConnection$e")
        } finally {
            urlConnection.disconnect()
        }
        return responseCode
    }
}

3.IPアドレスの確認

アクセスしたいサーバー(PC)のIPアドレスをcommand promptで以下を実行し確認

>ipconfig

4.PCとAndroid端末を同一のネットワーク環境に接続

5.Androidにて接続

Net#requestFileDownloadのrequestUrlにhttp://{確認したIPアドレス}:8080を設定
Net#requestFileDownloadのfileNameにダウンロードするファイル名を設定し実行

ファイルのダウンロードを確認

アプリ固有領域(/data/data/{package_name}/files配下)にファイルが生成されていることを確認
Device File Explorer でデバイス上のファイルを表示する

最後に

全体ソースはこちら
アクセス先やRequestMethod、ファイル名はアプリで動的に変更できるようにしています。

サーバーへのファイルアップロードはこちら

参考文献

Goサーバー側

Package http
Go言語 (golang) ファイル・ディレクトリの存在チェック
[Golang] ファイル読み込みサンプル

Androidアプリ側

Java で HTTP でファイルをダウンロードして保存する方法

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

Goの基本型について

基本型

論理値型

論理値はbool型で表します。下記のように、真の場合はtrue、偽の場合はfalseの値をとる動作をします。また、型推論での定義もできます。

var a bool  //変数aをbool型で定義
a = true    //変数aにtrueを代入
a := true

数値型

数値型には整数型が用意されており、符合付き整数型符合なし整数型の2つの型が用意されています。

符合付き整数型はint8int16int32int64の型があり、数値がサイズを表しています。
符合なし整数型はuint8(byte)uint16uint32uint64の型があり、同様に数値がサイズを表しています。符合付きと違う点として、uint8byte型という別名でも定義することができ、uint8と同じ動作をします。

また、64bit実装のGoではint型はint64と同じ動作をしますが、下記のように、同じ符合付き64bit整数を表す方の変数同士であっても、型が異なるというところから、暗黙的な型変換はコンパイルエラーが発生してしまいます。

a := int(10)
b := int64(5)
b = a  //コンパイルエラー
fmt.Printf("%v\n", b)

このように、暗黙的に型変換をしようとしてしまうとコンパイルエラーが出てしまいます。しかし、下記のようにしっかり型を指定し、明示的に型変換を行えばコンパイルエラーは発生せずに実行することができます。

a := int(10)
b := int64(a)
fmt.Printf("%v\n", b)

この方法を使えば、型のサイズの最大値を超えた数値の処理が可能になります。

a := int8(128)  //コンパイルエラー
fmt.Printf("%v\n", a)
a := 128
b := int8(a)
fmt.Printf("%v\n", b)

int8の最大値である127を超えた128での処理ができるようになりました。これは、明示的に行うことで、最大値127に1が加わったときの2進数表現上でオーバーフロー(桁あふれ)が起こり、ラップアラウンドさせられ、値が最小値に戻るようになるからです。なので、この場合出力される値はint8の最小値である-128になるということです。

アップアラウンドの対策として、mathパッケージをインポートし、利用することで、桁あふれを意識しながらコードを書いていくことができます。

浮動小数点型

浮動小数点型にはfloat32: 単精度浮動小数点float64: 倍精度浮動小数点の2つの型が用意されています。ちなみに、float32はfloat64より精度が低く、メモリ使用効率の面を考慮しないのであれば、特別ない理由がない限り使用するメリットは少ないと思われます。

a := 1.0           //float64
b := float32(1.0)  //float32

また、浮動小数点型にも、mathパッケージによって数値範囲を表す定数が用意されていますし、浮動小数点リテラルを使って型の異なった値をとる場合には明示的な型変換を行う必要があります。しかし、小数点部は切り捨てられます。

次に、演算が不能になる特殊な値に関してです。下記の演算による出力結果として、それぞれ+Int(正の無限大)、-Int(負の無限大)、NaN(非数)という意味を持ちます。

1.0 / 0.0   //+Int
-1.0 / 0.0  //-Int
0.0 / 0.0   //NaN

複素数型

aが実部、bが虚部、iは虚数単位として、a + biというリテラルで表現されており、complex64complex128の2つの型が用意されています。それぞれ、complex64型の実部、虚部にはfloat32が使われており、complex128型の実部、虚部にはfloat64が使われています。

下記は、複素数リテラルを使用した代入方法です。ちなみに、+ 記号についてはあくまでリテラルの一部であって、演算子ではありません。また、complex関数を使い、実部、虚部それぞれの引数を取ることでそれぞれの値を返し、生成することもできます。

a := 1.0 + 2i
a := complex(1.0, 2)

このリテラルの、実部、虚部の値を取り出すための関数も用意されています。

a := 1.0 + 2i

real(a)  //1.0
imag(a)  //2

rune型

これは文字を表す型であり、シングルクォーテーションで囲まれた文字を、Unicodeコードポイントで表現し、出力します。また、バックスラッシュから始まるプリフィックスを付加することで、Unicode文字を表現できます。他にも、文字列表現のエスケープシーケンスも用意されています。別名int32と定義されており、同じ動作をします。

a := '例'
fmt.Printf("%v", a)  //20363
a := '\U00101234'
fmt.Printf("%v", a)  //1053236

文字列型

string型と定義されており、ダブルクォーテーションで囲まれた文字列リテラルが用意されています。

a := "例題"
fmt.Printf("%v", a)  //例題
a := "\u65e5本\U00008a9e"
fmt.Printf("%v", a)  //日本語

文字列リテラルにはもうひとつ、RAW文字列リテラルという形式があります。これは、バッククォーテーションを使い、複数行の文字列に対応したり、その文字に対して処理を行わないという性質があります。

a := '
例題
例題
例題
'
fmt.Printf("%v", a)  //例題
               例題
               例題

最後に

今回はGoの基本型についてまとめました!基本型そのものの表面だけではなく、意味や仕組みについてある程度踏み込んで理解できたのではないでしょうか?

この調子でアウトプット続けますー!

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

【Go言語】Elm ArchitectureでTUIアプリが作れるbubbleteaでちょっとリッチなToDoアプリを作る

はしがき

本記事ではElm Architecture likeにTUIアプリを作成できるbubbleteaというフレームワークを使ってToDoアプリを作っていきます。

タスク/完了したタスク一覧表示、追加、編集、完了、削除を行うことができます。
イメージ画像↓
イメージ画像

Elm Architectureについて

Elm Architectureを知らない方は公式ガイド(日本語訳)この記事をざっと読んでからにしたほうが理解がしやすいかと思います。(本記事ではElm Architectureの解説は殆どしません。)

筆者はElmを少し触ったことある程度なのでElm Architectureに対する理解が甘いです。
なので何か間違いがあればコメントやTwitterで指摘していただけると幸いです。

本記事で実装する機能

なお本記事ではタスク一覧表示と、タスクの追加までの解説(ガイド)にしようかと思っています。そこからあとはただただコード書き足してくだけなので。
適当にコードだけ読んで雰囲気を感じとりたい方、他の機能の実装の仕方を詳しく知りたい方、マサカリを投げてくださる方はyuzuy/todo-cliを参照してください!

それでは早速実装を始めていきましょう!

実装

筆者の環境

バージョン
OS macOS Catalina 10.15.7
iTerm 3.3.12
Go 1.15.2
bubbletea 0.7.0
bubbles 0.7.0

注: bubbleteaについて。このアプリを実装している途中にも破壊的変更が入ったので違うバージョンのbubbleteaを使う場合でバグが発生した場合はリポジトリを参照してください。

タスクの一覧表示

この節ではタスク一覧を表示してカーソルでタスクを選択できるところまで実装します。

最初にパッケージをgo getしましょう。

// bubbletea
go get github.com/charmbracelet/bubbletea

// utility
go get github.com/charmbracelet/bubbles

Model

まずタスクの構造を定義します。

main.go
type Task struct {
    ID        int
    Name      string
    IsDone    bool
    CreatedAt time.Time
}

ミニマムな構造になっているので他にもFinishedAtとか欲しい場合は追加しましょう。

次にmodelを定義します。Elm Architectureのモデルにあたるところです。
Elm Architectureではアプリの状態をモデルで管理します。
この章で
扱う状態はタスクの一覧とカーソルの位置の2つなので、実装は以下の通りになります。

main.go
type model struct {
    cursor int
    tasks  []*Task
}

これだけではbubbleteaがモデルとして扱ってくれません。
モデルとして扱うにはmodeltea.Modelを実装させる必要があります。

tea.Modelの定義↓

// Model contains the program's state as well as it's core functions.
type Model interface {
    // Init is the first function that will be called. It returns an optional
    // initial command. To not perform an initial command return nil.
    Init() Cmd

    // Update is called when a message is received. Use it to inspect messages
    // and, in response, update the model and/or send a command.
    Update(Msg) (Model, Cmd)

    // View renders the program's UI, which is just a string. The view is
    // rendered after every Update.
    View() string
}

まずは初期化関数であるInit()から実装していきますが、このアプリでは最初に実行するべきコマンドがないので、nilを返すだけで大丈夫です。
(modelstructの初期化は別で行います。)
(本記事ではコマンドを殆ど扱わないのでコマンドについてはあまり気にしないでよいです。気になる場合はElm公式ガイド(日本語訳)を参照してみるといいでしょう。)

main.go
import (
    ...
    tea "github.com/charmbracelet/bubbletea"
)

...

func (m model) Init() tea.Cmd {
    return nil
}

Update

Update()ではユーザーの操作(Msg)を元にmodelの状態を変化させます。

main.go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "j":
            if m.cursor < len(m.tasks) {
                m.cursor++
            }
        case "k":
            if m.cursor > 1 {
                m.cursor--
            }
        case "q":
            return m, tea.Quit
        }
    }

    return m, nil
}

tea.KeyMsg(ユーザーのキー操作)をハンドリングして、
"j"が押されたらcursorの値を1増やす。
"k"が押されたらcursorの値を1減らす。
"q"が押されたら、tea.Quitコマンドを返してアプリを終了することを定義しています。

カーソルが無限に上がったり下がったりすると困るのでm.cursor > 1などの条件を付けています。
ちなみに、case "j", "down"case "k", "up"とすると、矢印キーでもカーソルを上下に動かすことができるようになるので、お好みでどうぞ。

View

View()ではmodelを元に描画するテキストを生成します。
stringでゴリゴリ書いていきます。

main.go
func (m model) View() string {
    s := "--YOUR TASKS--\n\n"

    for i, v := range m.tasks {
        cursor := " "
        if i == m.cursor-1 {
            cursor = ">"
        }

        timeLayout := "2006-01-02 15:04"
        s += fmt.Sprintf("%s #%d %s (%s)\n", cursor, v.ID, v.Name, v.CreatedAt.Format(timeLayout))
    }

    s += "\nPress 'q' to quit\n"

    return s
}

カーソルはインデックス番号で数えていないのでカーソルがそのタスクを指しているかは、インデックスim.cursor-1を比較して判定しています。

これでタスクを表示するのに十分な材料が揃いました!
main関数を定義してアプリを起動できるようにしましょう!

main

main関数ではmodelstructの初期化、アプリの起動を行います。

main.go
func main() {
    m := model{
        cursor: 1,
        tasks:  []*Task{
            {
                ID:        1,
                Name:      "First task!",
                CreatedAt: time.Now(),
            },
            {
                ID:        2,
                Name:      "Write an article about bubbletea",
                CreatedAt: time.Now(),
            },
        }
    }

    p := tea.NewProgram(m)
    if err := p.Start(); err != nil {
        fmt.Printf("app-name: %s", err.Error())
        os.Exit(1)
    }
}

本来ならタスクはファイル等から読み込みますが、そこまで実装すると時間がかかるので今回はハードコーディングします。
tea.NewProgram()でプログラムを生成、p.Start()で起動します。

早速go runコマンドで実行しましょう!
タスク一覧が表示され、"j","k"キーでカーソルを上下に移動させることができるはずです!

タスクの追加

さて、タスクの一覧表示ができましたが、これではToDoアプリは名乗れそうにありません。
この節ではToDoアプリの最も大事な機能の一つ、タスクの追加を実装していきます。

Model

先程まではカーソルの位置、タスク一覧を保持するだけで大丈夫でしたが、タスクの追加を実装するにあたってユーザーからの入力を受け取り、保持するフィールドを設ける必要があります。

bubbleteaでテキスト入力を実装するにはgithub.com/charmbracelet/bubbles/textinputというパッケージを利用します。

main.go
import (
    ...
    input "github.com/charmbracelet/bubbles/textinput"
)

type model struct {
    ...
    newTaskNameInput input.Model
}

これだけではタスク一覧表示モード(以下ノーマルモード)とタスク追加モード(以下追加モード)の判別ができないので、modeというフィールドも追加します。

main.go
type model struct {
    mode int
    ...
    newTaskNameInput input.Model
}

modeの識別子も定義しましょう。

main.go
const (
    normalMode = iota
    additionalMode
)

Update

早速Update()関数の変更に入っていきたいところですが、タスクを追加するにあたって、足りない要素が1つ。
このToDoアプリではタスクのidを連番で管理しようと思っているので、最新のタスクのidを保持しておく必要があります。

グローバル変数として宣言、main()で初期化、Update()でインクリメント、という風に実装します。
(書いてる途中に思いましたが、modelのフィールドとして管理するのもありかもしれません。)

Twitter@ababupdownbaさんにご指摘いただいて、やっぱりmodelのフィールドにした方がいいぽいのでそっちで実装します。

main.go
type model struct {
    ...
    latestTaskID int
}

func main() {
    ...
    // 今回はハードコーディング祭りですが、ちゃんと実装する場合はファイル等から読み込むときなどで初期値を入れましょう。
    m.latestTaskID = 2
    ...
}

さて、本題のUpdate()ですが、追加モードではノーマルモードとは違い全ての文字キーを入力として扱うことになります。

そのためmodel.Update()とは別にmodel.addingTaskUpdate()を定義して、model.mode == additionalModeであればそちらで処理するようにします。

ノーマルモードでは"a"が押されたときに追加モードに変更するように、
追加モードでは"ctrl+q"が押されたときにノーマルモードに戻るように、"enter"が押されたときにタスクが追加されるようにしましょう。

main.go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    if m.mode == additionalMode {
        return m.addingTaskUpdate(msg)
    }

    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        ...
        case "a":
            m.mode = additionalMode
        ...
    }

    return m, nil
}

func (m model) addingTaskUpdate(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "ctrl+q":
            m.mode = normalMode
            m.newTaskNameInput.Reset()
            return m, nil
        case "enter":
            m.latestTaskID++
            m.tasks = append(m.tasks, &Task{
                ID  :      m.latestTaskID,
                Name:      m.newTaskNameInput.Value(),
                CreatedAt: time.Now(),
            })

            m.mode = normalMode
            m.newTaskNameInput.Reset()

            return m, nil
        }
    }

    var cmd tea.Cmd
    m.newTaskNameInput, cmd = input.Update(msg, m.newTaskNameInput)

    return m, cmd
}

m.newTaskNameInput.Value()でタイプされた値を取り出し、m.newTaskNameInput.Reset()でリセットしています。
input.Update()でキー入力をハンドリングしてm.newTaskNameInputを更新しています。

View

View()Update()と同じく処理を分ける必要があります。
ですがこれはたった6行の変更で実装できます!

main.go
func (m model) View() string {
    if m.mode == additionalMode {
        return m.addingTaskView()
    }
    ...
}

func (m model) addingTaskView() string {
    return fmt.Sprintf("Additional Mode\n\nInput a new task name\n\n%s", input.View(m.newTaskNameInput))
}

input.View()input.ModelView()用にstringへ変換してくれる変数です。

main

新たなフィールドmodenewTaskNameInputの初期化を加えましょう。

main.go
func main() {
    newTaskNameInput := input.NewModel()
    newTaskNameInput.Placeholder = "New task name..."
    newTaskNameInput.Focus()

    m := model{
        mode: normalMode,
        ...
        newTaskNameInput: newTaskNameInput,
    }
    ...
}

Placeholderは何も入力されていないときに表示する文字列です。

Focus()を呼び出すことによってそのinput.Modelがフォーカスされます。
これは複数の入力を一画面で扱うときなんかに使うらしいです。今回は2つ以上の入力をさせるわけではないのでおまじない程度に思っていただければ大丈夫だと思います。

さあ、これでタスクの追加も実装できました!
早速先程同様、go runコマンド等で実行してみましょう!

あとがき

本記事ではbubbleteaを用いたちょっとリッチなToDoアプリの作り方を解説しました。
思っていたより簡単に実装ができて、tigのような複雑なCLIツールを作ってみたかったけどはじめの一歩を踏み出せていなかった僕にとってはとても魅力的なフレームワークでした。

bubbleteaにはこれ以外にもリッチなTUIアプリケーションを作るための機能がたくさんあるのでぜひリポジトリを覗いてみてください!

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

GoのSentry出力にcustom errorを使用する

はじめに

go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。

それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-goCaptureException()ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
- pingcap/errors
- pkg/errors
- go-errors/errors

今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。

sentry-goのソースを読む

sentry-goには大きく以下の3つのCapture方法があり

基本的にはCaptureExceptionCaptureMessageを使用すると思いますが
ソースコードを読んで分かる通りCaptureException CaptureMessageではEventの作成のみオリジナルの処理で最終的には全てCaptureEventが呼ばれています。

今回Stacktraceをcaptureする処理として重要なのは
CaptureException内でEventのStacktraceを取得しているExtractStacktraceです。

見てわかる通り、reflectionで各error packageのStacktrace実装から決め打ちでStacktraceを取得しています。
要するにcustom errorで各packagesのStacktraceの実装と同じInterfaceを実装すればSentryでStacktraceを取得できるはずです。

custom errorをSentryに対応させる

元々作成していたcustom errorはpkg/errorsをベースに拡張されたものでしたのでpkg/errorsのStacktraceのInterfaceを実装していきます。

custom errorにpkg/errorsのStacktrace methodを実装する

Sentryがreflectionで呼んでいるcustom errorに実装すべきmethodはこちら
- StackTrace

// Frame represents a program counter inside a stack frame.
// For historical reasons if Frame is interpreted as a uintptr
// its value represents the program counter + 1.
type Frame uintptr
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// stack represents a stack of program counters.
type stack []uintptr

func (s *stack) StackTrace() StackTrace {
    f := make([]Frame, len(*s))
    for i := 0; i < len(f); i++ {
        f[i] = Frame((*s)[i])
    }
    return f
}

Frameはスタックトレースの各フレーム情報を指します。StackTraceはその集合体です。
上記を実装しただけではcustom errorのstackには何も情報を持たないため
errorが発生した際にgolangのruntime情報からFrameを作成する必要があります。

pkg/errorsの関数をそのまま使えればよかったのですがpkg/errorsでStacktraceを取得するcallersはprivateな関数であるためcustom errorにも同様の処理をそのまま実装する必要があります。
error作成時にStacktraceを取得する実装は以下の通り。

func callers() *stack {
    const depth = 32
    const skip = 4
    var pcs [depth]uintptr
    n := runtime.Callers(skip, pcs[:])
    var st stack = pcs[0:n]
    return &st
}

depthは取得するStacktraceの深さ、runtime.Callers() のパラメータの「4」はStacktraceにerror package内の情報までstackしないようにskipするstack数を示しています。
このskip数はerror packagesの実装により異なるのでcallers()を呼ぶまでのネストの数を調べましょう。

ちなみにGo1.7以上である場合Stacktrace(runtime.Frames)を取得するruntime.CallersFrames()関数が追加されている為そちらを利用する事も可能です。
https://golang.org/pkg/runtime/#example_Frames

Stacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。

error.go
type CustomError interface {
    Error() string
    Status() *status.Status
}

type customError struct {
    status  *status.Status
    *stack // ここでStacktraceのmethodを実装しているのがポイント
}

func NewCustomError(code codes.Code, message string, args ...interface{}) error {
    return newCustomError(nil, code, message, args...))
}

func newCustomError(code codes.Code, message string, args ...interface{}) error {
    s := status.Newf(code, message, args...)
    return &customError{s, callers()}
}

custom error以外のorigin errorのStacktraceを取得する

そのアプリ内でcustom errorしか使わない場合は上記の実装だけで問題ないですが
実際のアプリでは別のサブシステムやlibrary内で起きたoriginのerrorを保持する必要が出て来ると思います。
その場合、custom errorのstackはoriginのerrorのStacktraceを引き継がないといけません。

今回の場合サブシステム内で使用されているerror packageはpkg/errorsであるとしましょう。
pkg/errorsのStacktraceを取得する方法はpkg/errorsのソースコードを眺めているとコメントに細かく記載されています。
https://github.com/pkg/errors/blob/v0.9.1/errors.go#L66

// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface:
//
//     type stackTracer interface {
//             StackTrace() errors.StackTrace
//     }
//
// The returned errors.StackTrace type is defined as
//
//     type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
//     if err, ok := err.(stackTracer); ok {
//             for _, f := range err.StackTrace() {
//                     fmt.Printf("%+s:%d\n", f, f)
//             }
//     }
//
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.

上記を参考にしつつorigin errorであるpkg/errorsのStacktraceを取得してstackに詰めなおすには以下のように実装します。

error.go
type CustomError interface {
    Error() string
    Status() *status.Status
    Origin() error
}

type customError struct {
    status  *status.Status
    origin  error  // origin errorを格納する
    *stack
}

func NewCustomErrorFrom(origin error, code codes.Code, message string, args ...interface{}) error {
    return newCustomError(origin, code, message, args...))
}

func newCustomError(origin error, code codes.Code, message string, args ...interface{}) error {
    s := status.Newf(code, message, args...)
    if origin != nil {
        // https://github.com/pkg/errors
        type stackTracer interface {
            StackTrace() errors.StackTrace
        }
        if e, ok := origin.(stackTracer); ok {
            originStack := make([]uintptr, len(e.StackTrace()))
            for _, f := range e.StackTrace() {
                originStack = append(originStack, uintptr(f))
            }
            var stack stack = originStack
            return &applicationError{s, origin, &stack}
        }
    }
    return &CustomError{s, origin, callers()}
}

origin errorがpkg/errorsだった場合はpkg/errorsのStackTrace実装を呼び出してFrameを取得してからその値をプログラムカウンタの値に一度変換してstackに格納します。
勿論サブシステムがpkg/errors以外のerror packageを使用している場合そのpackage毎にStacktraceの実装は異なるので別途対応が必要です。

おわりに

特定のlibraryを拡張してcustom errorを実装すること自体は割と簡単に行えますが
Sentry等サードパーティ製のlibraryを使用してそのcustom errorを使用する際は多くのerror libraryのお作法に沿って作成してあげないと正常に動作しない事があります。
特にcustom errorを実装する際にはStacktraceをきちんと実装する事を忘れないように気をつけましょう。

おまけ

コード周りもう少し細かく書いたものをこちらに載せています。
https://zenn.dev/tomtwinkle/articles/18447cca3232d07c9f12

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

[Go]Sentryに対応したcustom errorの作り方

はじめに

go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。

それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-goCaptureException()ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
- pingcap/errors
- pkg/errors
- go-errors/errors

今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。

sentry-goのソースを読む

sentry-goには大きく以下の3つのCapture方法があり

基本的にはCaptureExceptionCaptureMessageを使用すると思いますが
ソースコードを読んで分かる通りCaptureException CaptureMessageではEventの作成のみオリジナルの処理で最終的には全てCaptureEventが呼ばれています。

今回Stacktraceをcaptureする処理として重要なのは
CaptureException内でEventのStacktraceを取得しているExtractStacktraceです。

見てわかる通り、reflectionで各error packageのStacktrace実装から決め打ちでStacktraceを取得しています。
要するにcustom errorで各packagesのStacktraceの実装と同じInterfaceを実装すればSentryでStacktraceを取得できるはずです。

custom errorをSentryに対応させる

元々作成していたcustom errorはpkg/errorsをベースに拡張されたものでしたのでpkg/errorsのStacktraceのInterfaceを実装していきます。

custom errorにpkg/errorsのStacktrace methodを実装する

Sentryがreflectionで呼んでいるcustom errorに実装すべきmethodはこちら
- StackTrace

// Frame represents a program counter inside a stack frame.
// For historical reasons if Frame is interpreted as a uintptr
// its value represents the program counter + 1.
type Frame uintptr
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// stack represents a stack of program counters.
type stack []uintptr

func (s *stack) StackTrace() StackTrace {
    f := make([]Frame, len(*s))
    for i := 0; i < len(f); i++ {
        f[i] = Frame((*s)[i])
    }
    return f
}

Frameはスタックトレースの各フレーム情報を指します。StackTraceはその集合体です。
上記を実装しただけではcustom errorのstackには何も情報を持たないため
errorが発生した際にgolangのruntime情報からFrameを作成する必要があります。

pkg/errorsの関数をそのまま使えればよかったのですがpkg/errorsでStacktraceを取得するcallersはprivateな関数であるためcustom errorにも同様の処理をそのまま実装する必要があります。
error作成時にStacktraceを取得する実装は以下の通り。

func callers() *stack {
    const depth = 32
    const skip = 4
    var pcs [depth]uintptr
    n := runtime.Callers(skip, pcs[:])
    var st stack = pcs[0:n]
    return &st
}

depthは取得するStacktraceの深さ、runtime.Callers() のパラメータの「4」はStacktraceにerror package内の情報までstackしないようにskipするstack数を示しています。
このskip数はerror packagesの実装により異なるのでcallers()を呼ぶまでのネストの数を調べましょう。

ちなみにGo1.7以上である場合Stacktrace(runtime.Frames)を取得するruntime.CallersFrames()関数が追加されている為そちらを利用する事も可能です。
https://golang.org/pkg/runtime/#example_Frames

Stacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。

error.go
type CustomError interface {
    Error() string
    Status() *status.Status
}

type customError struct {
    status  *status.Status
    *stack // ここでStacktraceのmethodを実装しているのがポイント
}

func NewCustomError(code codes.Code, message string, args ...interface{}) error {
    return newCustomError(nil, code, message, args...))
}

func newCustomError(code codes.Code, message string, args ...interface{}) error {
    s := status.Newf(code, message, args...)
    return &customError{s, callers()}
}

custom error以外のorigin errorのStacktraceを取得する

そのアプリ内でcustom errorしか使わない場合は上記の実装だけで問題ないですが
実際のアプリでは別のサブシステムやlibrary内で起きたoriginのerrorを保持する必要が出て来ると思います。
その場合、custom errorのstackはoriginのerrorのStacktraceを引き継がないといけません。

今回の場合サブシステム内で使用されているerror packageはpkg/errorsであるとしましょう。
pkg/errorsのStacktraceを取得する方法はpkg/errorsのソースコードを眺めているとコメントに細かく記載されています。
https://github.com/pkg/errors/blob/v0.9.1/errors.go#L66

// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface:
//
//     type stackTracer interface {
//             StackTrace() errors.StackTrace
//     }
//
// The returned errors.StackTrace type is defined as
//
//     type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
//     if err, ok := err.(stackTracer); ok {
//             for _, f := range err.StackTrace() {
//                     fmt.Printf("%+s:%d\n", f, f)
//             }
//     }
//
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.

上記を参考にしつつorigin errorであるpkg/errorsのStacktraceを取得してstackに詰めなおすには以下のように実装します。

error.go
type CustomError interface {
    Error() string
    Status() *status.Status
    Origin() error
}

type customError struct {
    status  *status.Status
    origin  error // origin errorを格納する
    *stack
}

func NewCustomErrorFrom(origin error, code codes.Code, message string, args ...interface{}) error {
    return newCustomError(origin, code, message, args...))
}

func newCustomError(origin error, code codes.Code, message string, args ...interface{}) error {
    s := status.Newf(code, message, args...)
    if origin != nil {
        // https://github.com/pkg/errors
        type stackTracer interface {
            StackTrace() errors.StackTrace
        }
        if e, ok := origin.(stackTracer); ok {
            originStack := make([]uintptr, len(e.StackTrace()))
            for _, f := range e.StackTrace() {
                originStack = append(originStack, uintptr(f))
            }
            var stack stack = originStack
            return &applicationError{s, origin, &stack}
        }
    }
    return &CustomError{s, origin, callers()}
}

origin errorがpkg/errorsだった場合はpkg/errorsのStackTrace実装を呼び出してFrameを取得してからその値をプログラムカウンタの値に一度変換してstackに格納します。
勿論サブシステムがpkg/errors以外のerror packageを使用している場合そのpackage毎にStacktraceの実装は異なるので別途対応が必要です。

おわりに

特定のlibraryを拡張してcustom errorを実装すること自体は割と簡単に行えますが
Sentry等サードパーティ製のlibraryを使用してそのcustom errorを使用する際は多くのerror libraryのお作法に沿って作成してあげないと正常に動作しない事があります。
特にcustom errorを実装する際にはStacktraceをきちんと実装する事を忘れないように気をつけましょう。

おまけ

コード周りもう少し細かく書いたものをこちらに載せています。
https://zenn.dev/tomtwinkle/articles/18447cca3232d07c9f12

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

DBコネクションプールをgolangで実現

初めに

GCPをつかっていてコネクションプールの話に行き着きました。
「最大コネクション数が100を超えました」というエラーが発生。
なぜコネクション数が多くなってしまうのかを調べ、見つけた対処方法となります。

環境

クラウド:GCP
WEBサーバーをVue/CloudRun
APIサーバーをGolang/CloudRun
DBサーバーをMySQL/CloudSQL

原因

APIを叩くたびにAPIサーバーとDBサーバー間で新しいコネクションができているから

と判明した。

つまり、コネクションの使い回しができていなかった。

対策

database.sqlの以下のメソッドを使用し、コネクションプールの設定を行う。
また、ORMでもコネクションプールできるらしい。

メソッド名 説明
func (db *DB) SetMaxOpenConns(n int) 接続の最大数を設定。 nに0以下の値を設定で、接続数無制限。
func (db *DB) SetMaxIdleConns(n int) コネクションプールの最大接続数を設定。
func (db *DB) SetConnMaxLifetime(d time.Duration) 接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。

以下のようにして実装。

DBconnection
func NewLocalDBConnection() error {
    /* ===== connect datebase ===== */
    // user
    user := os.Getenv("MYSQL_USER") 
    // password
    password := os.Getenv("MYSQL_PASSWORD") 
    // connection database
    database := os.Getenv("MYSQL_DATABASE") 
    // connection host
    host := "localhost" 
    // connection port
    port := "3306" 

    var err error
    DB, err = setupDB("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, password, host, port, database))
    if err != nil {
        return fmt.Errorf("sql.Open: %v", err)
    }

    return err
}

//this function is a function for connection pooling
func setupDB(Driver string, dsn string) (*sql.DB, error) {
    db, err := sql.Open(Driver, dsn)
    if err != nil {
        return nil, err
    }
    //コネクションプールの最大接続数を設定。
    db.SetMaxIdleConns(100)
    //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。
    db.SetMaxOpenConns(100)
    //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。
    db.SetConnMaxLifetime(100 * time.Second)

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

環境構築, Buildまで -Go編-

Goについて

静的型付けプログラミング言語として急速に人気が向上しているGo言語(Golang)。

1. インストール

https://golang.org/dl

go コマンドを使用する。
正常にインストールされていたら、以下のような結果が表示がされます。

Go is a tool for managing Go source code.

Usage:

    go &lt;command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help &lt;command>" for more information about a command.

2. 実際にコードをBuild

package main

import "fmt"

func main() {
    fmt.Printf("Hello World!")
}

Go言語はPHPやperlなどのインタプリタ言語とは違い、コンパイル言語です。
ファイルの実行をする前に必ずコンパイルをして、実行可能な状態にしなければなりません。

go build hello.go

//コンパイルが完了すると、「hello」というファイルが作成されます。
./hello

web業界出身だとPHPやRuby、javascriptといったコンパイルの必要がないインタプリタ言語がメインなので、コンパイル言語かつ静的型付け言語のGoを敬遠しがちかもしれません。が、習得は比較的簡単な言語といわれています。

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

Goで再帰クロシージャを実装

はじめに

Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。

再起クロシージャ

まず、クロシージャとはなのですが、下記に参考にした文を記載致します。

Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。

ということで、無名関数を表しています。今回はこれを使用し再起関数を作成してみることから始めました。

  1. 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := solveFact(a)
    fmt.Print(result)
}

func solveFact(x int) int {
    var fact func(n int) int
     fact = func(n int) int{
        if n == 0{ return 1 }
        return n * fact(n - 1)
    }
    return fact(x)
}

実行結果

❯ go run fact.go
4
24
  1. フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := solveFib(a)
    fmt.Print(result)
}

func solveFib(x int) int {
    var fib func(n int) int
    fib = func(n int) int{
        if n <= 1{return n}
        return fib(n -1) + fib(n - 2)
    }
    return fib(x)
}

実行結果

❯ go run fib.go
10
55

まとめ

入出力には、fmt.Scan()/fmt.Printf()を使用しています。ただ、入出力量が怖いので、bufioを使用し、バッファリングするようにしています。

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

Goで再帰クロージャを実装

はじめに

Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。

再帰クロージャ

まず、クロージャとはなのですが、下記に参考にした文を記載致します。

Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。

ということで、無名関数を表しています。今回はこれを使用し再帰関数を作成してみることから始めました。

  1. 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := solveFact(a)
    fmt.Print(result)
}

func solveFact(x int) int {
    var fact func(n int) int
     fact = func(n int) int{
        if n == 0{ return 1 }
        return n * fact(n - 1)
    }
    return fact(x)
}

実行結果

❯ go run fact.go
4
24
  1. フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := solveFib(a)
    fmt.Print(result)
}

func solveFib(x int) int {
    var fib func(n int) int
    fib = func(n int) int{
        if n <= 1{return n}
        return fib(n -1) + fib(n - 2)
    }
    return fib(x)
}

実行結果

❯ go run fib.go
10
55

まとめ

入出力には、fmt.Scan()/fmt.Printf()を使用しています。ただ、入出力量が怖いので、bufioを使用し、バッファリングするようにしています。

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

Goで再帰クロシージャを実装

はじめに

Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。

再起クロシージャ

まず、クロシージャとはなのですが、下記に参考にした文を記載致します。

Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。

ということで、無名関数を表しています。今回はこれを使用し再起関数を作成してみることから始めました。

  1. 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.go
package fact

import (
    "bufio"
    "fmt"
    "os"
)

func main() {

    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := fact(a)
    fmt.Print(result)
}

func fact (n int) int{
    if n == 0 { return 1 }
    return n * fact(n - 1)
}

実行結果

❯ go run fact.go
4
24
  1. フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a int

    fmt.Fscan(r, &a)

    result := fib(a)
    fmt.Print(result)
}

func fib (n int) int{
    if n <= 1 { return n }
    return fib(n -1) + fib(n - 2)
}

実行結果

❯ go run fib.go
10
55

まとめ

入出力には、fmt.Scan()/fmt.Printf()を使用しています。ただ、入出力量が怖いので、bufioを使用し、バッファリングするようにしています。(競プロ意識。。ではないですが。w)

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