- 投稿日:2020-12-15T20:30:02+09:00
[GitHub Actions] Goプログラムを定期実行する
Fringe81 アドベントカレンダー2020の15日目の記事です。
これまで何かのプログラムを定期実行させるときは、AWS LambdaとAWS CloudWatchを使ってましたが、GitHub Actionsだけでできそうだなと思ったのでやってみました。
GitHubにサンプルコードを公開しています。
実装
以下のようなYAMLファイルを
${root}/.github/workflowsディレクトリに配置し、${root}/main.goに実際に処理したいGoプログラムを配置します。github/workflows/job.yamlon: push: schedule: - cron: "*/5 * * * *" jobs: run: name: run runs-on: ubuntu-latest steps: # GOPATH等のセットアップ - name: Set Up uses: actions/setup-go@v2 with: go-version: ~1.15 # repositoryの中身にアクセスするためにチェックアウト - name: Checkout uses: actions/checkout@v2 # 実行 - name: Run run: go run main.go env: WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}定義ファイルの設定
詳しくはこちら
- on: ワークフローをトリガーするGitHubイベントの名前
- 上記ファイルの場合は、リポジトリにpushした時と5分に1回イベントが発火する。
- jobs: 各jobを設定
- 上記ファイルの場合は、以下のようなjobを設定した。
- セットアップ
- repositoryの中身に移動
- 実行(secretsから環境変数に代入し、プログラム側では
os.Getenv()で実際の値を取得)実装時にはまったこと
onの設定
サンプルコードでは、
pushとscheduleを設定しました。
pushはどのブランチでも実行されますが、scheduleはデフォルトまたはベースブランチの直近のコミットで実行されるので、デフォルトまたデフォルトまたはベースブランチ以外で作業している場合は注意が必要です。環境変数の設定
今回はサンプルとして、Slackにメッセージを送信するプログラムを用意しました(Slackにメッセージを送信するだけならcurlで良いが、今回はGoプログラムを実行したのであえてGoプログラムを用意した)。
Slackにメッセージを送信するためには、Webhook URLが必要になるが、このURLのように外部に公開したくない変数を用いる場合は、リポジトリの暗号化されたシークレットの作成を参考に環境変数を設定し、os.Getenv()を使う。定期実行できる最短の間隔は、5分に1回であり、厳密には設定できない
これはめっちゃハマった。
デバッグしようと思ってpushして10分たっても実行されずプログラムがおかしいのかなと思って時間を溶かしてしまった、、、定期実行するためにはcron構文を使う(参考)が、docにも記載がある通り、5分に1回よりも短く設定することはできません。また、5分に1回と設定(
*/5 * * * *)しても、5分に1回実行されるわけではありません(なんで?最大18分遅延した)。感想
時間を厳密にする必要のない場合は、コードの管理とcronの設定を同一リポジトリで行えるので便利だなと思いました。
cronの時間が厳密でない理由をどなたかご存知でしたら教えてください参考
- 投稿日:2020-12-15T20:23:33+09:00
GoでJsonをUnmarshalする際にプリミティブ型のデータ欠損を検知する方法
概要
タイトル通り、GoでJsonをUnmarshalする際にプリミティブ型のデータ欠損を検知する方法を紹介します。
TL;DR
データ欠損を検知したいフィールドをポインタ型で定義しよう。
説明
前提知識として、Goでは変数宣言時にゼロ値で初期化します。(参考記事: ゼロ値を使おう #golang)
==本題==
以下のような構造のJsonを受け取って処理を行うAPIがあるとします。
(具体的に、受け取ったjsonを元にユーザ情報を更新するAPIのようなものを想像してください)input.json{ "id": 4, "name": "木板 太郎", "age": 24, "profile": "僕はGopherくんがあまり好きではありません" }これをパースするGoプログラムは以下です。(httpサーバを実装するのが面倒だったので、そこは割愛します。)
type InputJson struct { ID int `json:"id"` Name string `json:"name"` Age int `json:"age"` Profile string `json:"profile"` } func main() { var input InputJson json.Unmarshal([]byte("{\n \"id\": 4,\n \"name\": \"木板 太郎\",\n \"age\": 24,\n \"profile\": \"僕はGopherくんがあまり好きではありません\" \n}"), &input) fmt.Printf("%+v\n", input) } // Output // {ID:4 Name:木板 太郎 Age:24 Profile:僕はGopherくんがあまり好きではありません}うまくパースできていますね。
今度は以下のJsonがリクエストされたと想定します。
{ "id": 5, "name": "禅 太郎", "age": 22, "profile": "最近の趣味はGopher君を愛でることです" }しかし、HTTP通信中に何らかの原因で、一部のフィールドが欠損してしまいました。
{ "id": 5, "name": "禅 太郎", "age": 22 }欠損したフィールドの値がnilなってくれれば、Goアプリケーション側でエラーを返すことができます。
しかし、Goでは先述した通り、フィールドにはゼロ値が設定されます。
この場合、欠損したprofileには空の文字列が設定されます。type InputJson struct { ID int `json:"id"` Name string `json:"name"` Age int `json:"age"` Profile string `json:"profile"` } func main() { var input InputJson json.Unmarshal([]byte("{\n \"id\": 5,\n \"name\": \"禅 太郎\",\n \"age\": 22 \n}"), &input) fmt.Printf("%+v\n", input) } // output // {ID:5 Name:禅 太郎 Age:22 Profile:} ← nilにならない!!これでは、意図的に空文字をリクエストしたのか、欠損による空文字なのかを判別できません。
このように、jsonをUnmarshalする際に欠損したデータがあった場合、そのフィールドにはnilを設定したいと思う場面があります。
解決策
以上の問題はフィールドをポインタ型にすることで解決することができます。
以下のように、リクエスト時にゼロ値を許容したくないフィールドをポインタで定義します。
type InputJson struct { ID int `json:"id"` Name string `json:"name"` Age int `json:"age"` Profile *string `json:"profile"` // 欠損して欲しくないフィールドをポインタ型にする } func main() { var input InputJson json.Unmarshal([]byte("{\n \"id\": 5,\n \"name\": \"禅 太郎\",\n \"age\": 22\n \n}"), &input) fmt.Printf("%+v\n", input) } // output // {ID:5 Name:禅 太郎 Age:22 Profile:<nil>} ← nilになった!このように、欠損したフィールドがnilに設定されるようになりました。
あとは
if input.Profile != nil {return fmt.errors.New("データが欠損してるよ")}のようにエラーを返すようにすれば、誤ったデータのまま更新が行われずにすみますね。結び
今回はGoでJsonをUnmarshalする際にプリミティブ型のデータ欠損を検知する方法の紹介でした。
正直、そもそもAPIの設計段階でゼロ値を許容しないようにすればいいじゃないかという気持ちもあります。。。(そもそも空文字や0が設定されない前提の設計にする)誤字脱字などありましたらコメントいただけると助かります。
- 投稿日:2020-12-15T16:09:51+09:00
Google Driveをコマンドラインで扱う-skicka編
これまで
gdrive(prasmussen/gdrive)を試していた。もうメンテナンスしてないので、skickaに変更(認証を通せなかった……)
skickaについては、ほとんど下記のすばらしい記事たちに書いてあります。屋上屋を重ねているようで恐縮……ですが、macOS Big Surでもやれた検証ということで……。
- インストール手順→【メモ】Skickaを使ってGoogleDriveに簡単アップロード
- 認証について→skickaの『「Google でログイン」機能が一時的に無効』を一時的に解決する
- 主なコマンド→GDriveをCLIで使う - skicka -
skickaのインストールから、コマンド例まで
goのインストール
- ダウンロードして、インストール(*1)
$ go versionでインストールされていることを確認skickaのインストール
- githubからインストール
$ go get github.com/google/skicka- パスを通す
$ export PATH=~/go/bin;$PATH- →
.bash_profileにexport PATH="/Users/xxxxx/go/bin:$PATH"追加- 初期化ファイル作成
$ skicka init認証を通す
- 下記でURLを取り、ブラウザに入力するが「Google でログイン機能が一時的に無効」と出てくる
$ skicka -no-browser-auth df- Client ID と Client Secret を取得 (*2)
~/.skicka.configを編集- 該当行のコメント(
;)を外すのを忘れててハマった!- たとえば
$ skicka lsとすると、ブラウザでアプリ認証がはじまる(1回だけ)コマンドラインで操作
ファイルリスト(ls)、最初は時間がかかった(1回だけ?)
$ skicka ls skicka: attempting to launch browser to authorize. (Re-run skicka with the -no-browser-auth option to authorize directly.) Updating metadata cache: [========================================] 100.00% 4m4s <ここに表示される>フォルダサイズを調べる
$ skicka du /xxx/yyy/アップロードとダウンロード
$ skika upload(download) /xxx/yyy/ ./zzz/気づいたこと
- 下記はさきにパースしてから、ダウンロードを開始してくれる(便利だ!)
- ファイル名が同じものはスキップされる
- Google Driveでは同じフォルダに同じファイル名のものが存在できるから
- Google Appsファイルはスキップされる
- ファイル名(フォルダ名)に
/が入っているものはスキップされる(そんなファイルはないと言われる)参考サイト
- GO:Download and install (*1)
- 本家:google/skicka
- GDriveをCLIで使う - skicka -
- 【メモ】Skickaを使ってGoogleDriveに簡単アップロード
- 認証:skickaの『「Google でログイン」機能が一時的に無効』を一時的に解決する (*2)
環境
skicka committed on 17 Jun
go version go1.15.6 darwin/amd64
macOS BigSur バージョン 11.0.1(20B50)
MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
- 投稿日:2020-12-15T16:01:59+09:00
google認証のrefresh_tokenの扱いでハマったポイント
はじめに
ある案件の要件に「Googleでログインしているユーザーで、スプレッドシートに帳票を出力してほしい」とありましたので、その実現のためにはまったポイントについて、refresh_tokenの「取り方」「保管の仕方」「使い方」の3つの観点で整理しようと思います。
サーバー側のロジックはGoで記述し、Webフレームワークは一部echoを使用しています。全体概要
解説のためmain.goに全て記述しました。
著書『Go言語によるWebアプリケーション開発』(以下、「教科書」)の内容を元に実装しています。package main import ( "fmt" "github.com/gomodule/redigo/redis" "github.com/google/uuid" "github.com/joho/godotenv" "github.com/labstack/echo" "github.com/stretchr/gomniauth" "github.com/stretchr/gomniauth/providers/google" "github.com/stretchr/objx" "golang.org/x/oauth2" google2 "golang.org/x/oauth2/google" "log" "net/http" "os" "strings" ) // 認証情報はRedisに保存する var conn redis.Conn /* CLIENT_ID プロジェクトのクライアントID SECRET_VALUE プロジェクトのシークレットValue REDIRECT_URL プロジェクトで設定したリダイレクトURL SECURITY_KEY 任意の文字列 */ func main(){ log.Println("server start...") _ = godotenv.Load() var err error conn, err = redis.Dial("tcp", os.Getenv("REDIS_CONN")) if err != nil{ panic(err) } // google認証機能の設定 gomniauth.SetSecurityKey(os.Getenv("SECURITY_KEY")) gomniauth.WithProviders( google.New(os.Getenv("CLIENT_ID"), os.Getenv("SECRET_VALUE"), os.Getenv("REDIRECT_URL")), ) e := echo.New() // ハンドラの登録 e.GET("/login", Login) e.GET("/callback", Callback) g := e.Group("") g.Use(AuthGuard()) // ログインしていないと見れないページ g.GET("/private", Private) // echoサーバーの起動 e.Logger.Fatal(e.Start(":1323")) } // Login GoogleログインのURLの発行処理 func Login(c echo.Context) error{ log.Println("Login is invoked") // ! refresh_tokenを取り出すときのポイント // provider, err := gomniauth.Provider("google") 教科書ではここでこれを使っているが、それだとScorpesをいじれないし、refresh_tokenも取れない。 // url, err := provider.GetBeginAuthURL(nil, nil) // 代わりにoauth2でconfigを直接いじる。 config := oauth2.Config{ ClientID: os.Getenv("CLIENT_ID"), ClientSecret: os.Getenv("SECRET_VALUE"), Endpoint: google2.Endpoint, RedirectURL: os.Getenv("REDIRECT_URL"), Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/spreadsheets", // スプレッドシートの使用権限を追加 }, } // こうするとログイン後にスプレッドシートの操作権限を渡して良いかの確認ダイアログが出るurlを取得でき、「refreshToken」が取れる。 url := config.AuthCodeURL(os.Getenv("SECURITY_KEY"), oauth2.AccessTypeOffline, oauth2.ApprovalForce) // 生成したurlを返す。フロントはこれにリクエスト、リダイレクトしてもらう。 return c.String(http.StatusOK, url) } /* googleログイン後はリダイレクトURLに対してクエリ「?code=XXXX」がついた状態でリダイレクトする 教科書では直接GoのCallback関数にリダイレクトさせるように記述されているが、 フロントとAPIでソースコードが別れている場合ではうまくいかなかった。 そのため、一度フロントにURLを返して、改めて再度queryにcodeの値をセットして、Goの/callbackにリクエストを投げてもらっている。 */ // Callback ログイン後のコールバック処理 func Callback(c echo.Context) error{ log.Println("Callback is invoked") provider, err := gomniauth.Provider("google") if err != nil{ return c.String(http.StatusInternalServerError, err.Error()) } // codeの値を取り出す。Googleの認可処理 code := c.QueryParam("code") cred, err := provider.CompleteAuth(objx.MustFromURLQuery("code=" + code)) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } // 変数「user」にGoogleで保持しているユーザー情報が取れる。access_token、emailと、アバターの画像、名前、jwtトークンに加えて、上記の対応によりここでrefresh_tokenが含まれるようになる。 user, err := provider.GetUser(cred) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } // refresh_tokenを保管するときのポイント // redisにその情報を保存するため、keyを生成し、cred,userからほしい情報だけ取り出して保存する。 uuID, err := uuid.NewRandom() if err != nil { panic(err) } uuidStr := uuID.String() refreshToken := fmt.Sprintf("%v", cred.Get("refresh_token")) jsonStr := fmt.Sprintf(`{"refresh_token":"%s","email":"%s"}`, refreshToken, user.Email()) log.Println(jsonStr) _, err = conn.Do("SET", uuidStr, jsonStr, "NX") if err != nil{ return c.String(http.StatusInternalServerError, err.Error()) } // フロントにredisのキーを返す。フロントは今後Header["Authorization"]に入れてリクエストしてもらう。 return c.String(http.StatusOK, uuidStr) } // Private ログインしていない場合、"success"は表示されない。 func Private(c echo.Context) error{ log.Println("Private is invoked") // context からトークンを取り出す token := c.Get("token").(string) log.Println(token) // tokenを表示させる。 return c.JSON(http.StatusOK, token) //return c.String(http.StatusOK, "success") } // AuthGuard ログイン状態の確認 func AuthGuard() echo.MiddlewareFunc{ return func(next echo.HandlerFunc) echo.HandlerFunc{ return func(c echo.Context) error{ log.Println("AuthGuard is invoked") // Header["Authorization"]からredisのキーを取り出す。 token := c.Request().Header.Get("Authorization") key := strings.ReplaceAll(token, "Bearer ", "") // redisから情報を取り出す。 res, _ := redis.String(conn.Do("GET", key)) if res == ""{ // ない場合、ログインが必要だと表示 return c.String(http.StatusUnauthorized, "login required") } // contextに値を詰めておく。 c.Set("token", res) // ログインしているので、その関数を実行する。 if err := next(c); err != nil{ return c.String(http.StatusInternalServerError, err.Error()) } return nil } } } // ! refresh_tokenの使い方のポイント // リフレッシュトークンを元にアクセストークンを取得する func getClient(refreshToken string) *http.Client { log.Println(model.IsInvoked()) urlValue := url.Values{ "client_id": {os.Getenv("CLIENT_ID")}, "client_secret": {os.Getenv("SECRET_VALUE")}, "refresh_token": {refreshToken}, "grant_type": {"refresh_token"}, } resp, err := http.PostForm("https://www.googleapis.com/oauth2/v3/token", urlValue) if err != nil { log.Fatalf("Error when renew token %v", err) } body, err := ioutil.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { log.Fatal(err) } var token oauth2.Token _ = json.Unmarshal(body, &token) config := oauth2.Config{} return config.Client(context.Background(), &token) }refresh_tokenの「取り方」のポイント
にてログイン機能の実装は解説されているのですが、その通りに実装すると、refresh_tokenを変数credの中から取得することができませんでした。(Login関数内のポイント①を参照)
そのため、config.AuthCodeURLのnilを渡している二箇所にそれぞれ、oauth2.AccessTypeOffline, oauth2.ApprovalForceを引数に渡すと、ユーザーがGoogleログインするときに「〇〇 が Google アカウントへのアクセスをリクエストしています」というダイアログを出すようになります。
すると、Callback関数のprovider.GetUser(cred)の返り値のなかに、refresh_tokenが含まれるようになります。refresh_tokenの「保管の仕方」のポイント
refresh_tokenが流出した場合、その人の権限でスコープで記述した範囲のことができてしまうので、ユーザーに直接見える形で管理するというのは避けなければなりません。
そのため、今回はuuID.String()で生成した値をキーに、メールアドレスと一緒にredisに保管しています。キーをフロントとやり取りするときはAuthorizationヘッダー内に入れます。refresh_tokenの「使い方」のポイント
いざ使うときにはrefresh_tokenをそのまま使うのではなく、その都度access_tokenを生成しなければなりません。
getClient関数にて、その記述をしました。さいごに
まだまだ経験の浅いエンジニアです。上記がベストプラクティスなのかは確証がありません・・・。
他にもっと良い方法があった場合、ぜひぜひおしえてください。
- 投稿日:2020-12-15T14:43:53+09:00
Go言語 XMLを変換してDB内を検索し、一致した情報をXML形式で返す
はじめに
自分はプログラミング歴5ヶ月の超初心者で、自分と同じような超初心者と自分の備忘録のために書いております。
もし、気になる点やもっといい方法があれば指摘していただけると幸いです。やること
今回は、Go言語でAPIを使ってXML形式のリクエストをDBに保存するの続きをやる。
DBに保存するだけじゃ物足りないので、保存したデータ内を別のキーワードでDB内を検索する機能と検索結果をXML形式で出力するところもやってみようと思った。(まだ物足りない)実装する機能
前回はXMLのデータをUnmarshalして保存するだけだったから今回は
・キーワードでリクエストを送信してXMLを受け取り、DB内を検索
・検索したキーワードとヒットしたキーワード別のヒット件数を出力
・一致したデータをXML形式に変換して出力
・ヒットしなかったらその旨を伝えてプログラムを終了するを実装する。
やってみる
今回も引き続き国会議事録検索システムを使っていく。
DB
検索したキーワードも保存した方がいいかなと思ってテーブル構造をちょっと変えた。
例によってid以外は面倒なので文字列にしてある。+---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int | NO | MUL | NULL | auto_increment | | issueid | varchar(255) | YES | | NULL | | | house | varchar(5) | YES | | NULL | | | date | varchar(255) | YES | | NULL | | | keyword | varchar(255) | YES | | NULL | | +---------+--------------+------+-----+---------+----------------+あと、検索対象が必要なので「コロナ」に加えて
・ワクチン
・菅義偉(人名を出すのは大丈夫なのか?)
・緊急事態宣言での検索結果もDBに保存しておいた。
リクエスト
ここは前回と同じ処理です。キーワードを変更するだけ。
HttpGetについては
Go言語でAPIを使ってXML形式のリクエストをDBに保存する
を参照してください。word := "ああ" eWord := url.QueryEscape(word) data := HttpGet("https://kokkai.ndl.go.jp/api/meeting_list?any=" + eWord)返すデータ
今回はヒットしたアイテムをDBに保存したときのキーワードも返したいので、取得した時とは別の構造体を定義する。
type Return struct { ID string `xml:"recordData>meetingRecord>issueID"` House string `xml:"recordData>meetingRecord>nameOfHouse"` Date string `xml:"recordData>meetingRecord>date"` Hitword string `xml:"recordData>meetingRecord>hitword"` } type ReturnList struct { XMLName xml.Name `xml:"data"` Recs []Return `xml:"records>record"` }検索する
DB接続、レコード取得部分等は省略。
今回はヒット件数、キーワード別のヒット件数も返すのでそれらを格納する変数と後でXMLに変換して出力するデータをmDataListにReturnList型で定義// ヒット件数 hit := 0 // ヒットしたレコードの検索時キーワード keywords := make([]string, 0) // 返すデータ mDataList := ReturnList{}検索して、ヒットすれば
hitに1を加算し、そのデータのID,House,DateとヒットしたレコードのキーワードをReturn型のスライスであるmDataList.Recsにappendする。
キーワード別の件数表示のためkeywordsにもappendする。
resultにはDBから取得したデータが前回内容のRecordList型で入っている。for _, rec := range result.Recs { for _, scan := range scanned { if rec.ID == scan.issueID { hit++ mData := Return{ ID: rec.ID, House: rec.House, Date: rec.Date, Keyword: scan.Hitword, } mDataList.Recs = append(mDataList.Recs, mData) keywords = append(keywords, scan.Keyword) } } }結果を出力する
ヒットしなかった場合の出力
os.Exit()はdeferを実行しないらしくpanicもGoexitもなんか違う気がするのですが、いい方法があれば教えていただきたいです。if hit == 0 { fmt.Println("ヒットしませんでした。") fmt.Printf("検索したキーワード: %v\n", word) os.Exit(1) }ヒットした場合の出力
mapを範囲ループするとGoでは順番が毎回異なってしまい、今回は順番が変わっても影響はないがなんとなく気持ち悪いので順番を固定しておく。// どのキーワードで何件ヒットしたか resultWords := make(map[string]int) for _, w := range keywords { resultWords[w]++ } // キーワード別件数の順序を保持するための処理 keys := make([]string, 0) for i, _ := range resultWords { keys = append(keys, i) } sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] }) // 結果をXML形式に変換 buf, err := xml.MarshalIndent(mDataList, "", " ") if err != nil { log.Fatal(err) } // 出力 fmt.Printf("%v件が見つかりました。\n検索したキーワード: %v\n", hit, word) fmt.Println("検索結果") for _, w := range keys { fmt.Printf("%v: %v件\n", w, resultWords[w]) } fmt.Println("\n以下、XML形式で結果をお知らせします。\n") fmt.Println(string(buf))実行結果
go run . 5件が見つかりました 検索したキーワード: ああ 検索結果 菅義偉: 2件 ワクチン: 2件 コロナ: 1件 以下、XML形式で結果をお知らせします。 <data> <records> <record> <recordData> <meetingRecord> <issueID>120115254X02020200529</issueID> <nameOfHouse>参議院</nameOfHouse> <date>2020-05-29</date> <keyword>コロナ</keyword> </meetingRecord> </recordData> </record> <record> <recordData> <meetingRecord> <issueID>120105254X01420200402</issueID> <nameOfHouse>衆議院</nameOfHouse> <date>2020-04-02</date> <keyword>ワクチン</keyword> </meetingRecord> </recordData> </record> <record> <recordData> <meetingRecord> <issueID>120105254X00720200227</issueID> <nameOfHouse>衆議院</nameOfHouse> <date>2020-02-27</date> <keyword>菅義偉</keyword> </meetingRecord> </recordData> </record> <record> <recordData> <meetingRecord> <issueID>120015254X00320191009</issueID> <nameOfHouse>参議院</nameOfHouse> <date>2019-10-09</date> <keyword>ワクチン</keyword> </meetingRecord> </recordData> </record> <record> <recordData> <meetingRecord> <issueID>119805254X03120190621</issueID> <nameOfHouse>衆議院</nameOfHouse> <date>2019-06-21</date> <keyword>菅義偉</keyword> </meetingRecord> </recordData> </record> </records> </data>終わりに
読んでいただきありがとうございました。
もっと深い知識で記事を書けるようになりたいです。
- 投稿日:2020-12-15T00:01:24+09:00
Goで手早くJSON APIを構築してみた
※この記事は自身のブログからの転載です。
Goで手早くJSON APIを構築してみた | outputableGoで手早くJSON APIを構築してみたのでメモっておく。
環境構築
手短にいきたいのでDockerで。
軽量なalpineを選択。
コンテナ側の使用PORTも忘れず開放しておく。
ビルダーイメージ側のプラットフォームも実行側イメージに合わせておかないとGoサーバが起動しないので注意。
- Dockerfile
FROM golang:1.15.3-alpine3.12 as builder ENV GO111MODULE=on ENV GOPATH= WORKDIR /usr/src/app COPY . ./ RUN go mod init gochi1 && go get && go build . FROM alpine:3.12 as gochi1 COPY --from=builder /usr/src/app . EXPOSE 8080 CMD ["./gochi1"]
- .dockerignore
イメージ化の際避けたいファイルなどを指定しておく。
.gitignore go.mod go.sum
ここからgo moduleに準じたプロジェクトを作る。$ mkdir gochi1 $ cd gochi1 $ touch main.goプログラムの構築
サーバーを作る。
ライブラリで「chi」というのがあったので今回はそれを利用してみる。
- main.go
package main import ( "gochi1/resources" "net/http" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "os" "fmt" ) func main() { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.BasicAuth("secret-room", map[string]string{"user1": "value1"})) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ベタ打ち")) }) r.Mount("/users", resources.UsersResource{}.Routes()) port := os.Getenv("PORT") if port == "" { port = "8080" } http.ListenAndServe(fmt.Sprintf(":%s", port), r) }
- resources/todos.go
ロジックの分離。
DBの代わりにグローバル変数で状態変化を再現している。package resources import ( "net/http" "github.com/go-chi/chi" "encoding/json" "io/ioutil" "reflect" ) type Response2 struct { Name string `json:"name"` Todos []string `json:"todos"` } var state Response2 type UsersResource struct {} func (ur UsersResource) Routes() chi.Router { r := chi.NewRouter() r.Get("/", ur.List) r.Post("/", ur.Create) r.Put("/", ur.Delete) r.Route("/{id}", func(r chi.Router) { r.Get("/", ur.Get) r.Put("/", ur.Update) r.Delete("/", ur.Delete) }) return r } func (ur UsersResource) List(w http.ResponseWriter, r *http.Request) { res := &Response2{ Name: "John", Todos: []string{"compile", "clean", "console"}} w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(res); err != nil { panic(err) } return } func (ur UsersResource) Create(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) if err != nil { panic(err) } var response2 Response2 error := json.Unmarshal(body, &response2) if error != nil { panic(err) } for i, v := range response2.Todos { todo := v if !reflect.DeepEqual(state, Response2{}) { todo = state.Todos[i] } if len(todo) > 9 && len(todo) <= 15 { response2.Todos[i] = todo + "(long)" } else if len(todo) > 15 { response2.Todos[i] = v } else { response2.Todos[i] = todo + "(short)" } } res := &Response2{ Name: response2.Name + "(updated)", Todos: response2.Todos} state = *res w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(res); err != nil { panic(err) } return } func (ur UsersResource) Get(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ベタ打ち")) } func (ur UsersResource) Update(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ベタ打ち")) } func (ur UsersResource) Delete(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ベタ打ち")) }サーバ起動、アクセスしてみる
サーバ起動
$ go run main.go以下のコマンド or ブラウザアクセスでレスポンスをみてみる。
$ curl -X GET localhost:8080/users -u user1:value1BASIC認証がかけられており、ユーザー名、パスワードが求められる。
認証が失敗すれば、レスポンスヘッダ
WWW-Authenticate: `Basic realm=secret-room`のように返ってくる。
$ curl -X POST localhost:8080/users -d '{"name": "test", "todos": ["compile","test","package"]}' -u user1:value1また、POSTでデータを送ってみると、送るたびにレスポンスが変化しているのを確認した。
dockerでやってみる
カレントディレクトリの構成は、前述のファイル群を含んだ以下の構成で行う。
$ tree -a . ├── .dockerignore ├── go.mod ├── Dockerfile ├── go.sum ├── main.go └── resources └── users.go$ docker build -t tester1/gochi1:1.0 . $ docker run -it --rm -d -p 8080:8080 --name tester1-gochi1 tester1/gochi1:1.0前項で起動していたGoサーバを一旦停止し、dockerコンテナ作成・起動後、前述のURLにアクセスすると非コンテナ時と同様に動作した。
後片付けは
$ docker ps -a //で該当のコンテナIDを探して $ docker stop [コンテナID] $ docker images //で該当イメージIDを探して $ docker rmi [イメージID]で完了した。
最後に
とりあえずGoのライブラリを使って即席でJSON APIを立ててみたが、比較的簡単に実装できてお手軽感があっていいな、と思った。
動きも早いのでこれからもっと触っていくための走り書きをここにして終わりにしたいと思う。
おしまい。
- 投稿日:2020-12-15T00:00:10+09:00
LeetCodeに毎日挑戦してみた 110. Balanced Binary Tree(Python、Go)
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
25問目(問題110)
110. Balanced Binary Tree
問題内容
Given a binary tree, determine if it is height-balanced.
For this problem, a height-balanced binary tree is defined as:
a binary tree in which the left and right subtrees of every node differ in height by no more than 1.
(日本語訳)
二分木が与えられた場合、それが高さのバランスが取れているかどうかを判断します。
この問題では、高さバランスのとれた二分木は次のように定義されます。
すべてのノードの左右のサブツリーの高さが1以下で異なる二分木。
Example 1:
Input: root = [3,9,20,null,null,15,7] Output: trueExample 2:
Input: root = [1,2,2,3,3,null,null,4,4] Output: falseExample 3:
Input: root = [] Output: true考え方
二分木探索として再帰関数を用います
左のノードと右のノードの深さを測り、差が1以上あった場合は−1を返していきます
差が1以下で、left,rightが-1でない場合、深い方の数字をmaxで返して
最終的な値が−1だったらfalse,違ったらtrueで返します
解答コード
class Solution(object): def isBalanced(self, root): def check(root): if root is None: return 0 left = check(root.left) right = check(root.right) if left == -1 or right == -1 or abs(left - right) > 1: return -1 return 1 + max(left, right) return check(root) != -1
- Goでも書いてみます!
func isBalanced(root *TreeNode) bool { if root == nil { return true } lh := height(root.Left) rh := height(root.Right) if absDiff(lh, rh) > 1 { return false } return isBalanced(root.Left) && isBalanced(root.Right) } func absDiff(a, b int) int { if a >= b { return a - b } return b - a } func height(root *TreeNode) int { if root == nil { return 0 } return 1 + maxHeight(height(root.Left), height(root.Right)) } func maxHeight(a, b int) int { if a >= b { return a } return b }


