20201201のGoに関する記事は12件です。

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))
}

以上

最後に

私の場合、普段から変数名にスネークケースを使いがちだったのでキャメルケースで変数を定義するのは違和感がありますが使っていくうちに慣れるでしょうって感じ。

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

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をクリックしてダウンロード

627ee2cbc04080088f50a8c01127552d.png
あら、かわいい…

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の簡単なアプリ作っていければと思います!

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

AtCoder Regular Contest 108のメモ

前置き

Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。

問題

https://atcoder.jp/contests/arc108

A

Q_A.go
package 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.go
package 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

覚えてたら後で書きます。

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

完全個人用のURL管理サービスをReact×Firebase×Go×Herokuで作った話

はじめに

こんにちはRIN1208です。

この記事はタイトルに書いてある通り、完全に個人で使う用に作成したのでそれについて書いていきたいと思います。
今回作ったものはこちら

この記事はITRCアドベントカレンダーの1日目の記事になります。

 今回の構成

スクリーンショット 2020-11-25 14.17.15.png

今回は上記のような構成で作成しています。サーバーサイドはHeroku、フロントエンドはFirebase Hosingにデプロイしています。
またJWTが無効な値だった場合は6, 7の部分の処理はせずにエラーを返すようにしました。

できたもの

以下のようなURLを貼り付けて管理するだけのサービスです。完全個人用なので規約等はありません。
シンプルなのもにしたかったので機能は特にありません。
スクリーンショット 2020-12-01 16.15.20.png

使用技術

 環境

  • 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のプロジェクを開きスクリーンショット 2020-12-01 16.42.22.png
上の画像のように プロジェクトの設定 > サービスアカウント > 新しい秘密鍵の生成 をタップし生成して下さい。この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:master

CIトークンを取得する

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を使用しましたがめっちゃ便利でした。ただ個人用ですのでエラーハンドリングやログもかなり適当になってます......

また間違っている点などがございましたらコメントなどで指摘していただけると助かります。

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

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

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

daprでつくるマイクロサービス

はじめに

この記事は、 富士通クラウドテクノロジーズ Advent Calendar 20202日目の記事です。

1日目は @miyuush さんの ニフクラがTerraformに対応したので使ってみた【基礎編】 でした!
昨日リリースされたばかりで生まれたてホヤホヤ感のある nifcloud/terraform-provider-nifcloud v1.0.0 ですがこれからの機能エンハンスが楽しみですね!どんどんIaCにしていきたい:yum:

改めましてこんにちは!NIFCLOUDのいくつかのサービスのAPIを開発している @kzmake と申します。
入社しサービスを開発してはや4年目になりました。ここ数年はいくつかのサービス開発を経験し、そこそこcleanでdddなアプリケーションをかけるようになってきたました :sunglasses:
最近はどうすればスピード感ある開発ができるかな〜と考えている今日このごろです。

今日は自分が使ってみたいなぁ〜と感じている

  1. github.com/dapr/dapr について
  2. daprを使ったマイクロサービスアプリケーション実装

を紹介したいと思います!

dapr とは

image.png

dapr は、 Distributed Application Runtime という名のとおりマイクロサービスアプリケーションとして必要な機能をビルディングブロックとして提供してくれるランタイムです。stable はまだ v0.11.3 と比較的若いながら、★8.4k とかなりホット :fire: な OSSプロジェクトではないかなとおもっています。そのコンセプトは、Any language, any framework, anywhere としており多様性をもった利用ができるところもポイントですね。

本来実装したいコアロジックにサイドカーとして利用することで、簡単にマイクロサービスを作成することができます。
更にそれぞれのビルディングブロックは抽象化されており、 HTTP/gRPC API を通して利用するものとなっているため言語に縛られない開発ができるのも魅力となっています。

マイクロサービスのためのビルディングブロック

dapr が現在(2020/11/29)提供しているビルディングブロックには下記のものがあります。

dapr_bilding_blocks.png

  • 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、本番環境では何かしらのクラウドサービスなど切り替えもできるのがいいですね:tada:

