- 投稿日:2020-12-01T23:34:32+09:00
don't use underscores in Go names;の簡易的な対処法(初学者向け)
はじめに
Goを最近勉強していて変数宣言で"don't use underscores in Go names"が出たのでメモ書きしておきます。
初学者の方の参考になれば幸いです。事象
bool型の変数を定義して、結果と型を出力させるという簡単なコードです。
package main import ( "fmt" "reflect" ) func main() { a := 1 b := 5 var num_bool bool = a > b //ここに "don't use underscores in Go names;"が出てる fmt.Println(num_bool) fmt.Println(reflect.TypeOf(num_bool)) }そのままでも以下のようにビルドは通りますが何か気持ちが悪いので対処します。
PS C:\pg\Go\study> go build main.go PS C:\pg\Go\study> ./main false bool対処
don't use underscores in Go names;
雑に和訳しますと"名前でアンダーラインを使わないでください"ということです。
つまり、変数名にアンダースコアを入れなければ解決ということになります。
num_bool を numBool にしてやればいいですね。package main import ( "fmt" "reflect" ) func main() { a := 1 b := 5 var numBool bool = a > b fmt.Println(numBool) fmt.Println(reflect.TypeOf(numBool)) }以上
最後に
私の場合、普段から変数名にスネークケースを使いがちだったのでキャメルケースで変数を定義するのは違和感がありますが使っていくうちに慣れるでしょうって感じ。
- 投稿日:2020-12-01T22:28:08+09:00
Railsしか使ったことない駆け出しエンジニアが、Goの環境構築してみた!
なぜGoを入れるの?
2020/12/1 現在、本気でいきたい企業がGoを開発環境としているから!面接受けるからには、少しでも勉強しとかなきゃと息込んでやってみました!
なお、私の環境は
macOS Catalina ver.10.15.7です。公式HPにGo!
git cloneとかで入れる方法もあるそうですが、せっかくだからGoの公式HPを参考にすることを決意!
1. https://golang.org/doc/installにGo!
2. Download Go for Macをクリックしてダウンロード
3. インストールされているか確認
"The package should put the /usr/local/go/bin directory in your PATH environment variable."とのことので、コンソールにて
コンソールにて//ディレクトリ移動 % cd /usr/local/go/bin //バージョンチェック bin % go version go version go1.15.5 darwin/amd64とインストールできていたようです!よかった…
試しに簡単なコードを書いてみた
上の続きのhttps://golang.org/doc/tutorial/getting-startedにも書いてあるように、試しにGoが書けるか、実際にGoファイルを作って試してみました。
1. 適当な場所に"hello.go"を作成
2. チュートリアル通りのコードを記述
hello.go//package→関数をグループしたもの的な? package main //なんかお決まりの表現的な? import "fmt" //ここはJavaScriptと変わらない感じ。Printlnはconsole.logみたいなメソッド?? func main() { fmt.Println("Hello, World!") }●Declare a main package (a package is a way to group functions).
●Import the popular fmt package, which contains functions for formatting text, including printing to the console. This package is one of the standard library packages you got when you installed Go.
●Implement a main function to print a message to the console. A main function executes by default when you run code in the file.
↓↓
この英文を読んだ感じ、これぐらいの理解しかできません(泣)
パッケージって、クラスみたいなもの??
でも響きがかっこいい笑笑3. 実際にコードがコンソール上で動くか確かめよう
hello.goのコンソール上で//rubyの時もこんな感じだったなあ… $ go run hello.go //実行結果 Hello, World!ひとまず、コンソール上で動くのを確認しました!
次は、できたらGoの簡単なアプリ作っていければと思います!
- 投稿日:2020-12-01T21:40:01+09:00
AtCoder Regular Contest 108のメモ
前置き
Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。問題
https://atcoder.jp/contests/arc108
A
Q_A.gopackage main import ( "fmt" ) func main() { var S, P int64 fmt.Scanf("%d %d", &S, &P) var ans string = "No" var N int64 for N=1; N*N<=P; N++{ M := S - N if P == N * M{ ans = "Yes" } } fmt.Printf("%s\n", ans) }B
Q_B.gopackage main import ( "fmt" "strings" ) func check(queue []string) []string{ if len(queue) >= 3{ l := len(queue) if (queue[l-3] == "f") && (queue[l-2] == "o") && (queue[l-1] == "x"){ queue = queue[:l-3] queue = check(queue) } } return queue } func main() { var N int fmt.Scanf("%d", &N) var s string fmt.Scanf("%s", &s) c := strings.Split(s, "") var queue []string for i:=0; i<N; i++{ queue = append(queue, c[i]) queue = check(queue) } fmt.Printf("%d\n", len(queue)) }C
覚えてたら後で書きます。
D
覚えてたら後で書きます。
E
覚えてたら後で書きます。
F
覚えてたら後で書きます。
- 投稿日:2020-12-01T17:38:47+09:00
完全個人用のURL管理サービスをReact×Firebase×Go×Herokuで作った話
はじめに
こんにちはRIN1208です。
この記事はタイトルに書いてある通り、完全に個人で使う用に作成したのでそれについて書いていきたいと思います。
今回作ったものはこちらこの記事はITRCアドベントカレンダーの1日目の記事になります。
今回の構成
今回は上記のような構成で作成しています。サーバーサイドはHeroku、フロントエンドはFirebase Hosingにデプロイしています。
またJWTが無効な値だった場合は6, 7の部分の処理はせずにエラーを返すようにしました。できたもの
以下のようなURLを貼り付けて管理するだけのサービスです。完全個人用なので規約等はありません。
シンプルなのもにしたかったので機能は特にありません。
使用技術
環境
- golang
- react(yarn)
- firebase cli
- heroku cli
上記の環境がある前提で説明していきます
フロントエンド
- React.js
reduxは使用しておりません- axios
- material-ui
cssが大の苦手な為使用しました- Firebase
フロントエンドは認証と Firebase Hosingを使用しましたバックエンド
- Golang (Gin)
- Heroku
- FireStore
フロントで使用しても良かったのですがサーバーサイド書きたかったのでしようしました。CI
- GitHub Actions
masterにpushされた際にフロントはFirebase Hosing、バックエンドはHerokuにデプロイれるようにしましたこの記事で説明する部分
今回作ったものを説明するにあたり、全て説明するとさすがに長くなるので以下の要所のみ説明していきます
フロントエンド
- JWTの部分
サーバーサイド
- JWTの部分
- FireStoreの部分
- GitHub Actionsについて
フロントエンド
まずはフロントエンドjwtの取得の部分です
const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); useEffect(() => { firebase.auth().onAuthStateChanged(user => { setLoading(false) setUser(user) if (user) { user.getIdToken().then(function (idToken) { localStorage.setItem('jwt', idToken) }); localStorage.setItem('uid', user.uid) } }) }) const logout = () => { firebase.auth().signOut(); localStorage.removeItem('uid') }ログイン時にuser.getIdToken().then(function (idToken) でjwtを取得し、
localStorage.setItem('jwt', idToken)でローカルストレージにjwtを保存していますバックエンド
GoでFirestoreを扱うための下準備
FireBaseのプロジェクを開き
上の画像のように プロジェクトの設定 > サービスアカウント > 新しい秘密鍵の生成 をタップし生成して下さい。このjsonファイルはGitHubに上げたりしないで下さい。CORSの設定
以下のように書きCORSの設定をします
port := os.Getenv("PORT") if port == "" { err := godotenv.Load(fmt.Sprintf("./%s.env", os.Getenv("GO_ENV"))) if err != nil { fmt.Println(err) } port = os.Getenv("LOCAL_PORT") } r := gin.Default() r.Use(cors.New(cors.Config{ AllowMethods: []string{ "POST", "GET", "OPTIONS", "PUT", "DELETE", }, AllowHeaders: []string{ "Content-Type", "Content-Length", "Authorization", "Uid", }, AllowOrigins: []string{ "http://localhost:3000", os.Getenv("FRONT_URL1"), os.Getenv("FRONT_URL2"), }, MaxAge: 24 * time.Hour, })) pkg.Serve(r, ":"+port)Firestoreの処理
FireStoreの処理の部分です。interfaceを使用してClean Architectureっぽく書いています。
type FirestoreAuth struct { Type string `json:"type"` Project_id string `json:"project_id"` Private_key_id string `json:"private_key_id"` Private_key string `json:"private_key"` Client_email string `json:"client_email"` Client_id string `json:"client_id"` Auth_uri string `json:"auth_uri"` Token_uri string `json:"token_uri"` Auth_provider_x509_cert_url string `json:"auth_provider_x509_cert_url"` Client_x509_cert_url string `json:"client_x509_cert_url"` } type FireBaseClient struct { FireBase *firebase.App FireStore *firestore.Client Ctx context.Context CollectionRef *firestore.CollectionRef DocumentRef *firestore.DocumentRef Auth *auth.Client } type FireBaseHandler interface { Collection(path string) *FireBaseClient Set(ctx context.Context, data interface{}) error Doc(id string) *FireBaseClient Documents(ctx context.Context) *firestore.DocumentIterator Delete(ctx context.Context) error VerifyIDToken(ctx context.Context, idToken string) error } type FireBase struct { FireBaseHandler } func Init_firebase() FireBaseHandler { ctx := context.Background() sa := option.WithCredentialsFile("./firestore.json") app, err := firebase.NewApp(ctx, nil, sa) if err != nil { return nil } client, err := app.Firestore(ctx) if err != nil { return nil } auth, err := app.Auth(ctx) if err != nil { return nil } return &FireBaseClient{ FireBase: app, FireStore: client, Ctx: ctx, Auth: auth, } } //データを書き込み func (fb *FireBase) InsertData(data model.Content) { updateError := fb.Collection(data.Uid).Doc(data.Content_id).Set(context.Background(), map[string]interface{}{ "content_id": data.Content_id, "comment": data.Comment, "url": data.Url, "date": data.Date, }) if updateError != nil { log.Printf("An error has occurred: %s", updateError) } } //データを削除 func (fb *FireBase) DeleteData(uid, id string) error { err := fb.Collection(uid).Doc(id).Delete(context.Background()) if err != nil { return err } return nil } //データを取得 func (fb *FireBase) GetData(uid string) []model.Content { var res_data []model.Content iter := fb.Collection(uid).Documents(context.Background()) for { doc, err := iter.Next() if err == iterator.Done { break } if err != nil { log.Fatalf("Failed to iterate: %v", err) } data := doc.Data() var content model.Content content.Comment = data["comment"].(string) content.Url = data["url"].(string) content.Content_id = data["content_id"].(string) content.Date = int(data["date"].(int64)) res_data = append(res_data, content) } return res_data } //jwt認証 func (fb *FireBase) AuthJWT(jwt string) error { idToken := strings.Replace(jwt, "Bearer ", "", 1) err := fb.VerifyIDToken(context.Background(), idToken) if err != nil { return err } return nil } func (fb *FireBaseClient) VerifyIDToken(ctx context.Context, idToken string) error { _, err := fb.Auth.VerifyIDToken(ctx, idToken) return err } func (fb *FireBaseClient) Collection(path string) *FireBaseClient { fb.CollectionRef = fb.FireStore.Collection(path) return fb } func (fb *FireBaseClient) Set(ctx context.Context, data interface{}) error { _, err := fb.DocumentRef.Set(ctx, data, firestore.MergeAll) return err } func (fb *FireBaseClient) Doc(id string) *FireBaseClient { fb.DocumentRef = fb.CollectionRef.Doc(id) return fb } func (fb *FireBaseClient) Documents(ctx context.Context) *firestore.DocumentIterator { res := fb.CollectionRef.Documents(ctx) return res } func (fb *FireBaseClient) Delete(ctx context.Context) error { _, err := fb.DocumentRef.Delete(ctx) return err }GoでFireStoreをを使用する際にFireBaseの認証のjsonを読み込ませるのですがgithubにpushするわけにも行かないので今回はjsonを作成するようにしました
func CreateFireStoreJson() { fp, err := os.Create("./firestore.json") if err != nil { fmt.Println(err) return } defer fp.Close() file := fmt.Sprintf(` { "type": "%s", "project_id": "%s", "private_key_id": "%s", "private_key": "%s", "client_email": "%s", "client_id": "%s", "auth_uri": "%s", "token_uri": "%s", "auth_provider_x509_cert_url": "%s", "client_x509_cert_url": "%s" }`, os.Getenv("FS_TYPE"), os.Getenv("FS_PROJECT_ID"), os.Getenv("FS_PRIVATE_KEY_ID"), os.Getenv("FS_PRIVATE_KEY"), os.Getenv("FS_CLIENT_EMAIL"), os.Getenv("FS_CLIENT_ID"), os.Getenv("FS_AUTH_URI"), os.Getenv("FS_TOKEN_URI"), os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL"), os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL")) _, err = fp.Write(([]byte)(file)) if err != nil { fmt.Println(err) } }GitHub Actionsを使ってFirebaseとHerokuにデプロイする
ymlを作成する
.github/workflows/deploy.ymlをプロジェクトのルートディレクトリに作成して下さい。
これはGithub Actionsの設定ファイルです。今回はdeploy.ymlにしていますが.yml形式のファイルであれば問題ないです。name: ci on: push: braches: - master jobs: firebase: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup node uses: actions/setup-node@v1 with: node-version: 14.4.0 - name: Install dependencies run: | yarn - name: Build React app env: #FireBaseの環境変数を定義しています REACT_APP_FB_API_KEY: ${{ secrets.REACT_APP_FB_API_KEY }} REACT_APP_FB_AUTH_DOMAIN: ${{ secrets.REACT_APP_FB_AUTH_DOMAIN }} REACT_APP_FB_DATABASE_URL: ${{ secrets.REACT_APP_FB_DATABASE_URL }} REACT_APP_FB_PROJECT_ID: ${{ secrets.REACT_APP_FB_PROJECT_ID }} REACT_APP_FB_STORAGE_BUCKET: ${{ secrets.REACT_APP_FB_STORAGE_BUCKET }} REACT_APP_FB_MESSAGEING_SENDER_ID: ${{ secrets.REACT_APP_FB_MESSAGEING_SENDER_ID }} REACT_APP_SERVER_URL: ${{ secrets.REACT_APP_SERVER_URL }} run: | yarn install && yarn build - name: Setup Firebase CLI run: | npm install -g firebase-tools - name: Deploy Firebase env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} #firebaseのcitoken run: | firebase deploy --token $FIREBASE_TOKEN backend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Deploy to Heroku env: HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }} # herokuのcitoken HEROKU_APP_NAME: herokuのプロジェクト名 if: github.ref == 'refs/heads/master' && job.status == 'success' run: | git push https://heroku:$HEROKU_API_TOKEN@git.heroku.com/$HEROKU_APP_NAME.git origin/master:masterCIトークンを取得する
FireBaseのトークンを取得する
firebase login:ci上記のコマンドを打つと以下のようにトークンが表示されます
✔ Success! Use this token to login on a CI server: {TOKENの文字列}Herokuのトークンを取得する
heroku auth:token上記のコマンドを打つと以下のようにトークンが表示されます
› Use heroku authorizations:create to generate a long-term token {TOKENの文字列}GitHub ActionsでHerokuにデプロイする際にProfile等は必要ないみたいです
環境変数を定義する
リポジトリを開き Setting > Secrets > New repository secret で環境変数を設定します
設定する際は${{ secrets.FIREBASE_TOKEN }}のような書き方で取得できます。上記のが完了したらmasterにpushするとFireBaseとHerokuにデプロイされるようになります。
おわりに
ここまで読んでくださりありがとうございます。
今回は個人で使用するURLを管理するサービスについて書きました。
初めてGitHub Actionsを使用しましたがめっちゃ便利でした。ただ個人用ですのでエラーハンドリングやログもかなり適当になってます......また間違っている点などがございましたらコメントなどで指摘していただけると助かります。
- 投稿日:2020-12-01T16:24:04+09:00
c++のstd::set::lower_boundとupper_boundをGoで作ってみました
はじめに
零細企業の事務をやっているおじさんです。Go歴は半年です。
螺旋本のpart3 第16章の16.13線分交差問題AOJ CGL6_JをGoで解こうとしたところ、そこに載っているサンプルコードでc++のlower_boundとupper_boundが、Goにはないようだったので、自作しました。
注意
・二分探索木で作っています
・イテレータではなく、Nodeを返します
・sliceには使えません
・初心者なので、ここで紹介しているものが不完全である可能性は十分あります
・四角い車輪の再発明の可能性大Go版lower_boundのコード
func lowerBoundNode(n *Node, x interface{}) *Node { if n.key < x.(int) { if n.right != nil { n = lowerBoundNode(n.right, x) } } else if n.key >= x.(int) { if n.left != nil && n.left.key >= x.(int) { n = lowerBoundNode(n.left, x) } } return n } func (t *Tree) lowerBound(x interface{}) *Node { return lowerBoundNode(t.root, x) }t.lowerBound(x)で、x <= n.key となる最初の*Nodeを返します。
n.key < x なら右に行ってn.key >= xとなるノードを探し、n.key >= x なら左に行って最初のノードを探しています。Go版upper_boundのコード
func upperBoundNode(n *Node, x interface{}) *Node { if n.key <= x.(int) { if n.right != nil { n = upperBoundNode(n.right, x) } } else if n.key > x.(int) { if n.left != nil && n.left.key > x.(int) { n = upperBoundNode(n.left, x) } } return n } func (t *Tree)upperBound(x interface{})*Node{ return upperBoundNode(t.root,x) }t.upperBound(x)で、x < n.key となる最初の*Nodeを返します。
lowerBoundと大体同じです。コード全部
AOJ CGL6_Jへの提出用ではなく、AOJ ALDS1_8_Cへ提出したものを改変しています。
AOJ ALDS1_8_Cにある入力例を使用し実行→paiza.ioもしかしたらsetとして使えるかもしれませんが、多分遅いです。
package main import ( "bufio" "fmt" "os" "strconv" "strings" ) var rdr = bufio.NewReaderSize(os.Stdin, 1024*1024) func readLine() string { buf := []byte{} for { l, p, e := rdr.ReadLine() if e != nil { panic(e) } buf = append(buf, l...) if !p { break } } return string(buf) } func readInts() []int { s := strings.Split(readLine(), " ") res := []int{} for _, v := range s { i, _ := strconv.Atoi(v) res = append(res, i) } return res } type Node struct { key int parent *Node left *Node right *Node } func NewNode() *Node { res := Node{} return &res } type Tree struct { root *Node } func NewTree() *Tree { res := Tree{ root: nil, } return &res } // 先行順巡回アルゴリズムでNodeのkeyを返す func (t *Tree) preParse(z *Node) string { if z == nil { return "" } return " " + strconv.Itoa(z.key) + t.preParse(z.left) + t.preParse(z.right) } // 中間順巡回アルゴリズムでNodeのkeyを返す func (t *Tree) inParse(z *Node) string { //fmt.Println(z) if z == nil { return "" } return t.inParse(z.left) + " " + strconv.Itoa(z.key) + t.inParse(z.right) } func find(sl []int, x int) int { for i := 0; i < len(sl); i++ { if sl[i] == x { return i } } return -1 } /* 1 insert(T, z) 2 y = NIL // x の親 3 x = 'T の根' 4 while x ≠ NIL 5 y = x // 親を設定 6 if z.key < x.key 7 x = x.left // 左の子へ移動 8 else 9 x = x.right // 右の子へ移動 10 z.p = y 11 12 if y == NIL // T が空の場合 13 'T の根' = z 14 else if z.key < y.key 15 y.left = z // z を y の左の子にする 16 else 17 y.right = z // z を y の右の子にする */ func (t *Tree) insert(k int) { //fmt.Println("k",k) x := t.root var y *Node //var flag bool=false for x != nil { y = x if k == x.key { //既にxをkeyに持つNodeがあった場合 return } else if k < x.key { x = x.left } else { x = x.right } } var new_node *Node new_node = &Node{left: nil, right: nil, key: k} new_node.parent = y if y == nil { t.root = new_node } else if new_node.key < y.key { y.left = new_node } else { y.right = new_node } //fmt.Println(new_node) } func (t *Tree) find(k int) *Node { x := t.root for x != nil && k != x.key { if k < x.key { x = x.left } else { x = x.right } } return x } func (t *Tree) deleteNode(z *Node) { //yを削除対象とする var y *Node if z.left == nil || z.right == nil { y = z } else { y = getSuccessor(z) } //yの子xを決める var x *Node if y.left != nil { x = y.left } else { x = y.right } //yの親を設定する if x != nil { x.parent = y.parent } // 削除する if y.parent == nil { t.root = x } else if y == y.parent.left { y.parent.left = x } else { y.parent.right = x } if y != z { z.key = y.key } } func getSuccessor(x *Node) *Node { if x.right != nil { return getMinimum(x.right) } var y *Node y = x.parent for y != nil && x == y.right { x = y y = y.parent } return y } func getMinimum(x *Node) *Node { for x.left != nil { x = x.left } return x } func lowerBoundNode(n *Node, x interface{}) *Node { if n.key < x.(int) { if n.right != nil { n = lowerBoundNode(n.right, x) } } else if n.key >= x.(int) { if n.left != nil && n.left.key >= x.(int) { n = lowerBoundNode(n.left, x) } } return n } func (t *Tree) lowerBound(x interface{}) *Node { return lowerBoundNode(t.root, x) } func upperBoundNode(n *Node, x interface{}) *Node { //fmt.Println("lowerBoundNode n:",&n,n,"x",x) if n.key <= x.(int) { if n.right != nil { n = upperBoundNode(n.right, x) } } else if n.key > x.(int) { if n.left != nil && n.left.key > x.(int) { n = upperBoundNode(n.left, x) } } return n } func (t *Tree) upperBound(x interface{}) *Node { return upperBoundNode(t.root, x) } func main() { n := readInts()[0] t := NewTree() //fmt.Println(n,*t) for i := 0; i < n; i++ { tmp := strings.Split(readLine(), " ") if tmp[0] == "insert" { v, _ := strconv.Atoi(tmp[1]) t.insert(v) } else if tmp[0] == "print" { fmt.Println(t.inParse(t.root)) fmt.Println(t.preParse(t.root)) fmt.Println("t.root", t.root) fmt.Println("t.lowerBound(t.root,x)", t.lowerBound(1)) fmt.Println("t.upperBound(t.root,x)", t.upperBound(1)) } else if tmp[0] == "find" { v, _ := strconv.Atoi(tmp[1]) res := t.find(v) if res != nil { fmt.Println("yes") } else { fmt.Println("no") } } else if tmp[0] == "delete" { v, _ := strconv.Atoi(tmp[1]) //fmt.Println(t.find(v)) t.deleteNode(t.find(v)) } } }参考
お気楽 Go 言語プログラミング入門(二分探索木)
プログラミングコンテスト攻略のためのアルゴリズムとデータ構造(Amazon)
https://cpprefjp.github.io/reference/set/set/lower_bound.html
https://cpprefjp.github.io/reference/set/set/upper_bound.html
- 投稿日:2020-12-01T16:24:04+09:00
daprでつくるマイクロサービス
はじめに
この記事は、 富士通クラウドテクノロジーズ Advent Calendar 2020 の2日目の記事です。
1日目は @miyuush さんの ニフクラがTerraformに対応したので使ってみた【基礎編】 でした!
昨日リリースされたばかりで生まれたてホヤホヤ感のある nifcloud/terraform-provider-nifcloud v1.0.0 ですがこれからの機能エンハンスが楽しみですね!どんどんIaCにしていきたい改めましてこんにちは!NIFCLOUDのいくつかのサービスのAPIを開発している @kzmake と申します。
入社しサービスを開発してはや4年目になりました。ここ数年はいくつかのサービス開発を経験し、そこそこcleanでdddなアプリケーションをかけるようになってきたました
最近はどうすればスピード感ある開発ができるかな〜と考えている今日このごろです。今日は自分が使ってみたいなぁ〜と感じている
- github.com/dapr/dapr について
- daprを使ったマイクロサービスアプリケーション実装
を紹介したいと思います!
dapr とは
dapr は、 Distributed Application Runtime という名のとおりマイクロサービスアプリケーションとして必要な機能をビルディングブロックとして提供してくれるランタイムです。stable はまだ v0.11.3 と比較的若いながら、★8.4k とかなりホット な OSSプロジェクトではないかなとおもっています。そのコンセプトは、Any language, any framework, anywhere としており多様性をもった利用ができるところもポイントですね。
本来実装したいコアロジックにサイドカーとして利用することで、簡単にマイクロサービスを作成することができます。
更にそれぞれのビルディングブロックは抽象化されており、 HTTP/gRPC API を通して利用するものとなっているため言語に縛られない開発ができるのも魅力となっています。マイクロサービスのためのビルディングブロック
dapr が現在(2020/11/29)提供しているビルディングブロックには下記のものがあります。
- Service-to-service invocation:
/v1.0/invoke
- 他のマイクロサービスサービスへ通信するための機能
- State management:
/v1.0/state
- key/valueベースの永続化や参照機能
- Publish and subscribe:
/v1.0/publish
and/v1.0/subscribe
- Publish/subscribeモデルで非同期にメッセージを送受信する機能
- Resource bindings:
/v1.0/bindings
- 外部コンポーネントやサービスを抽象化しイベントの送受信を行う機能
- Actors:
/v1.0/actors
- 分散性や並行・並列性をもち、非同期なメッセージ駆動のアクターモデルを提供
- Observability
- ログ・トレース・メトリクス・ヘルスチェックといったオブザーバビリティに必要な要素を提供
- Secrets:
/v1.0/secrets
- 安全にパスワードなどのクレデンシャルなデータにアクセスする機能
それぞれのコンポーネントはライブラリとしてアプリケーションに組み込むのではなく、yamlのコンポーネント定義ファイルをロードさせることで利用することができます。
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: messagebus spec: type: pubsub.redis metadata: - name: redisHost value: redis:6379 - name: redisPassword value: ""ビルディングブロックとしての提供なので、実装に一切手を加えず、検証環境ではredis、本番環境では何かしらのクラウドサービスなど切り替えもできるのがいいですね
ミドルウェア(http)
ミドルウェアも各種ビルディングブロックと同様にコンポーネントを定義することで利用可能となっています。
apiVersion: dapr.io/v1alpha1 kind: Configuration metadata: name: pipeline namespace: default spec: httpPipeline: handlers: - name: oauth2 type: middleware.http.oauth2 - name: uppercase type: middleware.http.uppercase特徴的なのが、
ratelimit
やoauth2
など定義済みのミドルウェアだけでなく、 Open Policy Agent(OPA) を用いてミドルウェアを追加できるようです( ex: https://play.openpolicyagent.org/p/oRIDSo6OwE )。提供されているミドルウェアは https://github.com/dapr/components-contrib/blob/master/middleware/http で確認できます。サポートしているSDK
daprが提供する HTTP/gRPC API にアクセスするためのsdkを利用することもできます。提供されているSDKは、
となってます。今後のロードマップはこんな感じらしい。
ダッシュボード
まだ機能は少ないですが、ダッシュボードも利用できるようです。今後の機能追加も期待ですね!
IDEのサポート
言語サポートだけでなく、IDEのサポートもあるみたいです
んでなにを作ったの?
time.Now()
など現在時刻を直接取得せず、マイクロサービスでシンプルな
時計サービス : github.com/kzmake/dapr-clock
をつくってみました(同期にこのアイデアをいただきました)!$ docker-compose up -d --build $ docker-compose exec dev curl -s http://web/now | jq . { "hour": 15, "minute": 33, "second": 25 }上記のコードは、時計をざっくりとモデリングし…
- clock: 現在時刻を取得するサービス
/v1.0/clock/invoke/now
で時刻を取得するAPIを提供
hour-hand/minute-hand/second-hand
から針の情報を取得する- hour-hand: 時針を管理するサービス
時刻同期のイベント
に時針を設定する60分間経過のイベント
を元に分針を運針する
- 時針を永続化する
23
→0
になると24時間経過のイベント
を発行する- minute-hand: 分針を管理するサービス
時刻同期のイベント
に分針を設定する60秒間経過のイベント
を元に分針を運針する
- 分針を永続化する
59
→0
になると60分間経過のイベント
を発行する- second-hand: 秒針を管理するサービス
時刻同期のイベント
に秒針を設定する1秒間経過のイベント
を元に秒針を運針する
- 秒針を永続化する
59
→0
になると60秒間経過のイベント
を発行する- ticker: 1秒をカウントするサービス
一定間隔(1sec)毎
に1秒間経過のイベント
を発行する- synchronizer: NTPを用いて時刻を同期するサービス
一定間隔(24h)毎
に時刻同期のイベント
を発行するをマイクロサービスとして設計しています。
アーキテクチャ
daprを利用して以下のような設計してみました。
今回は、
- Service-to-service invocation
clock
サービスからsecond-hand/minute-hand/hour-hand
の呼び出し- Publish and subscribe
- cronを使って
ticker
サービス /synchronizer
サービス を一定間隔毎にトリガー- Resource bindings
- redis経由で各イベントを通知
- State management
- 時針・分針・秒針の情報を永続化
を利用してみようとおもいます。
github.com/kzmake/dapr-clock
github.com/dapr/go-sdk を利用してdaprサイドカーと通信するマイクロサービスアプリケーションを github.com/kzmake/dapr-clock の
/microservices
に各マイクロサービスを作成しました。各機能についてdaprでどう実装できるかを紹介していきたいと思います。現在時刻を取得する機能
ユーザーは
clock
サービスを通してsecond-hand/minute-hand/hour-hand
サービスが管理する時針・分針・秒針を取得するとします。
また、second-hand/minute-hand/hour-hand
はそれぞれの針の状態を時針で持ちたくないのでdaprが提供するストア機能で永続化も試みます。ここでdaprのビルディングブロックとしては、を利用し、dapr上で
POST /v1.0/invoke/clock/method/now
のAPIを提供します。まず、APIリクエストを受け取る部分から記述していきます。今回はアプリケーション <--> dapr
間でgrpcを利用したかったのでgithub.com/dapr/go-sdk/service/grpc
を使っています。Service-to-service invocation
下記は
clock
サービスの実装ですが、second-hand/minute-hand/hour-hand
も呼び出す handler が違うだけで差分はありません。second-hand/minute-hand/hour-hand
も同じようにAddServiceInvocationHandler
を使って"now"
のAPIを追加しています。import ( daprd "github.com/dapr/go-sdk/service/grpc" "github.com/kzmake/dapr-clock/microservices/clock/handler" ) func main() { s, err := daprd.NewService(":3000") if err != nil { log.Fatalf("failed to start the server: %+v", err) } // POST /v1.0/invoke/clock/method/now if err := s.AddServiceInvocationHandler("now", handler.Now); err != nil { log.Fatalf("error adding invocation handler: %+v", err) } if err := s.Start(); err != nil { log.Fatalf("server error: %+v", err) } }
clock
サービスのhandlerは、second-hand/minute-hand/hour-hand
のAPIをリクエストするため、daprのビルディングブロックである Service-to-service invocation を利用します。client.InvokeService
を利用している部分がそれにあたりますが、daprが提供する固定のエンドポイントを利用することでサービスディスカバリーを実装する必要がないようになっています!func Now(ctx context.Context, in *common.InvocationEvent) (*common.Content, error) { client, err := dapr.NewClient() if err != nil { return nil, err } // POST /v1.0/invoke/hour-hand/method/now hourHandRes, err := client.InvokeService(ctx, "hour-hand", "now") if err != nil { return nil, err } // POST /v1.0/invoke/minute-hand/method/now minuteHandRes, err := client.InvokeService(ctx, "minute-hand", "now") if err != nil { return nil, err } // POST /v1.0/invoke/second-hand/method/now secondHandRes, err := client.InvokeService(ctx, "second-hand", "now") if err != nil { return nil, err } // ...それぞれのレスポンスの json.Unmarshal 処理してレスポンスを作成するなど return &common.Content{ContentType: "application/json", Data: res}, nil }State management
上記のリクエストを
second-hand/minute-hand/hour-hand
サービスで受け付け、 永続化されている針を取得していきます。State management では、コンポーネントとしてyamlで定義したstatestore
を利用していきます。バックエンドとして多くのものをサポートしていますが、今回はredisを使用しています。apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.redis metadata: - name: redisHost value: localhost:6379 - name: redisPassword value: ""State management では、
metadata
で設定したname
やnamespace
を元にコード内で永続・参照先を指定します。今回の例ではstatestore
を指定します。参照のみなので、client.GetState(ctx, "statestore", "hour")
のようにデータを取得します。
hour-hand
サービスのPOST /v1.0/invoke/hour-hand/method/now
を処理するハンドラーとしては下記のようになります。import ( dapr "github.com/dapr/go-sdk/client" "github.com/dapr/go-sdk/service/common" ) func Now(ctx context.Context, in *common.InvocationEvent) (*common.Content, error) { client, err := dapr.NewClient() if err != nil { return nil, err } item, err := client.GetState(ctx, "statestore", "hour") if err != nil { return nil, err } // ...レスポンスを生成 return &common.Content{ContentType: "application/json", Data: res}, nil }一定間隔毎に針を運針する機能
Resource bindingsでサポートしている
cron
を利用して一定間隔でスケジューリングできるトリガーをもとに、ticker
サービスでイベントを発行し、second-hand/minute-hand/hour-hand
サービスの運針(データを永続化)を実現します。tickerからブロードキャストするのではなく、
ticker --[Ticked]--> second-hand --[60sTicked]--> minutes-hand --[60mTicked]--> hour-hand
とtickerのイベントをトリガーに各サービスもイベントを発行するようにしています。daprのビルディングブロックとしては、を使いました。
Resource bindings
まずは
ticker
サービスのトリガーとなるinput bindings
から見ていきます。apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: ticker spec: type: bindings.cron metadata: - name: schedule value: "@every 1s"と1sec毎に
ticker
サービスの処理を行うためのトリガーを設定します。Service-to-service invocationと同様に、ここではticker
コンポーネントとhandler.Tick
ハンドラーを結びつけるため、s.AddBindingInvocationHandler("ticker", handler.Tick)
を行います。import ( daprd "github.com/dapr/go-sdk/service/grpc" "github.com/kzmake/dapr-clock/microservices/ticker/handler" ) func main() { s, err := daprd.NewService(":3001") if err != nil { log.Fatalf("failed to start the server: %+v", err) } if err := s.AddBindingInvocationHandler("ticker", handler.Tick); err != nil { log.Fatalf("error adding binding handler: %+v", err) } if err := s.Start(); err != nil { log.Fatalf("server error: %+v", err) } }このようにするだけで、外部コンポーネントをトリガーとして処理を行うサービスを作成できます。
Publish and subscribe
次はマイクロサービスの肝となるPublish/Subcribeをdaprを使って実装していきます。
pubsub
コンポーネントの定義し、apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: pubsub spec: type: pubsub.redis metadata: - name: redisHost value: localhost:6379 - name: redisPassword value: ""コードではコンポーネントのname/namespaceを指定して
client.PublishEvent(ctx, "pubsub", "Ticked", nil)
を使うことでメッセージのPublishを実装します。func Tick(ctx context.Context, in *common.BindingEvent) ([]byte, error) { client, err := dapr.NewClient() if err != nil { return nil, err } if err := client.PublishEvent(ctx, "pubsub", "Ticked", nil); err != nil { return nil, err } return nil, nil }
second-hand
サービスのSubscribeは、AddTopicEventHandler
を使ってSubscribeハンドラーを登録します。import ( "github.com/dapr/go-sdk/service/common" daprd "github.com/dapr/go-sdk/service/grpc" "github.com/kzmake/dapr-clock/microservices/second-hand/handler" ) func main() { s, err := daprd.NewService(":3000") if err != nil { log.Fatalf("failed to start the server: %+v", err) } // ...invokeなど if err := s.AddTopicEventHandler(&common.Subscription{ PubsubName: "pubsub", Topic: "Ticked", Route: "/increase", }, handler.Increase); err != nil { log.Fatalf("error adding event handler: %+v", err) } if err := s.Start(); err != nil { log.Fatalf("server error: %+v", err) } }あとは、イベントを受信した際にdaprストア機能で秒針の情報を取得し、1sec分運針した後、永続化します。
func Increase(ctx context.Context, e *common.TopicEvent) (bool, error) { client, err := dapr.NewClient() if err != nil { return false, err } item, err := client.GetState(ctx, "statestore", "second") // ...itemからsecを取得 // 59 -> 0 への運針であれば新規にイベント発行 if (sec+1)/60 == 1 { if err := client.PublishEvent(ctx, "pubsub", "Ticked.60s", nil); err != nil { return 0, err } } // ...1sec運針する処理 if err := client.SaveState(ctx, "statestore", "second", []byte(fmt.Sprintf("%d", sec))); err != nil { return false, err } return false, nil }
59 -> 0
へと秒針が一周する場合は"Ticked.60s"
をPublishしてminute-hand/hour-hand
にイベントを渡していくようにしてみました。今回はこのようにサービス毎のイベントをPub/Subすることで時針・分針・秒針の運針をdaprを使って実装してみました。現在時刻を同期する機能
先程と基本的には同じビルディングブロックを利用し、
cron
を利用して一定間隔でスケジューリングできるトリガーをもとに、synchronizer
サービスでNTPより取得した現在時刻の同期イベントを発行し、second-hand/minute-hand/hour-hand
サービスの時刻同期を実現します。今回はsynchronizer
からブロードキャストし、ペイロードにjsonを渡すことでsecond-hand/minute-hand/hour-hand
の時刻を設定します。ここも dapr のビルディングブロックとしては、を使いました。新規に下記の
synchronizer
コンポーネントを定義し、1日毎
に現在時刻の同期を試みます。apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: synchronizer spec: type: bindings.cron metadata: - name: schedule value: "@daily"Publish and subscribe
Resource bindings を利用してトリガーとしてコンポーネントを登録しつつ、 Publish and subscribe でペイロードを持つイベントを発行してみます。Publish部分では NTP より取得した時刻情報をペイロードに設定してPublishさせます。
func Synchronize(ctx context.Context, in *common.BindingEvent) ([]byte, error) { client, err := dapr.NewClient() if err != nil { return nil, err } time, err := ntp.Time("ntp server address...") if err != nil { return nil, err } payload, err := json.Marshal(map[string]interface{}{"hour": time.Hour(), "minute": time.Minute(), "second": time.Second()}) if err != nil { return nil, err } if err := client.PublishEvent(ctx, "pubsub", "Synchronized", payload); err != nil { return nil, err } return nil, nil }後は
second-hand/minute-hand/hour-hand
でイベントをSubscribeし、時刻同期を行うといった実装になります。以上で時計サービスとして、
- 現在時刻を取得する機能
- 一定間隔で秒針・分針・時針を運針する機能
- 現在時刻を同期する機能
と最低限の機能を dapr + マイクロサービス として実現できたかなと思います!
さいごに
どうでしたでしょうか!まだまだ成長途中なOSSかと思いますが、コンポーネントとしてインフラの差し替え可能だったり、ビルディングブロックが豊富だったりと dapr に乗っかることでコアロジックに集中できそうだなと改めて思いました
github.com/kzmake/dapr-clock は6マイクロサービスを組み合わせてノリで作ってみましたが、設計&開発には1日もかかりませんでした これなら開発効率ももりもり上げていけそうですね (v1.0 が待ち遠しい…)
ニフクラのサービスとして提供している hatoba 上で dapr を動かしたり、マルチクラウドに利用しても面白いかもしれないですね!
今回紹介しきれなかったものに、
があるんですが、また別の機会で紹介しようと思います。(github.com/dapr/go-sdkはまだactorを利用できないようでした…残念… #21)
さて、明日は @yaaamaaaguuu さんが VMware製品を気軽に検証するためのtips について書いてくれるようです!お楽しみに
参考文献
この記事は以下の情報を参考にしています。
- 投稿日:2020-12-01T15:11:53+09:00
LeetCodeに毎日挑戦してみた 58. Search Insert Position(Python、Go)
はじめに
無料英単語サイトE-tanを運営中の@ishishowです。
プログラマとしての能力を上げるために毎日leetcodeに取り組み、自分なりの解き方を挙げていきたいと思います。
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
13問目(問題58)
58. Length of Last Word
問題内容
Given a string
s
consists of some words separated by spaces, return the length of the last word in the string. If the last word does not exist, return0
.A word is a maximal substring consisting of non-space characters only.
(日本語訳)
文字列
s
がスペースで区切られたいくつかの単語で構成されている場合、文字列の最後の単語の長さを返します。最後の単語が存在しない場合は、を返し0
ます。言葉は、唯一の非空白文字から成る最大の部分文字列です。
Example 1:
Input: s = "Hello World" Output: 5Example 2:
Input: s = " " Output: 0考え方
strip(接頭語、設備後の空白の削除)とsplit(空白で文字列を配列に分割)を使う
最後の要素の文字数を戻り値とする
- 解答コード
class Solution(object): def lengthOfLastWord(self, s): strs = s.strip().split(" ") return len(strs[-1])
- Goでも書いてみます!
import ( "strings" ) func lengthOfLastWord(s string) int { strs := strings.Split(strings.TrimSpace(s), " ") return len(strs[len(strs)-1]) }
- 投稿日:2020-12-01T10:23:03+09:00
2020年,Go言語でお世話になったライブラリ/検討したけど導入しなかったライブラリ
この記事はWanoグループ Advent Calendar 20201日目の記事になります。一年早いですね。コロナやリモートワークもあってテンション的に埋まるかどうなのかー。
いきなりネタもないので転用できそうなフォーマットで雑多に記事を書きます。本記事では年末っぽく,2020年に自分が使ったGo言語ライブラリ関連のよかったものを一括で振り返えろうかと思います。
- お世話になったライブラリ
- あんまり使わなくなった/検討したけど落ちたライブラリ
この2つの観点で、あまりまとめずつらつら書きました。
単なるライブラリの話題でも、選定がわかれば問題意識や施策が振り返えれるって気もするので、いい機会かなーと思います。1.2020年、お世話になったライブラリ
Go Generate系
愚直な筋肉の使いどころは考えようってのが2020の施策として多かったので、めんどくさいものをなんとかしてくれるgo generate周りはお世話になりました。
github.com/smallnest/gen
type Admin struct { ID int `gorm:"primary_key;AUTO_INCREMENT;column:id;type:uint;" json:"id"` CreateTime time.Time `gorm:"column:create_time;type:datetime;default:CURRENT_TIMESTAMP;" json:"create_time"` UpdateTime time.Time `gorm:"column:update_time;type:timestamp;default:CURRENT_TIMESTAMP;" json:"update_time"` DisplayName null.String `gorm:"column:display_name;type:varchar;size:255;" json:"display_name"` LoginName string `gorm:"column:login_name;type:varchar;size:255;" json:"login_name"` Password string `gorm:"column:password;type:varchar;size:255;" json:"password"` AdminRoleID int `gorm:"column:admin_role_id;type:utinyint;" json:"admin_role_id"` }ORMとしてGORMを使うことが多いのですが、MySQLの構造体の生成には最近これを使っています。
このgeneratorは生成テンプレートが自由にカスタムできるのが特徴です。余談ですが、最近は大事なプロジェクトは特にDBから引っ張ってきたデータをいきなりドメイン上のmodelとして使わず、(O/Rマッピングせず)めんどくても詰め替えと関連レコードの手動取得を行うという試みをやっています。
このへんは筋肉がいるものの、その愚直な詰め替え作業部分があんまり辛くならないように、このジェネレータでLINQ式っぽいのや各種ヘルパー関数を追加するなど、いろいろと試行錯誤しています。github.com/golang/mock
モック生成ツールです。
例えばさくっとaws-sdk-goのs3のmockみたいなのを作るとき、
type s3SdkMock struct { s3iface.S3API uploadedFiles map[string]bool } func (self *s3SdkMock) PutObject(put *s3.PutObjectInput) (*s3.PutObjectOutput, error) { self.uploadedFiles[*put.Key] = true return &s3.PutObjectOutput{}, nil }とかでもカンタンですが、あまりにあちこちのテストで使うようだと、interfaceごとまるっと差し替え可能なモックツールにしちゃったほうがいいです。
テスト書く障壁はがんがん下げていきたい。github.com/cheekybits/genny
generate支援ツールです。go templateではない、コンパイルエラーにならないgenerateネタがgoで書けます。
このツールであらゆる構造体にLINQ式とかを気軽に生やせる環境を整えています。
愚直に文字列置換するだけなので、ASTが必要なタイプのジェネレートには向きませんが、IDEのコード支援を受けられるのはかなり強く、充分に重宝しています。import ( "errors" "github.com/cheekybits/genny/generic" ) // Start ################# KeyTypeSlice ################################ type KeyType generic.Type type KeyTypeSlice []KeyType func (rcv KeyTypeSlice) Take(limit int) (result KeyTypeSlice) { if len(rcv) < limit { return rcv } return rcv[0:limit] } func (rcv KeyTypeSlice) Length() int { return len(rcv) } //http://golangcookbook.com/chapters/arrays/reverse/ func (rcv KeyTypeSlice) Reverse() KeyTypeSlice { for i := len(rcv)/2 - 1; i >= 0; i-- { opp := len(rcv) - 1 - i rcv[i], rcv[opp] = rcv[opp], rcv[i] } return rcv } func (rcv KeyTypeSlice) Append(list ...KeyType) (result KeyTypeSlice) { var old []KeyType = rcv newList := append(old, list...) return newList } func (rcv KeyTypeSlice) Prepend(list ...KeyType) (result KeyTypeSlice) { var old []KeyType = rcv newList := append(list, old...) return newList } func (rcv KeyTypeSlice) Tos() (result []KeyType) { return ([]KeyType)(rcv) } ...KeyTypeのところに「VideoGenre」って文字列当てて生成すると,
// Start ################# VideoGenreSlice ################################ type VideoGenreSlice []VideoGenre // ついでに作った func (rcv VideoGenreSlice) Take(limit int) (result VideoGenreSlice) { if len(rcv) < limit { return rcv } return rcv[0:limit] } func (rcv VideoGenreSlice) Length() int { return len(rcv) } //http://golangcookbook.com/chapters/arrays/reverse/ func (rcv VideoGenreSlice) Reverse() VideoGenreSlice { for i := len(rcv)/2 - 1; i >= 0; i-- { opp := len(rcv) - 1 - i rcv[i], rcv[opp] = rcv[opp], rcv[i] ...
type VideoGenreSlice []VideoGenre
な構造体の出来上がり。Log/Error系
github.com/golang/xerrors
階層化 Error パッケージです。
一部プロダクトでは優先的に使っており、お世話になりました。err = xerrors.Errorf("message: %w", err)で作ったエラーは
fmt.Printf("%+v", err)でスタックトレース表示できるようになります。
導入してないプロダクトでは、スタックトレースというかエラー行数が欲しいためにあちこちにlog.Error()を挟む... というようなことをやっていましたが、あまりGoっぽくないのもあり、随時これで置換えていきたいと思っています。
スタックトレース機能、なんでGoの仕様から漏れたんだっけ...。画像系
github.com/disintegration/imaging
画像変換をかなり便利にやってくれるライブラリです。依存がないのがいいし、拡大縮小/回転/フィルタ処理/クロッピングまでついています。
このライブラリのおかげで、imagemagickを使わなきゃいけない場面がかなり減りました。(まだプロジェクトのあちこちには残っているものの)
今は主にAWS Lambdaなどで動かしています。 リアルタイム変換要件もカンタン。Http
github.com/hashicorp/go-retryablehttp
ただ単にリトライ可能なHTTPクライアントってだけなんですが、あんまり書く機会が多いようだと、このへんで標準化したくなります。
いくつかのプロジェクトで使ったので地味にお世話にはなったんだと思います。Config
github.com/joho/godotenv
.envを読みこむツールです。
func main() { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file") }メインのプロダクトでは去年(2019)末くらいから12 Factor App化などアプリのモダン化にも注力しており、クラウドのKVSに秘匿情報を一元管理したりする施策を行ってきました。
つまり、アプリ起動後に秘匿情報ファイルを自らとりに行って設定を確定させたり、秘匿情報も書かれたステージごとの設定ファイルを渡すのではなく、なるべく個別の環境変数によって依存を注入する方向性です。ただまあこれってAWS ECSとかコンテナではParameter Storeとかと連係して綺麗に渡せるんですけど、EC2上の既存アプリへの当て方で悩みました。
起動直前のシェル芸が複雑になったり、リスタートやGraceful Shutdownなど、デプロイ上のプロセス管理ツールのライフサイクルとうまく噛み合わず、四苦八苦することになりました。
最終的に「Go側でも他の言語のように.envくらいは暗黙的に読み込んでいい」 くらいの温度感に落ち着き、このライブラリの選定に至りました。なければ何もしないのでシンプルですね。2. 2020年、あんまり使わなくなった/検討したけど使用に至らなかったライブラリ
github.com/spf13/viper
設定ファイル読み込みのための人気のライブラリです。12 Factor App化の文脈の一環で調査しました。
この記事が詳しいです。環境変数とyamlの組み合わせなど、設定ファイル構築のためにいろんな手段が取れます。
ただ、あまりにもAPIが魔術的な気がして、何度か試したものの採用しませんでした。使いこなせば強力そうですが...。
現行は、せいぜい設定ファイル一枚と秘匿情報系環境変数の組合せ程度で済む感じなので、前述の通りgodotenvと設定ファイル(普通にGoのtemplate)の組み合わせでConfig系を構築しています。github.com/uber-go/zap
ロガーです。
IOをブロックせず速い!らしいので広告案件で使ってました。メソッドが他のロガー系実装と割と解離があったので、interfaceだけechoについているgithub.com/labstack/gommonのものに置き換えて、そのアダプタとしてzapを使う...という形をとっていました。
使用感自体はあまり好きではなかったのと、出力をカスタマイズしようとするとそこそこ複雑になったりして、一般的なアプリのプロジェクトだと too much感があり、あんまり周辺の評判はよろしくありませんでした...github.com/casbin/casbin
8200スターを集める認可系フレームワーク。
もともとGo言語だけのものだったんですが、様々な言語対応版があるようです。
様々なアクセス制御モデルに対応しています。
テスト用のEditorもあって、-> (https://casbin.org/editor)これがなかなかおもしろそうでした。認可系を業務ロジックのコアから切り離す...という文脈に興味があって調べたんですが、結局こういうツールで永続化層に吐かれるものってスキーマレスであり、ユーザーロール/リソースアクセス認可とかをこれのみで表現するのは管理のコストのが高そう、ということであまり使用に自信が持てませんでした。
綺麗な使い所はどこだろう。
github.com/google/wire
DIツールです。
Dependency Injection自体は意識してやっていこう、というのが2020の方針のひとつでした。
* テスト可能性があがる
* XXXConfigみたいなのをリレーさせず階層毎のコードの責務がすっきりする
テスト書く前に疲れちゃうことも多いんですが、意味のあるテストを書く、という以上に個別に小さいユニットテストを書ける単位でモジュールを切る癖がつくという副次的効果は個人的にもチーム的にも大きかったかな、と思います。これに限らずDIツールやDIコンテナは現段階ではToo much感があったので採用しませんでしたが、グローバル層というか依存を当てる側(mainパッケージ)は現在進行形で育っているので、いつかこういうツールが欲しくなるものなのかどうか..。
2021年
つらつら書いてきて、まだまだやること試したいこと直すこといっぱいなんだなあ、というのがライブラリ選定の振り返りでも見えてきた気がします。
Go言語、来年もひとまずよろしくです。(まだ1ヶ月ある)
- 投稿日:2020-12-01T10:11:50+09:00
BDDフレームワークのgodogでブラウザで動く振る舞いのテストを書く
こんにちは!むらってぃーです。
Go3 Advent Calendar 2020の4日目を担当させていただきます。今回はGoで開発されているBDDフレームワークを使い、ブラウザで動くテストを書いていきます。
結合テストの1つとして、プロダクトに役立ちそうであれば是非参考にしていただけると嬉しいです。BDDとは
まず、BDDとは「ビヘイビア駆動開発」を指します。
ビヘイビア駆動開発の概要は下記です。BDDではスペック(仕様)とテストは限りなく近い物である。従って、テスト駆動開発における「テストファースト」は、BDDにおいては「スペックファースト」となり、スペックを作ってから実装するという、より自然な形でのプログラム製作を実現している。
いくつかのテストフレームワークは、
アプリケーションの振る舞いを記述するストーリーフレームワーク
オブジェクトの振る舞いを記述するスペックフレームワーク
の2種類を含む。出典: Wikipedia
つまりは、テストを書いてから実装するテスト駆動開発と少し異なり、仕様を書いてからその振る舞いを満たすプログラムを書く形です。
BDDフレームワーク
BDDを行うためにいくつかフレームワークが用意されています。
代表的なものとしていくつか例を上げます。
- JBehave
- Javaのテスティングフレームワーク
- xSpec
- Rubyの「RSpec」を始祖とするテスティングフレームワークの総称
- Cucumber
- 「振る舞いを、フォーマットがある自然言語で書くfeatureファイル」と「実際のテスト実行を、プログラミング言語で書くstepファイル」の2つで1つのテストを構成
- さまざまなプログラミング言語でその派生が開発されている
参考リンク: TDD/BDDの思想とテスティングフレームワークの関係を整理しよう
godogとは
BDDフレームワークであるCucumberをGoで扱う派生ライブラリです。
このライブラリを使うと、Cucumberの形式である「featureで振る舞いを書き、stepでテスト実行をGoで書く」形式でテストをかけます。https://github.com/cucumber/godog
ホットドッグをイメージしたものなのでしょうか。Gopherがパンに挟まれてる姿が可愛いです。
godogを使ってテストを書く
テストの仕様として使う題材は以前書いた記事である、Cucumber × Puppeteer × chai でBDD開発におけるE2Eテスト実行環境の構築 と同じ物を使います。
内容は下記の通りです。
- DockerのNGINXイメージを使う
- シナリオ
- シナリオ名: nginxの初期表示画面から公式ページに飛ぶことができる
- 前提条件: nginxの初期表示画面が表示されている
- アクション: nginx.com のリンクをクリックする
- 結果: 遷移したページに Welcome to NGINX! が表示されている。
ではいきましょう。
godogコマンドインストール
$ go get -u github.com/cucumber/godog/cmd/godoggo getで入れたコマンドへのパスを通しておく必要があります。
下準備はこれだけです。nginxコンテナ立ち上げ
とりあえずdockerでサクッと。
$ docker run -p 8080:80 nginxfeatureファイル用意
スペックを書くためのファイルを用意します。
godogでは、featuresディレクトリの中に置かれたスペックのファイルを自動で読み取ってくれます。
しかし、いきなりプロジェクトルートにfeaturesがあると何のこっちゃになるので、e2eというディレクトリを切ってその中に入れます。e2e/features/nginx_scenario.featureFeature: nginx画面 エンドユーザーがnginxの様々なページで動作を行うシナリオ Scenario: nginxの初期画面から公式ページに飛ぶことができる Given nginxの初期画面が表示されている When "nginx.com" のリンクをクリックする Then 遷移したページに "Welcome to NGINX!" が表示されているテストを動かす
e2eディレクトリに移動してgodogコマンドを打つと、テスト実行結果が出力されます。
$ cd e2e; godog Feature: nginx画面 エンドユーザーがnginxの様々なページで動作を行うシナリオ Scenario: nginxの初期画面から公式ページに飛ぶことができる # features/nginx_scenraio.feature:4 Given nginxの初期画面が表示されている When "nginx.com" のリンクをクリックする Then 遷移したページに "Welcome to NGINX!" が表示されている 1 scenarios (1 undefined) 3 steps (3 undefined) 647.359µs You can implement step definitions for undefined steps with these snippets: func StepDefinitioninition1(arg1 string) error { return godog.ErrPending } func StepDefinitioninition2(arg1 string) error { return godog.ErrPending } func nginx() error { return godog.ErrPending } func FeatureContext(s *godog.Suite) { s.Step(`^"([^"]*)" のリンクをクリックする$`, StepDefinitioninition1) s.Step(`^遷移したページに "([^"]*)" が表示されている$`, StepDefinitioninition2) s.Step(`^nginxの初期画面が表示されている$`, nginx) }3つのスペックに対するテストの実装がされていないという出力です。
この出力の下半分がsnippetsになっていて、これをコピペするだけでテストファイルができます。では、これをコピペしてテストファイルを作りましょう。
e2e/nginx_scenario_test.gopackage e2e import "github.com/cucumber/godog" func StepDefinitioninition1(arg1 string) error { return godog.ErrPending } func StepDefinitioninition2(arg1 string) error { return godog.ErrPending } func nginx() error { return godog.ErrPending } func FeatureContext(s *godog.Suite) { s.Step(`^"([^"]*)" のリンクをクリックする$`, StepDefinitioninition1) s.Step(`^遷移したページに "([^"]*)" が表示されている$`, StepDefinitioninition2) s.Step(`^nginxの初期画面が表示されている$`, nginx) }この状態で再度テストを動かします。すると、
❯ godog Feature: nginx画面 エンドユーザーがnginxの様々なページで動作を行うシナリオ Scenario: nginxの初期画面から公式ページに飛ぶことができる # features/nginx_scenario.feature:4 Given nginxの初期画面が表示されている # nginx_scenario_test.go:14 -> nginx TODO: write pending definition When "nginx.com" のリンクをクリックする # nginx_scenario_test.go:6 -> StepDefinitioninition1 Then 遷移したページに "Welcome to NGINX!" が表示されている # nginx_scenario_test.go:10 -> StepDefinitioninition2 1 scenarios (1 pending) 3 steps (1 pending, 2 skipped) 266.809µsこのように、1つ目のSpecでPendingで止まっているのがわかります。
テストが動いているのが確認できたので、いよいよ中身を書いていきます。
中身を書く
今回は agouti というライブラリを使って、chromedriver経由でブラウザを操作します。
テストの中身はこのようになりました。
e2e/nginx_scenario_text.gopackage e2e import ( "errors" "fmt" "time" "github.com/cucumber/godog" "github.com/sclevine/agouti" ) var globalPage *agouti.Page var globalDriver *agouti.WebDriver func SeeNginxWelcomeView() error { // ブラウザでnginxのwelcomeページにアクセス if err := globalPage.Navigate("http://localhost:8080"); err != nil { return err } h1Text, err := globalPage.Find("h1").Text() if err != nil { return err } if h1Text != "Welcome to nginx!" { return errors.New("nginx初期画面ではありません") } return nil } func ClickLink(text string) error { // text が書かれているリンクをクリックする err := globalPage.FirstByLink(text).Click() if err != nil { return err } // 遷移時間分待つ(本当はsleep使わないで頑張りたい) time.Sleep(1 * time.Second) return nil } func SeeH1(wantText string) error { h1Text, err := globalPage.Find("h1").Text() if err != nil { return err } if h1Text != wantText { return fmt.Errorf("%s は h1 要素として見つかりません", wantText) } return nil } func FeatureContext(s *godog.Suite) { // テストシナリオの前処理でセッションを用意する s.BeforeSuite(func() { globalDriver = agouti.ChromeDriver(agouti.Browser("chrome")) if err := globalDriver.Start(); err != nil { panic(err) } page, err := globalDriver.NewPage() if err != nil { panic(err) } globalPage = page }) // テストシナリオの後処理でWebdriverを止める s.AfterSuite(func() { globalDriver.Stop() }) s.Step(`^nginxの初期画面が表示されている$`, SeeNginxWelcomeView) s.Step(`^"([^"]*)" のリンクをクリックする$`, ClickLink) s.Step(`^遷移したページに "([^"]*)" が表示されている$`, SeeH1) }先ほどのシナリオに対して、それぞれのブラウザで行う操作を書いています。
この状態でgodogを動かすと下記のように出力されます。
❯ godog Feature: nginx画面 エンドユーザーがnginxの様々なページで動作を行うシナリオ Scenario: nginxの初期画面から公式ページに飛ぶことができる # features/nginx_scenario.feature:4 Given nginxの初期画面が表示されている # nginx_scenario_test.go:15 -> SeeNginxWelcomeView When "nginx.com" のリンクをクリックする # nginx_scenario_test.go:32 -> ClickLink Then 遷移したページに "Welcome to NGINX!" が表示されている # nginx_scenario_test.go:42 -> SeeH1 1 scenarios (1 passed) 3 steps (3 passed) 9.789966057s全てのStepがPassし、シナリオもPass状態となりました。
これで、godogを使ってテストを用意し、実行することができました。最後に
今回はBDDフレームワークのgodogを使ってテストを書きました。
ブラウザを使ったテストだけではなく、APIやgRPCの結合テストにも使用することができます。
スペックベースでテストを書けば、featureファイル自体が仕様を表すものにもなってきます。
そのため、チームに参画する新規メンバーのプロダクトへのキャッチアップにも使用することが可能です。是非参考にしてみてください。
- 投稿日:2020-12-01T00:19:40+09:00
なんでGo?
書くことが思い浮かばず、「Qiitaだしポエムでもいいか」と開き直ったらこんな内容になってました。
「なぜGoを使うのか?」
3年ほど前、私は「新しい言語に触れてみたい」という単純な理由からGoを書き始めました。私の周りには優秀な人が多く、そういった人々がRustやHaskellといった言語にのめり込んでいく中で私は一人Goの沼へと足を踏み入れました。実際に書き始めてみると周囲から「なんでGo?」と聞かれる機会がそれなりにありました。そして私はその質問に満足な回答を持ち合わせていませんでした。
Goは面白い言語ではない
Goについて話をする上で避けて通れない話題が仕様の小ささです。Goが採用している言語機能の数は非常に少ないです。このことはGoの言語仕様がペライチのWebページに収まっていることからも明らかです。印刷してもA4 110ページに収まります。比較対象としてC++17のWorking Draftは1440ページにも渡ります。プログラミング言語の中でも特に複雑な部類に当てはまるC++と比較するのはフェアではないかもしれませんが、スケール感の違いはおわかりいただけるかと思います。
仕様が小さいが故にそれが「不十分である」例としてよく槍玉に挙げられるのは「比較的新しい静的型付き言語なのにジェネリクスがない」ことでしょうか。ジェネリクスがないので他の言語には当たり前に実装されているような
map
やfilter
のような機能は標準的に用意されていません。Goではそういった処理は全てfor
文で行うことになります。重複する機能がほとんどないので仕様の適切な取捨選択による創意工夫をすることができず、学べることも少ないです。正直に言ってしまえばGoは「味のない、面白みのない言語」です。人によってはそれだけでGoは学ぶに値しない言語として映るかもしれません。しかし、私はこの味気なさはむしろいいことだと捉えています。その理由は私がなぜGoを書くのかという問いに対する答えに直結しています。
Simplicity is Complicated
「単純さは複雑だ」
これはGoの開発者の一人であるRob Pike氏の言葉で、dotGo 2015での同氏の発表のタイトルでもあります。私がGoを書き続ける理由を一言で表すとすればこの言葉を選ぶでしょう。Goに興味がある人やGoを書いている人でこの発表を見たことがないという方はぜひ一度見てみることを強くおすすめします。
dotGo 2015 - Rob Pike - Simplicity is Complicated https://youtu.be/rFejpH_tAHM @YouTubeより
Goの小さな仕様はしばしば「シンプル」と評されますが、それは他の現代的な言語と比較して「シンプル」なのであり必ずしもGoがシンプルさに特化しているわけではありません。仕様を究極的にシンプルにしたければそれこそBrainf*ckのような言語になってしまいます。発表中にRob Pikeが言っているように、Goは「シンプル」なのではなく、「シンプルに感じる」だけなのです。
なぜ「シンプルに感じる」のか。それは「書きやすさ」と「読みやすさ」のバランスを絶妙に取っているからです。直交する必要十分な仕様のみを採用しているので書くときにどの言語機能を使うかで迷うことはなく、見慣れない仕様を採用していないので読み方がわからないこともありません。Goを扱う時、書き手としても読み手としても脳にかかる認知的負荷が小さいと感じることが多いです。
Goはメモリ管理モデルもガベージコレクションという、ユーザにとっては非常にシンプルな(認知的負荷の小さい)手法を採用しています。その一方で、実用に耐えるレベルのガベージコレクションを実現するためにその実装は非常に複雑になっています。これもタイトルである「単純さは複雑である」ことの例として発表の中で取り上げられています。
こういった「シンプルに見える」言語デザインを追求した結果、Goは「書く」あるいは「読む」時に知らなくてはならないことが少なく、また考えることも少ないと言えます。それは一方で「書く面白さ」や「学ぶ面白さ」を削いでしまうという側面もありますが、逆に「書くことよりも、作ることに集中させてくれる」とも言えます。Goに「書く楽しさ」ではなく「作る楽しさ」を見出した時、私はGoという言語の真価を自分の内に発見しました。
書くことと作ること
そうは言っても「書くことが楽しく」かつ「作ることが楽しい」と思える言語が一番であることは間違いありません。ですが、「書くことが楽しい言語」を駆使して「作ることが楽しい」という領域に至るのはとても大変です。私は今からC++の規格書に目を通して「正しい」C++を書けるようになる労力を割きたいとは思えません。それなら勉強に必要な時間を使ってGoでなにかを作っていたほうが楽しい、というのが私の考えです。そういう意味でもGoの「一度ある機能を覚えたらそれさえ使えればいい」という割り切り方に非常にシンパシーを感じています。
私は昔から制限のある環境下で創作する時に最も創造力が働く傾向があり、そういった側面からもGo言語ととても性格的にマッチしているというのもあるのだとは思います。少ない言語機能セットの中でいかに創造的にソフトウェアを作り上げるか。明瞭でわかりやすく読みやすく使いやすいソフトウェアを作るためにどうしたらいいか。複雑な言語に関する細かな仕様をちまちまと勉強するよりも、単純な言語でどんどんモノを作っている方が性に合っていました。
最後に
こういった「モノづくり」的なプログラミングへのアプローチを持っている方にはぜひGo言語をおすすめしたいです。これからGoに触れてみようと思っている方や、なんとなくGoを敬遠していたけど興味がないわけではないという方にこの記事を通じて「そういう捉え方もあるのか」と思っていただけたのなら幸いです。
- 投稿日:2020-12-01T00:12:18+09:00
初学者向け Go言語(Golang)の変数宣言*数値型
はじめに
メモ書きです。
初歩中の初歩ですのでGoを始めたての方の助力になればと思います。変数宣言1
整数型
整数型のnumを宣言します。
変数は以下のように宣言します。var [変数名][データ型]整数型は"int"を型に用います。
package main import ( "fmt" ) func main() { var num int num = 1 fmt.Println(num) }実行結果
PS C:\pg\Go\study> go run sample.go 1int型でデータのサイズを決めることができます。
サイズを指定しなかった場合はOSやCPUに依存した数値となります。int8 //8bit min~max -128~127 int16 //16bit min~max -32768~32767 int32 //32bit min~max -2147483648~2147483647 int64 //64bit min~max -9223372036854775808~9223372036854775807小数型
小数型のnumを宣言します。
小数型は"float32 or float64"を型に用います。package main import ( "fmt" ) func main() { var num float32 num = 1.234567 fmt.Println(num) }実行結果
PS C:\pg\Go\study> go run sample.go 1.234567float型のデータのサイズは以下のようになっています。
float32 //IEEE-754 32-bit float64 //IEEE-754 64-bit変数宣言2
変数の宣言と初期化を同時に行う方法です。
よく使う(個人的な主観)ので覚えておきましょう。[変数名] := 初期値実際に書くとこんな感じです。
package main import ( "fmt" ) func main() { num := 4 fmt.Println(num) }お分かりだと思いますが出力は"4"です。
小数点型も同じです。
package main import ( "fmt" ) func main() { num := 1.234 fmt.Println(num) }出力は"1.234"です。
参考文献
golang.jp-Go言語の仕様-
http://golang.jp/go_spec最後に
Javaより好きだな。脱Javaしか書けないプログラマー!
以上
- 投稿日:2020-12-01T00:09:29+09:00
Goで不要なブランチを削除するコマンドラインツールを作ってみた
少し前にGoの勉強用で不要なブランチを削除するコマンドラインツールを作ったので簡単にまとめてみます。実際に作ったものはこちらです。
https://github.com/yuzoiwasaki/sweep使い方
こんな感じで不要なブランチを削除できます。
masterブランチ以外を削除
$ git branch * master test1 test2 $ sweep $ git branch * mastertest1ブランチ以外を削除
$ git branch * master test1 test2 $ sweep -v test1 $ git branch * test1test1ブランチとmasterブランチ以外を削除
$ git branch * master test1 test2 $ sweep -v test1 -v master $ git branch * master test1なぜ作ろうと思ったか
Goでコマンドラインツールを作ってみようと思った時に、せっかくなら自分がほしいものを作りたかったからです。
ブランチの削除自体はワンライナーでできますし、エイリアスに登録しておけば毎回わざわざ長いコマンドを入力する必要もないのですが、エイリアスも面倒なのでこういうツールがあっても良いのではないかと思いました。
なんにせよ、何かを作ろうと思った時に自分のほしいものを作るというのは良いモチベーションだと思います(自分がほしいということは、もしかしたら他の人もほしいかもしれないですし)
コード全体
コード全体としてはこんな感じのコードになります。コマンドラインから引数を受け取って処理した後、シェルコマンドを呼び出すシンプルな構造です。
sweep.gopackage main import ( "flag" "fmt" "os/exec" ) type strslice []string func (s *strslice) String() string { return fmt.Sprintf("%v", multiflag) } func (s *strslice) Set(v string) error { *s = append(*s, v) return nil } var multiflag strslice func main() { flag.Var(&multiflag, "v", "Specify the branch you want to exclude") flag.Parse() if len(multiflag) == 0 { multiflag = append(multiflag, "master") } var b, e string for _, s := range multiflag { b = s e = e + " | grep -v " + s } cmdstr := "git checkout " + b + " && git branch" + e + " | xargs git branch -D" err := exec.Command("sh", "-c", cmdstr).Run() if err != nil { fmt.Printf("Error! Failed to sweep branches.\n") } }Goには
flag
というコマンドラインオプションをパースできるパッケージがあるので、こちらを使って引数を受け取り処理をしています。
https://golang.org/pkg/flag/また、最終的にはOS上でシェルコマンドを実行したかったため、合わせて
os/exec
もインポートしています。悩んだところ
flag
は便利なのですが、そのままでは同じオプションに対して複数の値を受け取ってパースすることができません。これを実現するためには、少し独自に拡張する必要があります。
flag
はflag.Var()
を使うことで独自型の変数をバインドすることができます。またflag
にはValue
というインターフェースが定義されているため、このインターフェースを満たす形で型を定義してあげることで独自に拡張することができます。便利ですね。you can create custom flags that satisfy the Value interface (with pointer receivers) and couple them to flag parsing by
flag.Var(&flagVal, "name", "help message for flagname")
・参考
https://qiita.com/hironobu_s/items/96e8397ec453dfb976d4
https://golang.org/pkg/flag/#Value終わりに
以上、簡単ではありますがGoでコマンドラインツールを作ってみた紹介でした。もしよければ使ってみてください。
拙作ですが、同じように初めてGoでツールを作る人の参考になれば嬉しいです。今のところ仕事でGoを使う予定はないのですが、これからも趣味で触っていきたいと思います。