- 投稿日:2020-10-22T23:34:07+09:00
Goでローカルサーバーを立てた~ファイルダウンロード~
初めに
Androidアプリからリクエストを投げて、GoでWebServerを立てたローカルサーバー上のファイルをダウンロードする
環境
PC Windows10
Android Studio 4.0
Kotlin 1.3.72
Android端末 Emulator(API Level 29)
Go 1.11.1構成図
Androidアプリからファイル名を指定して、サーバー側にリクエスト、サーバー側に保存されているファイルとステータスコードを返却実装
ローカルサーバー
Server.gopackage 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 にアクセスして、サーバーが起動していることを確認ファイルを準備
ダウンロードする用のファイルを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.ktfun 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で以下を実行し確認
>ipconfig4.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アプリ側
- 投稿日:2020-10-22T23:02:08+09:00
Goの基本型について
基本型
論理値型
論理値はbool型で表します。下記のように、真の場合はtrue、偽の場合はfalseの値をとる動作をします。また、型推論での定義もできます。
var a bool //変数aをbool型で定義 a = true //変数aにtrueを代入a := true数値型
数値型には整数型が用意されており、符合付き整数型と符合なし整数型の2つの型が用意されています。
符合付き整数型は
int8
、int16
、int32
、int64
の型があり、数値がサイズを表しています。
符合なし整数型はuint8(byte)
、uint16
、uint32
、uint64
の型があり、同様に数値がサイズを表しています。符合付きと違う点として、uint8
はbyte
型という別名でも定義することができ、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というリテラルで表現されており、
complex64
とcomplex128
の2つの型が用意されています。それぞれ、complex64型の実部、虚部にはfloat32
が使われており、complex128型の実部、虚部にはfloat64
が使われています。下記は、複素数リテラルを使用した代入方法です。ちなみに、+ 記号についてはあくまでリテラルの一部であって、演算子ではありません。また、complex関数を使い、実部、虚部それぞれの引数を取ることでそれぞれの値を返し、生成することもできます。
a := 1.0 + 2ia := complex(1.0, 2)このリテラルの、実部、虚部の値を取り出すための関数も用意されています。
a := 1.0 + 2i real(a) //1.0 imag(a) //2rune型
これは文字を表す型であり、シングルクォーテーションで囲まれた文字を、Unicodeコードポイントで表現し、出力します。また、バックスラッシュから始まるプリフィックスを付加することで、Unicode文字を表現できます。他にも、文字列表現のエスケープシーケンスも用意されています。別名int32と定義されており、同じ動作をします。
a := '例' fmt.Printf("%v", a) //20363a := '\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の基本型についてまとめました!基本型そのものの表面だけではなく、意味や仕組みについてある程度踏み込んで理解できたのではないでしょうか?
この調子でアウトプット続けますー!
- 投稿日:2020-10-22T16:02:26+09:00
【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/bubblesModel
まずタスクの構造を定義します。
main.gotype Task struct { ID int Name string IsDone bool CreatedAt time.Time }ミニマムな構造になっているので他にも
FinishedAt
とか欲しい場合は追加しましょう。次に
model
を定義します。Elm Architectureのモデルにあたるところです。
Elm Architectureではアプリの状態をモデルで管理します。
この章で
扱う状態はタスクの一覧とカーソルの位置の2つなので、実装は以下の通りになります。main.gotype model struct { cursor int tasks []*Task }これだけではbubbleteaがモデルとして扱ってくれません。
モデルとして扱うにはmodel
にtea.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を返すだけで大丈夫です。
(model
structの初期化は別で行います。)
(本記事ではコマンドを殆ど扱わないのでコマンドについてはあまり気にしないでよいです。気になる場合はElm公式ガイド(日本語訳)を参照してみるといいでしょう。)main.goimport ( ... tea "github.com/charmbracelet/bubbletea" ) ... func (m model) Init() tea.Cmd { return nil }Update
Update()
ではユーザーの操作(Msg)を元にmodel
の状態を変化させます。main.gofunc (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.gofunc (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 }カーソルはインデックス番号で数えていないのでカーソルがそのタスクを指しているかは、インデックス
i
とm.cursor-1
を比較して判定しています。これでタスクを表示するのに十分な材料が揃いました!
main
関数を定義してアプリを起動できるようにしましょう!main
main
関数ではmodel
structの初期化、アプリの起動を行います。main.gofunc 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.goimport ( ... input "github.com/charmbracelet/bubbles/textinput" ) type model struct { ... newTaskNameInput input.Model }これだけではタスク一覧表示モード(以下ノーマルモード)とタスク追加モード(以下追加モード)の判別ができないので、
mode
というフィールドも追加します。main.gotype model struct { mode int ... newTaskNameInput input.Model }
mode
の識別子も定義しましょう。main.goconst ( normalMode = iota additionalMode )Update
早速
Update()
関数の変更に入っていきたいところですが、タスクを追加するにあたって、足りない要素が1つ。
このToDoアプリではタスクのidを連番で管理しようと思っているので、最新のタスクのidを保持しておく必要があります。
グローバル変数として宣言、main()
で初期化、Update()
でインクリメント、という風に実装します。
(書いてる途中に思いましたが、model
のフィールドとして管理するのもありかもしれません。)
Twitterで@ababupdownbaさんにご指摘いただいて、やっぱりmodel
のフィールドにした方がいいぽいのでそっちで実装します。main.gotype model struct { ... latestTaskID int } func main() { ... // 今回はハードコーディング祭りですが、ちゃんと実装する場合はファイル等から読み込むときなどで初期値を入れましょう。 m.latestTaskID = 2 ... }さて、本題の
Update()
ですが、追加モードではノーマルモードとは違い全ての文字キーを入力として扱うことになります。そのため
model.Update()
とは別にmodel.addingTaskUpdate()
を定義して、model.mode == additionalMode
であればそちらで処理するようにします。ノーマルモードでは"a"が押されたときに追加モードに変更するように、
追加モードでは"ctrl+q"が押されたときにノーマルモードに戻るように、"enter"が押されたときにタスクが追加されるようにしましょう。main.gofunc (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.gofunc (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.Model
をView()
用にstring
へ変換してくれる変数です。main
新たなフィールド
mode
とnewTaskNameInput
の初期化を加えましょう。main.gofunc 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アプリケーションを作るための機能がたくさんあるのでぜひリポジトリを覗いてみてください!
- 投稿日:2020-10-22T15:50:50+09:00
GoのSentry出力にcustom errorを使用する
はじめに
go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors
等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-go
のCaptureException()
ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
- pingcap/errors
- pkg/errors
- go-errors/errors今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。
sentry-goのソースを読む
sentry-goには大きく以下の3つのCapture方法があり
- CaptureMessage : 文字メッセージの通知
- CaptureException : errorの通知
- CaptureEvent : custom可能なイベントの通知
基本的には
CaptureException
かCaptureMessage
を使用すると思いますが
ソースコードを読んで分かる通り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_FramesStacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。error.gotype 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.gotype 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
- 投稿日:2020-10-22T15:50:50+09:00
[Go]Sentryに対応したcustom errorの作り方
はじめに
go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors
等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-go
のCaptureException()
ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
- pingcap/errors
- pkg/errors
- go-errors/errors今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。
sentry-goのソースを読む
sentry-goには大きく以下の3つのCapture方法があり
- CaptureMessage : 文字メッセージの通知
- CaptureException : errorの通知
- CaptureEvent : custom可能なイベントの通知
基本的には
CaptureException
かCaptureMessage
を使用すると思いますが
ソースコードを読んで分かる通り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_FramesStacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。error.gotype 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.gotype 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
- 投稿日:2020-10-22T10:40:10+09:00
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以下の値を設定で、ずっと再利用可能。 以下のようにして実装。
DBconnectionfunc 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 }
- 投稿日:2020-10-22T08:36:36+09:00
環境構築, Buildまで -Go編-
Goについて
静的型付けプログラミング言語として急速に人気が向上しているGo言語(Golang)。
1. インストール
go コマンドを使用する。
正常にインストールされていたら、以下のような結果が表示がされます。Go is a tool for managing Go source code. Usage: go <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 <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」というファイルが作成されます。 ./helloweb業界出身だとPHPやRuby、javascriptといったコンパイルの必要がないインタプリタ言語がメインなので、コンパイル言語かつ静的型付け言語のGoを敬遠しがちかもしれません。が、習得は比較的簡単な言語といわれています。
- 投稿日:2020-10-22T00:39:46+09:00
Goで再帰クロシージャを実装
はじめに
Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。
再起クロシージャ
まず、クロシージャとはなのですが、下記に参考にした文を記載致します。
Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。
ということで、無名関数を表しています。今回はこれを使用し再起関数を作成してみることから始めました。
- 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.gopackage 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
- フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.gopackage 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を使用し、バッファリングするようにしています。
- 投稿日:2020-10-22T00:39:46+09:00
Goで再帰クロージャを実装
はじめに
Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。
再帰クロージャ
まず、クロージャとはなのですが、下記に参考にした文を記載致します。
Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。
ということで、無名関数を表しています。今回はこれを使用し再帰関数を作成してみることから始めました。
- 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.gopackage 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
- フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.gopackage 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を使用し、バッファリングするようにしています。
- 投稿日:2020-10-22T00:16:30+09:00
Goで再帰クロシージャを実装
はじめに
Go言語の勉強version2ということで、アルゴリズムの勉強をGoでやってみたというものです。
再起クロシージャ
まず、クロシージャとはなのですが、下記に参考にした文を記載致します。
Goの無名関数は「クロージャ」です。クロージャは日本語では「関数閉包」と呼ばれ、関数と関数の処理に関係する「関数外」の環境をセットにして「閉じ込めた(閉包)」ものです。
ということで、無名関数を表しています。今回はこれを使用し再起関数を作成してみることから始めました。
- 下記に階乗を計算する関数[func fact (n int) int]を作成してみました。
fact.gopackage 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
- フィボナッチ数列を計算する関数[func fib (n int) int]を作成してみました。
fib.gopackage 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)