ミドルウェア(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

特徴的なのが、 ratelimitoauth2 など定義済みのミドルウェアだけでなく、 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は、

となってます。今後のロードマップはこんな感じらしい。

ダッシュボード

image.png

まだ機能は少ないですが、ダッシュボードも利用できるようです。今後の機能追加も期待ですね!

IDEのサポート

言語サポートだけでなく、IDEのサポートもあるみたいです:tada:

んでなにを作ったの?

time.Now() など現在時刻を直接取得せず、マイクロサービスでシンプルな
時計サービス:timer: : 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分間経過のイベント を元に分針を 運針する
    • 時針を永続化する
    • 230 になると 24時間経過のイベント を発行する
  • minute-hand: 分針を管理するサービス
    • 時刻同期のイベント に分針を設定する
    • 60秒間経過のイベント を元に分針を 運針する
    • 分針を永続化する
    • 590 になると 60分間経過のイベント を発行する
  • second-hand: 秒針を管理するサービス
    • 時刻同期のイベント に秒針を設定する
    • 1秒間経過のイベント を元に秒針を 運針する
    • 秒針を永続化する
    • 590 になると 60秒間経過のイベント を発行する
  • ticker: 1秒をカウントするサービス
    • 一定間隔(1sec)毎1秒間経過のイベント を発行する
  • synchronizer: NTPを用いて時刻を同期するサービス
    • 一定間隔(24h)毎時刻同期のイベント を発行する

をマイクロサービスとして設計しています。

アーキテクチャ

daprを利用して以下のような設計してみました。

image.png

今回は、

を利用してみようとおもいます。

github.com/kzmake/dapr-clock

github.com/dapr/go-sdk を利用してdaprサイドカーと通信するマイクロサービスアプリケーションを github.com/kzmake/dapr-clock/microservices に各マイクロサービスを作成しました。各機能についてdaprでどう実装できるかを紹介していきたいと思います。

現在時刻を取得する機能

image.png

ユーザーは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で設定したnamenamespaceを元にコード内で永続・参照先を指定します。今回の例では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
}

一定間隔毎に針を運針する機能

image.png

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を使って実装してみました。

現在時刻を同期する機能

最後に電波時計的な機能を実装しようと思います。
image.png

先程と基本的には同じビルディングブロックを利用し、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 に乗っかることでコアロジックに集中できそうだなと改めて思いました :smile:

github.com/kzmake/dapr-clock は6マイクロサービスを組み合わせてノリで作ってみましたが、設計&開発には1日もかかりませんでした :sunglasses: これなら開発効率ももりもり上げていけそうですね :muscle: (v1.0 が待ち遠しい…)

ニフクラのサービスとして提供している hatoba 上で dapr を動かしたり、マルチクラウドに利用しても面白いかもしれないですね!

今回紹介しきれなかったものに、

があるんですが、また別の機会で紹介しようと思います。(github.com/dapr/go-sdkはまだactorを利用できないようでした…残念… #21)

さて、明日は @yaaamaaaguuu さんが VMware製品を気軽に検証するためのtips について書いてくれるようです!お楽しみに:yum:

参考文献

この記事は以下の情報を参考にしています。

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

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, return 0.

A word is a maximal substring consisting of non-space characters only.

(日本語訳)

文字列sがスペースで区切られたいくつかの単語で構成されている場合、文字列の最後の単語の長さを返します。最後の単語が存在しない場合は、を返し0ます。

言葉は、唯一の非空白文字から成る最大の部分文字列です。

Example 1:

  Input: s = "Hello World"
  Output: 5

Example 2:

  Input: s = " "
  Output: 0

考え方

  1. strip(接頭語、設備後の空白の削除)とsplit(空白で文字列を配列に分割)を使う

  2. 最後の要素の文字数を戻り値とする

  • 解答コード
  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])
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ヶ月ある)

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

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とは

スクリーンショット 2020-11-23 13.42.01.png

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/godog

go getで入れたコマンドへのパスを通しておく必要があります。
下準備はこれだけです。

nginxコンテナ立ち上げ

とりあえずdockerでサクッと。

$ docker run -p 8080:80 nginx

featureファイル用意

スペックを書くためのファイルを用意します。
godogでは、featuresディレクトリの中に置かれたスペックのファイルを自動で読み取ってくれます。
しかし、いきなりプロジェクトルートにfeaturesがあると何のこっちゃになるので、e2eというディレクトリを切ってその中に入れます。

e2e/features/nginx_scenario.feature
Feature: 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.go
package 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.go
package 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ファイル自体が仕様を表すものにもなってきます。
そのため、チームに参画する新規メンバーのプロダクトへのキャッチアップにも使用することが可能です。

是非参考にしてみてください。

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

なんでGo?

書くことが思い浮かばず、「Qiitaだしポエムでもいいか」と開き直ったらこんな内容になってました。

「なぜGoを使うのか?」

3年ほど前、私は「新しい言語に触れてみたい」という単純な理由からGoを書き始めました。私の周りには優秀な人が多く、そういった人々がRustやHaskellといった言語にのめり込んでいく中で私は一人Goの沼へと足を踏み入れました。実際に書き始めてみると周囲から「なんでGo?」と聞かれる機会がそれなりにありました。そして私はその質問に満足な回答を持ち合わせていませんでした。

Goは面白い言語ではない

Goについて話をする上で避けて通れない話題が仕様の小ささです。Goが採用している言語機能の数は非常に少ないです。このことはGoの言語仕様がペライチのWebページに収まっていることからも明らかです。印刷してもA4 110ページに収まります。比較対象としてC++17のWorking Draftは1440ページにも渡ります。プログラミング言語の中でも特に複雑な部類に当てはまるC++と比較するのはフェアではないかもしれませんが、スケール感の違いはおわかりいただけるかと思います。

仕様が小さいが故にそれが「不十分である」例としてよく槍玉に挙げられるのは「比較的新しい静的型付き言語なのにジェネリクスがない」ことでしょうか。ジェネリクスがないので他の言語には当たり前に実装されているような mapfilter のような機能は標準的に用意されていません。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を敬遠していたけど興味がないわけではないという方にこの記事を通じて「そういう捉え方もあるのか」と思っていただけたのなら幸いです。

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

初学者向け 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
1

int型でデータのサイズを決めることができます。
サイズを指定しなかった場合は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.234567

float型のデータのサイズは以下のようになっています。

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しか書けないプログラマー!
以上

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

Goで不要なブランチを削除するコマンドラインツールを作ってみた

少し前にGoの勉強用で不要なブランチを削除するコマンドラインツールを作ったので簡単にまとめてみます。実際に作ったものはこちらです。
https://github.com/yuzoiwasaki/sweep

使い方

こんな感じで不要なブランチを削除できます。

masterブランチ以外を削除

$ git branch
* master
  test1
  test2
$ sweep
$ git branch
* master

test1ブランチ以外を削除

$ git branch
* master
  test1
  test2
$ sweep -v test1
$ git branch
* test1

test1ブランチとmasterブランチ以外を削除

$ git branch
* master
  test1
  test2
$ sweep -v test1 -v master
$ git branch
* master
  test1

なぜ作ろうと思ったか

Goでコマンドラインツールを作ってみようと思った時に、せっかくなら自分がほしいものを作りたかったからです。

ブランチの削除自体はワンライナーでできますし、エイリアスに登録しておけば毎回わざわざ長いコマンドを入力する必要もないのですが、エイリアスも面倒なのでこういうツールがあっても良いのではないかと思いました。

なんにせよ、何かを作ろうと思った時に自分のほしいものを作るというのは良いモチベーションだと思います(自分がほしいということは、もしかしたら他の人もほしいかもしれないですし)

コード全体

コード全体としてはこんな感じのコードになります。コマンドラインから引数を受け取って処理した後、シェルコマンドを呼び出すシンプルな構造です。

sweep.go
package 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 は便利なのですが、そのままでは同じオプションに対して複数の値を受け取ってパースすることができません。これを実現するためには、少し独自に拡張する必要があります。

flagflag.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を使う予定はないのですが、これからも趣味で触っていきたいと思います。

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