20191219のGoに関する記事は14件です。

Goでmarkdown内のhclソースコードをfmtするCLIツール「tffmtmd」を作る過程で学んだことをまとめていく

はじめに

こちらはGo4 Advent Calendar 2019の19日目として書いております。
ギリギリ間に合った!!!

何を作ったか

gofmtmd というcliツールにinspireされて、tffmtmdというmarkdown内のhclソースコードをfmtするツールを作りました。

スクリーンショット 2019-12-19 22.12.15.png

gofmtmd でのblackfridayというmarkdown processerのmoduleの使い方を見て、「ちょっといじれば、これGoだけじゃなくMarkdown内のなんのコードでもfmtできるな??」となり、勢い余ってパクってしまいました。
その際に、Goでのmd内のソースコードの扱い方terraform fmtコマンドのソース(Goで書かれている)の構造OSSツールをリリースする際のお作法について学べたので、そこらへんをまとめていこうかなと思います。

Goでのmd内のソースコードの扱い方

MarkdownのASTを扱う場合、blackfridayを使用して、*blackfriday.NodeのWalkメソッドにNodeVisitor型を満たす関数を渡して再帰的に実行させることで実現するのが一番簡単そうです。
詳しくはアドベントカレンダーに向けてMarkdown に埋め込まれた Go のソースコードに gofmt をかけてくれるツールを作った - Qiitaをご参照ください。

以下のように、本家ではGoのCodeBlockを検知しているところを、HCLを検知してhclのsyntaxcheckをした後にformatをかけています。

func (i impleMdFile) hclFmtWalkerFunc(synerr *error) blackfriday.NodeVisitor {
    return func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
        if i.isHclCodeBlock(node) {
            _, syntaxDiags := hclsyntax.ParseConfig(node.Literal, i.filename, hcl.Pos{Line: 1, Column: 1})
            if syntaxDiags.HasErrors() {
                *synerr = errors.New("[tffmtmd] failed to format hcl source code. Please check syntax")
                return blackfriday.Terminate
            }
            result := hclwrite.Format(node.Literal)
            *i.md = bytes.ReplaceAll(*i.md, bytes.TrimRight(node.Literal, "\n"), bytes.TrimRight(result, "\n"))
        }
        return blackfriday.GoToNext
    }
}

terraform fmtコマンドのソース構造

terraform fmtってどんなソースで構成されているんやろ、みたいな知的好奇心がツール作成のモチベーションの大きな部分だったので読んでみました。

terraform/command/fmt.go
func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdiags.Diagnostics {
    var diags tfdiags.Diagnostics

    if len(paths) == 0 { // Assuming stdin, then.
        if c.write {
            diags = diags.Append(fmt.Errorf("Option -write cannot be used when reading from stdin"))
            return diags
        }
        fileDiags := c.processFile("<stdin>", stdin, stdout, true)
        diags = diags.Append(fileDiags)
        return diags
    }

    for _, path := range paths {
        path = c.normalizePath(path)
        info, err := os.Stat(path)
        if err != nil {
            diags = diags.Append(fmt.Errorf("No file or directory at %s", path))
            return diags
        }
        if info.IsDir() {
            dirDiags := c.processDir(path, stdout)
            diags = diags.Append(dirDiags)
        } else {
            switch filepath.Ext(path) {
            case ".tf", ".tfvars":
                f, err := os.Open(path)
                if err != nil {
                    // Open does not produce error messages that are end-user-appropriate,
                    // so we'll need to simplify here.
                    diags = diags.Append(fmt.Errorf("Failed to read file %s", path))
                    continue
                }

                fileDiags := c.processFile(c.normalizePath(path), f, stdout, false) //❶
                diags = diags.Append(fileDiags)
                f.Close()
            default:
                diags = diags.Append(fmt.Errorf("Only .tf and .tfvars files can be processed with terraform fmt"))
                continue
            }
        }
    }

    return diags
}

とってもシンプルな作りですね。
なるほど、.tfと.tfvarsのファイルの時だけ❶でc.processFileメソッドの中でごにょごにょやっている感じやな、c.processFileを読みに行きましょか(dirだったらc.processDirの中で同じようなことをやっている)

terraform/command/fmt.go
func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout bool) tfdiags.Diagnostics {
    var diags tfdiags.Diagnostics

    log.Printf("[TRACE] terraform fmt: Formatting %s", path)

    src, err := ioutil.ReadAll(r)
    if err != nil {
        diags = diags.Append(fmt.Errorf("Failed to read %s", path))
        return diags
    }

    // File must be parseable as HCL native syntax before we'll try to format
    // it. If not, the formatter is likely to make drastic changes that would
    // be hard for the user to undo.
    _, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.Pos{Line: 1, Column: 1})  //❷
    if syntaxDiags.HasErrors() {
        diags = diags.Append(syntaxDiags)
        return diags
    }

    result := hclwrite.Format(src) //❸

    if !bytes.Equal(src, result) {
        // Something was changed
        if c.list {
            fmt.Fprintln(w, path)
        }
        if c.write {
            err := ioutil.WriteFile(path, result, 0644)
            if err != nil {
                diags = diags.Append(fmt.Errorf("Failed to write %s", path))
                return diags
            }
        }
        if c.diff {
            diff, err := bytesDiff(src, result, path)
            if err != nil {
                diags = diags.Append(fmt.Errorf("Failed to generate diff for %s: %s", path, err))
                return diags
            }
            w.Write(diff)
        }
    }

    if !c.list && !c.write && !c.diff {
        _, err = w.Write(result)
        if err != nil {
            diags = diags.Append(fmt.Errorf("Failed to write result"))
        }
    }

    return diags
}

あら、❸のhclwrite.Formatしているだけかと思ったら、まず❷でhclsyntaxのチェックをしているのか。
syntax check前のコメントを読んでみる。

File must be parseable as HCL native syntax before we'll try to format
it. If not, the formatter is likely to make drastic changes that would
be hard for the user to undo.

ファイルはHCLのネイティブシンタックスとしてパースできるように、formatする前になっているべきで、やらないとformatterは元にユーザーが戻しづらい劇的な変化を起こしちゃう。
なるほど I understand.

ということでsyntax checkもformatもtffmtmdの処理に入れ込むことにしました。

GoのOSSツールをリリースする際のお作法

1. TableDrivenTestsを書き、Test Coverageを出しておく

GoはTableDrivenTestsで書いていくのが良いというのは、Gopher道場(卒業レポ: #5 Gopher道場を卒業しました - Qiita)で教わってから現場で実践はしてきましたが、自分の開発したツールに書いていくのは初めてでした。
可読性も上がるし、テストも追加しやすいのでこれはマストでやったほうが良いと思います。
その際、gotestsなんかで雛形をgenerateして、そこから作っていくと楽にかけるかなと思います。(jetbrains社製のIDEならデフォルトでついている)
carbon (5).png

また、ユーザーにとってTest Coverageが一目で見えることで、ツールとして安心して選びやすくなると思います。codecov.ioなんかを使用して、カバレッジをバッジ画像でREADMEにつけておくのが良さそうです。(これもgofmtmdからの受け売り。その他にGolangCIやCircleCIやgodocのバッジをつけておくととても可愛いし、ユーザーが安心して選びやすいと思う。)
設定方法の詳細はこちらを参考にしてください。 -> CircleCIとCodecovでGitHubにカバレッジバッジをつけよう! - VELTRA Engineering - Medium

スクリーンショット 2019-12-19 22.30.53.png

2. goreleaserで各プラットフォームへのバイナリを自動リリースする

goreleaserは、バイナリのクロスコンパイルと Github Releases へのデプロイ自動化ツールです。
直近のtagに対して、goreleaserコマンドを打つだけで.goreleaser.ymlファイルで指定したOSや環境に対応したバイナリをリリースしてくれます。(githubなら、tagではなく最新のコミットに紐付ける--snapshotふらぐもある)
なんて便利なんだ。。。。
スクリーンショット 2019-12-19 22.39.12.png
詳しい設定方法は公式チュートリアルをご参照ください!

最後に

ほぼほぼgofmtmdの受け売りだし簡易なツール作成でしたが、ほぼ初めてのパブリックな自家製ツールだったので、非常に勉強になることが多かったです。
あとはお仕事ではBitbucketを使っていることが多い(たまにGitlab)ので、久々にGithub使うと周辺ツールも相まって快適すぎてやばい。。

今回の記事では、本当はterraformソースからUMLをジェネるツールか、terraform-qiita-providerをGoで書く話を書くつもりでしたが、諸所ゴタゴタしている関係で今日までに完成させることができませんでした。。
それらのツールが完成したら、また諸々まとめてアウトプットしようと思っています。その際にも今回学べたことが大きく活きる気がしています。

最後に再度gofmtmdの作者の@po3rin さんに感謝の意を!ありがとうございました?‍♂️

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

GoでUFOを引っ捕らえる

いいっすよね。UFO。
あの愛らしいフォルムとか、中から何でてくるかわかんないミステリアス感とか…最高やん?

でも、皆さん実際に見たことあります?
もしかしたら………ないんじゃないですか?

「言われてみれば一度も見たことがない…?」
「小さい頃に見たあれは僕らの妄想なのか…?」
「UFOなんて本当はいないんだ…!!!」

と、不安な夜をお過ごしの方も多いと思います。

pose_zetsubou_man.png

でも安心してください。

いるんです。

そう。アメリカならね。

hakken_ufo.png

※これは FOSS4G Advent Calendar 2019 の19日目の記事です。

todo

  • UFOの目撃情報を追う
  • データを取得する
  • csvに吐き出す
  • geocodingする(位置情報を取得する)
  • csvを更新
  • GeoJSONに吐き出す
  • ブラウザの地図上に表示する

UFOの目撃情報を追う

アメリカには国立UFO情報センター(NUFORC)という1974年以来継続的に運用されている、アメリカ全土のUFO目撃情報が集約されたステキなサイトがあります。

すげえなアメリカ。

今回はこちらのデータセットを利用させてもらいましょう。

データはこちらに月ごとにまとめられており、最新のものは12月に2件ほど登録されていますが、今回は月ごとのデータが出揃った11月のものを拝借しましょう。

スクリーンショット 2019-12-14 11.06.04.png

11/2019をクリックすると以下の様にデータが表形式で格納されていることがわかります。

スクレイピングがイージーそうですね。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3230333934342f32643763353033332d363533302d653366372d373838612d3339343337336337653834352e706e67.png

ちなみに一番古いデータは西暦209年6月です。すげえな国立UFO情報センター。

スクリーンショット 2019-12-14 11.06.22.png

データを取得する

Pythonでスクレイピングなんて猫でも出来るので、今回はあまり触ったことのないGoでスクレイピングしてみましょう。

まずは$GOPATH/src/github.com/hogehoge以下にディレクトリを作成し、go.modを作成します。これでディレクトリごとにパッケージ管理できるみたい。

Goでは$GOPATH/src/github.com/hogehoge以下にソースコードを配置するのがお作法の様です。

mkdir $GOPATH/src/github.com/hogehoge/ufo_view
cd $GOPATH/src/github.com/hogehoge/ufo_view
go mod init

作成したディレクトリにmain.goを作成し、コードを書いていきましょう。

touch main.go

main.go
package main

//スクレイピングしたいURL
var targetURL = "http://www.nuforc.org/webreports/ndxe201911.html"

func main() {
}

まず先に必要なパッケージをインポートしておきます。(外部パッケージはgo get github.com/kellydunn/golang-geoの様な感じで事前にダウンロードしてください)

import (
    "flag"
    "fmt"
    "os"

    geo "github.com/kellydunn/golang-geo"

    "github.com/PuerkitoBio/goquery"
    "github.com/gocarina/gocsv"
    geojson "github.com/paulmach/go.geojson"
)

main.goの中にスクレイピングしたデータを格納するための構造体とその配列の型を定義しましょう

type ufoData struct {
    Date     string  `csv:"Date"`
    City     string  `csv:"City"`
    State    string  `csv:"State"`
    Shape    string  `csv:"Shape"`
    Duration string  `csv:"Duration"`
    Summary  string  `csv:"Summary"`
    Posted   string  `csv:"Posted"`
    Lat      float64 `csv:"Lat"`
    Lng      float64 `csv:"Lng"`
}

type ufoDates []ufoData

スクレイピングするための関数を書きます

ufoDateList := scraping.GetUFO(targetURL)

func GetUFO(url string) ufoDates {
    var ufoDataList ufoDates
    //URLを読み込んで格納
    doc, err := goquery.NewDocument(url)
    if err != nil {
        panic(err)
    }
    //mapのキーとするためのスライスを定義
    dataSample := []string{"Date", "City", "State", "Shape", "Duration", "Summary", "Posted", "Lat", "Lng"}
    f := 0
    //htmlのtrタグを取得
    doc.Find("tr").Each(func(index int, s *goquery.Selection) {

        ufo := make(map[string]string)
        //子タグ(1レコード)を取得
        s.Children().Each(func(i int, c *goquery.Selection) {
            //タグ内のtextを格納
            elements := c.Text()
            ufo[dataSample[i]] = elements
        })
        //構造体に格納
        data := ufoData{ufo["Date"],
            ufo["City"],
            ufo["State"],
            ufo["Shape"],
            ufo["Duration"],
            ufo["Summary"],
            ufo["Posted"],
            0,
            0}
        //ヘッダー行を飛ばす
        if f != 0 {
            //スライスに格納
            ufoDataList = append(ufoDataList, data)
        }
        f++
    })
    return ufoDataList
}

csvに吐き出す

取得したデータをcsvに吐き出します。

scraping.WriteCSV(afterGeoCodingDataList)

func WriteCSV(ufoDateList ufoDates) {
    file, _ := os.OpenFile("ufo_data.csv", os.O_WRONLY|os.O_CREATE, 0666)
    defer file.Close()

    gocsv.MarshalFile(&ufoDateList, file)
    return
}

作成されたcsvを見ましょう。

Date,City,State,Shape,Duration,Summary,Posted,Lat,Lng
11/30/19 06:30,New Port Richey,FL,Unknown,Few minutes,I seen something with colorful lights moving strangely. It was pretty low to be an airplane. And the way it moved was definitely not a,12/1/19,0,0
11/30/19 06:00,Plymouth,MA,Light,15 seconds,"Bright light moving in the sky at ASTRONOMICAL speed then disappearing, re-appearing  and plummeting to earth.",12/1/19,0,0
11/30/19 05:20,Lytham St Annes (UK/England),,,30-90 seconds,"15 stars moving quickly and silently over St Annes on Sea.  ((NUFORC Note:  ""Starlink"" satellites.  PD))",12/1/19,0,0
...

ちゃんと取得できてる!

geocodingする(位置情報を取得する)

気が付いた方もいるかもれませんが、元データには緯度経度の情報が含まれておらず、Lat,Lngのカラムは0となっています(ufoDataを0に設定しているので)

なので位置情報を付与していきましょう。

緯度経度の情報は入っていませんが、Cityカラムに都市名が入っていますので、この情報をもとにgeocodingしていきましょう!

※geocoding:地名などの情報から緯度経度などの位置情報を付与すること。

geocodingには、思考停止でGCPを使います。

Google Maps PlatformのGeocoding APIを叩きますのでGCPアカウントを作成しておいてください。

***APIを叩きすぎると家庭が崩壊します。不正利用にも気をつけましょう。***

クラウド破産は、こうして起きる!あっという間に請求額が!

AWSが不正利用され300万円の請求が届いてから免除までの一部始終

New Port Richeyと入力するとこんな感じで緯度経度が取得できます。

スクリーンショット 2019-12-14 15.34.31.png

こちらから試せます。

このAPIとgolang-geoパッケージを利用してgeocodingを行います。

api_keyなんかはflagパッケージを利用してコマンドライン引数からとるとか、環境変数に入れるなどしましょう。

公開すると一生後悔します。

afterGeoCodingDataList := scraping.GeoCoding(ufoDateList)

func GeoCoding(ufoDateList ufoDates) ufoDates {
    var afterGeoCodingDataList ufoDates
    var GoogleAPIKey = flag.String("a", "default", "api_keyを指定")
    flag.Parse()
    g := new(geo.GoogleGeocoder)
    geo.SetGoogleAPIKey(*GoogleAPIKey)
    //var data, _ = g.Geocode("New Port Richey")
    for _, d := range ufoDateList {
        var data, err = g.Geocode(d.City)
        if err != nil {
            d.Lat = 0
            d.Lng = 0
            afterGeoCodingDataList = append(afterGeoCodingDataList, d)
        } else {
            d.Lat = data.Lat()
            d.Lng = data.Lng()
            afterGeoCodingDataList = append(afterGeoCodingDataList, d)
        }
    }
    fmt.Println(afterGeoCodingDataList)
    return afterGeoCodingDataList
}

csvを更新

APIを叩くたびに課金される、というのは我々下級国民にとって精神的外傷が大きいので、geocodingした情報は先ほど作成したcsvに突っ込んで更新しましょう。

scraping.WriteCSV(afterGeoCodingDataList)

func WriteCSV(ufoDateList ufoDates) {
    file, _ := os.OpenFile("ufo_data.csv", os.O_WRONLY|os.O_CREATE, 0666)
    defer file.Close()

    gocsv.MarshalFile(&ufoDateList, file)
    return
}

geojsonに吐き出す

GeoJSONとは...

GeoJSONは、JavaScript Object Notation (JSON) を基とした、GISデータを記述するためのフォーマットです(地理空間データ交換フォーマット)。この形式では、Point, LineString, Polygon, MultiPoint, MultiLineString,MultiPolygon,GeometryCollectionをサポートしています。軽量言語であり、Web GISでの利用例が多く見られます。GitHubには、GeoJSONの地図表示機能があり、リポジトリにデータを配置するだけで可視化が可能です。以下では、GeoJSONで点、線、面を記述する手法について解説します。
「GIS実習オープン教材」より

とのことです。

要は位置情報の入ったJSONってことっすね。

scraping.StructToGeojson(ufoDateList)

func StructToGeojson(ufoDateList []*ufoData) {
    fc := geojson.NewFeatureCollection()
    for _, d := range ufoDateList {
        g := geojson.NewPointFeature([]float64{d.Lng, d.Lat})
        g.Properties["Date"] = d.Date
        g.Properties["City"] = d.City
        g.Properties["State"] = d.State
        g.Properties["Shape"] = d.Shape
        g.Properties["Duration"] = d.Duration
        g.Properties["Summary"] = d.Summary
        g.Properties["Posted"] = d.Posted
        fc.AddFeature(g)
    }
    fcJSON, err := fc.MarshalJSON()
    if err != nil {
        panic(err)
    }
    file, err := os.OpenFile("ufo_data.json", os.O_WRONLY|os.O_CREATE, 0600)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    file.Write(fcJSON)
}

これで以下の様なGeoJSONが吐き出されたと思います。
(整形してなくてすいません…)

{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-82.7192671,28.2441768]},"properties":{"City":"New Port Richey","Date":"11/30/19 06:30","Duration":"Few minutes","Posted":"12/1/19","Shape":"Unknown","State":"FL","Summary":"I seen something with colorful lights moving strangely. It was pretty low to be an airplane. And the way it moved was definitely not a"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-70.6672621,41.9584457]},"properties":{"City":"Plymouth","Date":"11/30/19 06:00","Duration":"15 seconds","Posted":"12/1/19","Shape":"Light","State":"MA","Summary":"Bright light moving in the sky at ASTRONOMICAL speed then disappearing, re-appearing  and plummeting to earth."}}...

ブラウザの地図上に表示する

フロントエンドはよくわからないので、ナニモワカラナイVue.jsを利用します。

# vuecliのインストール
npm i -g @vue/cli
# vueと言う名前でプロジェクトを作成して移動
vue create vue && cd vue

vue/src/assetsに作成したgeojsonの拡張子を.jsonに変更して入れていきましょう。

あと、どっかから適当にUFOの画像でも探してきて同じ場所に保存してください。

ファイルをバシバシ修正、存在しないなら作成していきます。

vue/src/main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import 'mapbox-gl/dist/mapbox-gl.css'

Vue.config.productionTip = false

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')
/vue/src/views/About.vue
<template>
    <div class="about">
        <MapPane></MapPane>
    </div>
</template>

<script>
    import MapPane from '@/components/MapPane.vue'

    export default {
        name: 'about',
        components: {
            MapPane
        }
    }
</script>
vue/src/components/MapPane.vue
<template>
    <div class='mapPane'>
        <div id='map'></div>
    </div>
</template>

<script>
    import mapboxgl from 'mapbox-gl'
    import ufo_data from '@/assets/ufo_data.json'
    import ufo_image from '@/assets/ufo.png'

    export default {
        name: 'MapPane',
        mounted: function () {
            this.mapCreate();
        },
        methods: {
            mapCreate: function () {
                let map = new mapboxgl.Map({
                    container: "map",
                    style: {
                        "version": 8,
                        "sources": {
                            "OSM": {
                                "type": "raster",
                                "tiles": ["http://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
                                "tileSize": 256,
                            }
                        },
                        "layers": [{
                            "id": "OSM",
                            "type": "raster",
                            "source": "OSM",
                            "minzoom": 0,
                            "maxzoom": 18
                        }]
                    },
                    center: [-82.7192671,28.2441768],
                    zoom: 12
                });

                map.on('load', function () {
                    map.addSource('ufo_point', {
                        type: 'geojson',
                        data: ufo_data
                    });

                    map.loadImage(ufo_image, function (error, res) {
                        map.addImage('ufo_image', res);
                    });

                    map.addLayer({
                        "id": "ufo_point",
                        "type": "symbol",
                        "source": "ufo_point",
                        "layout": {
                            "icon-image": "ufo_image",
                            "icon-allow-overlap": true,
                            "icon-size": 0.08
                        },
                    });
                });

                map.addControl(new mapboxgl.NavigationControl());
            }
        }
    }
</script>

<style scoped>
    #map {
        z-index: 0;
        height: 800px;
    }
</style>

修正したらローカルでサーバーを起動させていきましょう!

ローカルサーバーを起動

npm run serve

サーバーが立ち上がったらブラウザからhttp://localhost:8080/に接続してみましょう!

Dec-19-2019 21-13-53.gif

UFOいっぱいおる!!!!!!!!!

結論

Goは楽しい
Vue.jsは楽ちん
UFOはかわいい

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

なぜDeNA 20 新卒 Advent Calendar 2019を企画したのか【+専用アプリの解説】

はじめに

ついに最終日?
「なぜ内定者のカレンダーを企画したのか?」の理由と感想を書いていきます。
合わせて専用アプリの実装の解説も付けました?

また、この記事は@tocknとの合作記事です。

目次

章番号 タイトル
1章 内定者カレンダーを企画した理由
2章 全体の感想
3章 専用アプリ作成の発端
4章 専用アプリ作成のバックエンド実装
5章 専用アプリ作成のフロント実装
終章 さいごに

内定者カレンダーを企画した理由

「内定者で何かをしたい!」という思いと、「採用に貢献したい!」という思いが合わさった結果です。
この企画は来年で完成するもので、

  • ?‍?内定者のカレンダー
  • ?‍?新卒のカレンダー
  • ?‍?社員のカレンダー

の3つを作成することで

  • ?‍?内定者のカレンダー: どういう人が入社するのか
  • ?‍?新卒のカレンダー: 1年後の成長度、成長環境
  • ?‍?社員のカレンダー: 2年以降のキャリア

を一部ですが表現でき、DeNAという会社のことをより知ってもらえる機会になると考えました。
学生にとっては、どういう人が入社し、1年後の成長、その後のキャリアが見えるのは非常に有益な情報になると考えてます。
採用イベントで知ることが可能ですが記事という、より自然体で個人の思想がでる媒体での情報から感じ取れるものがあると私は思ってます。(就活中は各社の技術ブログはチェックしてました)

以上のような思いから企画をしました。

また、今回は「内定者vs社員!?」という企画をしたように、普通のカレンダーとは違う楽しみ方を提供できると考えています。

全体の感想

企画して本当によかったです。
内定者同士で交流が生まれたのは勿論ですが、「この子(サービス)知ってる!」となる機会も多く、同期をより知るきっかけになりました。
記事の中で研究内容や興味ある分野などを知ることもできて交流が増えました。

また、個人的にはイベントを企画する際のフローを今回で知れたのも良かったです。

申請などやり取りする中で、「弊社は頑張っている人を応援するとき、他の人を蹴落とすこと以外にNGを出すことはない」 と言っていただきましたが、本当にその通りだと感じました。

本当にいいカレンダーになったと思います。
申請など協力していただいた社員の方々、執筆者のみんな、読者の皆さん、本当に本当にありがとうございました。

専用アプリ作成の発端

vsの企画をしていた時にみんなに意見を募ったのが発端でした。
スクリーンショット 2019-12-19 14.58.00.png

そして何気なく送信したメッセージが始まりでした...。

スクリーンショット 2019-12-19 14.58.50.png

...3時間後にtocknがAPIを作りました...!?

19-12-19-14-50-51-928_deco.jpg

APIができたことでハッカソンが突如始まりましたw
(だって、APIあったら作るしかないじゃん)

19-12-19-14-51-25-135_deco.jpg

専用アプリ作成のバックエンド実装

バックエンド実装を担当した@tocknです。

naroのアイデアを見た時自分は地元のケンタッキーでお昼を食べていたのですが、すぐに実装したい衝動に駆られ、残っていたチキンフィレサンドを急いで口の中に突っ込みました。

そしてケンタッキーから家までの徒歩10分で、サーバーの構成を考えていました。

とりあえず自分はJSONを返すAPIだけ提供して、フロントは得意な人に実装してもらう形にして開発に柔軟性を持たせることにしました。

Qiitaには公式でAPIが提供されていますが、アドベントカレンダーに関するAPIは公開されていないようでした。案を見た感じ、アドベントカレンダーの内定者の総イイね数、社員の総イイね数をイイ感じに取得するAPIが必要になりそうです。そこで、カレンダーページをスクレイピングする事で総イイね数を取得する事にしました。

また、安く爆速で作るために慣れているGoを採用し、インフラはGAE, Datastoreを使う事に決めました。
(昨年度参加したDeNAのサマーインターンでGoを使ってスクレイピングを実装したのを思い出し、少しエモい気持ちになりました)

今回実装したコードはGitHubで公開しています。

ディレクトリ構成

.
├── Makefile
├── api
│   ├── handler
│   │   ├── handler.go
│   │   └── utils.go
│   └── router
│       └── router.go
├── appengine
│   ├── app.yaml
│   ├── cron.yaml
│   └── main.go
├── main.go
├── model
│   ├── article.go
│   ├── likes.go
│   └── repository
│       └── likes.go
├── persistence
│   ├── datastore
│   │   ├── article.go
│   │   └── likes.go
│   └── memory
│       ├── article.go
│       └── likes.go
└── qiita
    └── qiita.go

シンプルな構成になっていると思います。層も少ないのでそれぞれ簡単に説明していきます。

model

レスポンスのモデルおよびデータベースのモデルを定義している層です。レスポンスもデータベースも同じ構造で対応しています。楽ですね。

model/likes.go
type Likes struct {
    _kind     string    `boom:"kind" json:"-"`
    ID        string    `boom:"id" json:"-"`
    Shinsotsu int64     `json:"shinsotsu"`
    General   int64     `json:"general"`
    UpdatedAt time.Time `json:"updated_at"`
}

model/repository

データを永続化するためのインターフェースをここで定義しています。これによって永続化の実装を抽象化できるので、後述するサクッとローカルデバッグが実現できたりします。

model/repository/likes.go
type Likes interface {
    GetNew() (*model.Likes, error)
    Create(likes *model.Likes) error
}

qiita

QiitaのスクレイピングおよびAPIを叩いて値を取得する層です。以下は年、カレンダー名を引数に、そのカレンダーの総イイね数を返す関数です。

qiita/qiita.go
func GetAllLikes(year int64, title string) (int64, error) {
        doc, err := getAdventDoc(year, title)
        if err != nil {
                return 0, err
        }
        selection := doc.Find("div.adventCalendarJumbotron_stats[title=Likes]")
        likesStr := selection.Text()
        likesStr = strings.TrimSpace(likesStr)
        return strconv.ParseInt(likesStr, 10, 64)
}

api/handler

HTTPハンドラーをここで定義します。このHandler構造体はメンバにmodel/repositoryで定義したrepositoryインターフェースを持っており、New関数で生成します。インターフェースなので、Handler生成時に何を持たせるかで永続化の手法を変えることができます。(モックを持たせたり自由)

api/handler/handler.go
type Handler struct {
        likesRepo   repository.Likes
        articleRepo repository.Article
}

func New(lr repository.Likes, as repository.Article) *Handler {
        return &Handler{
                likesRepo:   lr,
                articleRepo: as,
        }
}

func (h *Handler) GetLikes(w http.ResponseWriter, r *http.Request) {
        likes, err := h.likesRepo.GetNew()
        if err != nil {
                respondError(w, r, err, http.StatusInternalServerError, nil)
                return
        }
        respondSuccess(w, r, http.StatusOK, likes)
}

persistence/datastore

GCPのDatastoreを使って永続化するための層です。model/repositoryの実装になっています。

persistence/datastore/likes.go
type likesRepository struct {
        client *boom.Boom
}

func NewLikesRepository(c *boom.Boom) repository.Likes {
        return &likesRepository{
                client: c,
        }
}

func (r *likesRepository) GetNew() (*model.Likes, error) {
        q := r.client.NewQuery("Likes").
                Order("-UpdatedAt").
                Limit(1)
        var ls []*model.Likes
        if _, err := r.client.GetAll(q, &ls); err != nil {
                return nil, err
        }
        if len(ls) == 0 {
                return nil, errors.New("not found")
        }
        return ls[0], nil
}

appengine/main

GAEで動かすためのmain関数です。ここでHandlerを生成し、Datastoreの実装を注入しています。

appengine/main.go
func main() {
        ctx := context.Background()
        if err := run(ctx); err != nil {
                panic(err)
        }
}

func run(ctx context.Context) error {
        ds, err := clouddatastore.FromContext(ctx)
        if err != nil {
                return err
        }
        c := boom.FromClient(ctx, ds)
        likeRepo := datastore.NewLikesRepository(c)
        articleRepo := datastore.NewArticleRepository(c)
        s := handler.New(likeRepo, articleRepo)
        r := router.New(s)
        http.Handle("/", r)
        appengine.Main()
        return nil
}

サクッとローカルデバッグ

APIとしてイイ感じに値を返せるか、手元でサクッとデバッグを行えるようにしたい。そこで活きてくるのがrepositoryによる永続化の抽象化です。今回persistence/memoryにも、model/repositoryの実装があります。これを使うと非常に簡単にデバッグできます。実装は以下の通りです。datastoreの実装ではclientをメンバに持っていましたが、memoryでは普通のmapを持っています。これを簡易DBとして扱います。

persistence/memory/likes.go
type likesRepository struct {
        memory []*model.Likes
}

func NewLikesRepository() repository.Likes {
        mem := make([]*model.Likes, 1)
        mem[0] = &model.Likes{
                Shinsotsu: 100,
                General:   101,
                UpdatedAt: time.Now(),
        }
        return &likesRepository{
                memory: mem,
        }
}

func (r *likesRepository) GetNew() (*model.Likes, error) {
        return r.memory[len(r.memory)-1], nil
}

そして、ルートにあるmain.goが手元デバッグ用のmainです。

main.go
func main() {
        if err := run(); err != nil {
                panic(err)
        }
}

func run() error {
        likeRepo := memory.NewLikesRepository()
        articleRepo := memory.NewArticleRepository()
        s := handler.New(likeRepo, articleRepo)
        r := router.New(s)
        log.Println("serving...")
        return http.ListenAndServe(":8080", r)
}

このようにmemoryの方を注入しているので、手元で簡単にデバッグができます!便利!(書こうと思えばテストも綺麗に書けますね?)

Cronで定期実行

リクエストのたびにQiitaをクロールしてはパフォーマンス悪いしQiitaにも申し訳ないので、Qiitaのクローリングは定期実行してGCPのDatastoreに保存します。

appengine/cron.yaml
cron:
- description: "update likes"
  url: /updatelikes
  schedule: every 10 minutes
  target: default
  timezone: Asia/Tokyo

こんな感じで定義すれば、10分おきにクロールしてくれます。

以上、バックエンド実装のお話でした。

専用アプリ作成のフロント実装

フロントエンド実装を担当した@naro143です。

「毎日記事を見るきっかけになる」ということでアプリであることは必須だったのですが、いい機会なのでPWAを初めて触るNuxtで実装することにしました。
結構汚かったり無駄な実装があると思います。
是非アドバイスください:pray:

本当に基礎的なことしかしてないので、基礎的でないところに絞って解説をします。

今回実装したコードはGitHubで公開しています。

アプリ紹介

サイトはこちら↓
https://vs-dena-advent-client.naro143.com/

nuxtの設定


コードを見る
nuxt.config.js
export default {
  mode: 'spa',
  /*
   ** Headers of the page
   */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      },
      {
        hid: 'og:image',
        property: 'og:image',
        content: 'https://vs-dena-advent-client.naro143.com/icon.png'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    htmlAttrs: {
      lang: 'ja'
    },
    bodyAttrs: {
      class: 'app'
    }
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [{ src: '~/assets/styles/app.sass', lang: 'sass' }],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [{ src: '~plugins/ga.js', mode: 'client' }],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    '@nuxtjs/google-analytics'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/pwa',
    '@nuxtjs/proxy',
    '@nuxtjs/style-resources'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {
    proxy: true
  },
  proxy: {
    '/api': {
      target: 'https://example.com',
      pathRewrite: { '^/api': '/' }
    },
    '/qiita': {
      target: 'https://qiita.com',
      pathRewrite: { '^/qiita': '/' }
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  workbox: {
    dev: true
  },
  styleResources: {
    sass: ['~/assets/styles/_variables.sass']
  },
  generate: {
    dir: 'docs'
  },
  manifest: {
    name: 'vs DeNA Advent',
    short_name: 'vs DeNA',
    start_url: '/',
    scope: '/',
    title: 'vs DeNA',
    description: 'vs DeNA Advent Calendarのアプリ',
    lang: 'ja',
    theme_color: '#5BBBB7',
    display: 'standalone',
    icons: []
  },
  googleAnalytics: {
    id: 'UA-hogehoge'
  }
}

ポイントはgithubpagesで公開する為にbuildで吐き出すディレクトリをdocsと指定しています。

Graphの生成


コードを見る
Graph.vue
<template>
  <div class="Graph">
    <h2>vs 総合いいね数</h2>
    <bar-chart :chart-data="dataCollection" :options="options" />
  </div>
</template>

<script>
import BarChart from './BarChart.js'
export default {
  name: 'Graph',
  components: {
    BarChart
  },
  data: () => {
    return {
      shinsotsu: 0,
      general: 0
    }
  },
  computed: {
    dataCollection() {
      return {
        display: false,
        labels: ['20卒内定者', '社員オールスター'],
        datasets: [
          {
            label: '総合いいね数',
            data: [this.shinsotsu, this.general],
            backgroundColor: ['#DC0451', '#FDC82F']
          }
        ]
      }
    },
    options() {
      const maxLikes = Math.max(this.shinsotsu, this.general)
      // maxGraphValue = 100(min), 200, ...
      const maxGraphValue = maxLikes + 100 - (maxLikes % 100)
      return {
        responsive: true,
        legend: {
          display: false
        },
        scales: {
          yAxes: [
            {
              display: true,
              ticks: {
                min: 0,
                max: maxGraphValue,
                stepSize: maxGraphValue / 10,
                fontSize: 16
              }
            }
          ],
          xAxes: [
            {
              display: true,
              ticks: {
                fontSize: 16
              }
            }
          ]
        },
        layout: {
          padding: {
            left: 10,
            right: 10
          }
        }
      }
    }
  },
  mounted() {
    this.getQiita()
  },
  methods: {
    async getQiita() {
      await this.$axios
        .get('https://example.com/likes')
        .then((response) => {
          this.shinsotsu = response.data.shinsotsu
          this.general = response.data.general
        })
    }
  }
}
</script>

<style scoped lang="sass">
.Graph
  h2
    margin-left: 10px
    margin-right: 10px
    border-bottom: 4px solid $color-primary
</style>

いいね数の棒グラフの描画にはvue-chartjsを使用しました。
使用法はchart.jsと同じです。
ラベルと、データ、グラフの色を設定します。
また、グラフの最大値と間隔はいいね数の最大値から計算をして設定しています。
グラフの色はDeNAカラーを参照しました。

https://dena.com/jp/company/policy/logoguide.html

さいごに

戦略はありましたが、勢いで始めた企画でした。
前例もなく、申請のフローもわからないので質問をしまくって動きまくって、人を巻き込むだけ巻き込みました。

同期にも全体で参加を募って、個人でもメッセージを送って誘いました。
みんな快く承諾してくれて、忙しい中で最高の記事を書き上げてくれました。

多くの方が記事に反応して、盛り上がってくれました。
社員さんからも「順調だね」と言っていただけました。

本当に大変でしたが最高の1ヶ月でした。
一生の思い出になると思います。
「早く行くなら一人でいけ、遠くまで行きたいならみんなと行け」を体感しました。

本当にありがとうございました。

来年の私たちのカレンダーも楽しみにお待ちください!?

未来の21卒の内定者へ、前例とフローはできたのでぜひ企画を検討してみてください

質問とか-> https://twitter.com/naro143
採用サイト-> https://student.dena.com/

カレンダーまとめ

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

眺めて覚えるGo言語

眺めながら学習する超簡単言語!

いきなりGOLANG

image.png

ex1.go
package main
import (
    "fmt"
)
func main() {
    fmt.Println("hello world")
}
C:\Users\hirat\go-work\web\hello>go run ex1.go
hello world

お決まりのコンソール出力

ex2.go
package main
import (
    "fmt"
)
func main(){
    a:=10
    b:=20
    c:=a+b
    fmt.Println(a,b,c)
}
C:\Users\hirat\go-work\web\hello>go run ex2.go
10 20 30

単純整数計算でした。

ex3.go
package main
import (
    "fmt"
)
func main() {
    a := 10
    b := 20
    c := a * b
    fmt.Printf("a=%v b=%v c=%v", a, b, c)
}
C:\Users\hirat\go-work\web\hello>go run ex3.go
a=10 b=20 c=200

Printfを使って出力

ex4.go
package main

import (
    "fmt"
)

func main() {
    a := 10
    b := 3
    c := a / b
    fmt.Printf("a=%v b=%v c=%v", a, b, c)
}
C:\Users\hirat\go-work\web\hello>go run ex4.go
a=10 b=3 c=3

整数で計算でした

ex5.go
package main

import (
    "fmt"
)
func main() {
    a := 10.0
    b := 3.0
    c := a / b
    fmt.Printf("a=%v b=%v c=%v", a, b, c)
}

go run ex5.go
a=10 b=3 c=3.3333333333333335

浮動小数点になります

ex6.go
package main

import (
    "fmt"
)

func main() {
    s:=0
    for i:=1;i<=10;i++{
        s+=i
    }
    fmt.Printf("s=%v",s)
}
C:\Users\hirat\go-work\web\hello>go run ex6.go
s=55

サクッとfor loop

ex7.go
package main

import (
    "fmt"
    "strconv"
)

func main() {
    for i:=1;i<=10;i++{
        s:="Hello "+strconv.Itoa(i)+"\n" //intから文字列に変換
        fmt.Printf(s)
    }   
}
C:\Users\hirat\go-work\web\hello>go run ex7.go
Hello 1
Hello 2
Hello 3
Hello 4
Hello 5
Hello 6
Hello 7
Hello 8
Hello 9
Hello 10

整数を文字列に変換して(strconv.Itoa)

ex8.go
package main
import (
    "fmt"
)
func main(){
    a:=[]int{1,2,3,4,5,6,7,8,9,10}
    for _,x:=range a{
        fmt.Println(x)
    }    
}
C:\Users\hirat\go-work\web\hello>go run ex8.go
1
2
3
4
5
6
7
8
9
10

サクッとforeach

ex9.go
package main
import (
    "fmt"
    "strings"
)
func main(){
    a:=strings.Split("千葉,西千葉,稲毛,新検見川,幕張,幕張本郷,津田沼,東船橋,船橋,西船橋,下総中山,本八幡,市川,小岩,新小岩,平井,亀戸,錦糸町,両国,浅草橋,秋葉原",",")
    for i,x:=range a{
        fmt.Println(i,x)
    }    
}
C:\Users\hirat\go-work\web\hello>go run ex9.go
0 千葉
1 西千葉
2 稲毛
3 新検見川
4 幕張
5 幕張本郷
6 津田沼
7 東船橋
8 船橋
9 西船橋
10 下総中山
11 本八幡
12 市川
13 小岩
14 新小岩
15 平井
16 亀戸
17 錦糸町
18 両国
19 浅草橋
20 秋葉原

サクッとforeach index

ex10.go
package main

import (
    "fmt"
    "strings"
)
func main(){
    a:=strings.Split("千葉,西千葉,稲毛,新検見川,幕張,幕張本郷,津田沼,東船橋,船橋,西船橋,下総中山,本八幡,市川,小岩,新小岩,平井,亀戸,錦糸町,両国,浅草橋,秋葉原",",")
    for i,x:=range a{
        if strings.Contains(x,"新"){
            fmt.Println(i,x,"新が含まれます。")
        } else if strings.Contains(x,"橋"){
            fmt.Println(i,x,"橋が含まれます。")
        } else {
            fmt.Println(i,x)
        }       
    }    
}
C:\Users\hirat\go-work\web\hello>go run ex10.go
0 千葉
1 西千葉
2 稲毛
3 新検見川 新が含まれます
4 幕張
5 幕張本郷
6 津田沼
7 東船橋 橋が含まれます
8 船橋 橋が含まれます
9 西船橋 橋が含まれます
10 下総中山
11 本八幡
12 市川
13 小岩
14 新小岩 新が含まれます
15 平井
16 亀戸
17 錦糸町
18 両国
19 浅草橋 橋が含まれます
20 秋葉原

とりあえずこれだけ知っていたら書けます。

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

なぜGoを研究で使うのか

はじめに

この記事は Qiita Advent Calendar GO5 の記事です。
大学・高専の教員や学生に向けてGoを紹介します。

Advent Calendar で書くには薄い内容の記事ですが、いち学生として研究でプログラム書くうえで感じた問題点を交えながら紹介するよう心がけました。

この記事をきっかけに研究でGoを使うことに興味を持っていただけたら幸いです。

対象読者は学生や教員を想定していますが、非エンジニアの方などもでも通じるところはあるのでは無いかと思います。

非情報系の研究室のプログラムについて

私はある高専の非情報系の学科の学生です。

普段は音響工学に関する研究をしているのですが、研究をする上でプログラムを書くことが多々あります。

私は研究用のプログラムで大事なのは再利用性であると考えています。研究でプログラムを使うときは、教員や先輩の残したコードを改良したり参照したりすることがあるのですが、エラーメッセージが適当であったり、スコープが分かりづらかったりと、読むだけで時間がかかります。これらは結果的に本質的でないところに時間をかけることになり、研究者にとっては致命的なロスに繋がります。

特に、高専はその特性上、1年ほどで研究室のメンバーがガラリと入れ替わるので、プログラムを再利用できることは死活問題になります。

なにが問題となっているのか

毎年、新たに研究室に加入する学生は、授業でCやPythonを習っていますが、実務に応用できるほどのことは習っていない学生がほとんどという印象があります(主観)。

そのため、新人の研究室員は実質的に1からプログラムを覚えることになります。しかし、研究に使える時間は限られているので、必然的に基礎がわかっているCもしくはPythonを勉強することになります。

したがって、先代たちが作成したプログラムはCやPythonで書かれているものがほとんどになります。

しかし、経験上CやPythonで書かれたプログラムは再利用し辛いものが多いです。

ドキュメントやコメントをしっかり書くように教えればいいのかもしれませんが、これはどの言語を使うにしても必要なことですし、全ての学生に教え込むのは無理があると思います。

したがって、CやPythonで研究用のコードを書くのが問題なのではないかと考えました。

半分こじつけです。C、Pythonごめんなさい。

なぜCではだめなのか

教員達は世代的に(?) Cを書ける方が多く、学生は教員のサポートを受けやすくなります。また、型が明示的であることや変数宣言がある点は読みやすいプログラムを書くうえでメリットとなります。

一方で、Cを使うと以下の問題が多く起きます。

  • コンパイラのエラーメッセージが分かりづらい
  • メモリ管理が必要
    • 安全なコードを書くことは経験の少ない学生とって難しい
  • ライブラリのやリンクの概念の学習コストが高い
    • リンク?どういうこと?
    • Makefile? なにそれ
    • 巨大なプログラム
    • 繰り返されるコピペ
    • コピペしたものを改良するため、一見同じでも動かない紛らわしいやつ
    • OSのバージョンが変わると動かなくなる実行ファイル

勉強する学生にとっても、教える教員にとっても地獄です。

もちろんCにも良い点はありますが、初心者からすると使いづらいと言わざるを得ないと思います。

なぜPythonではだめなのか

最近はこのような流れからPythonを使う研究室も増えてきています。

PythonはCに比べて入門しやすく、ライブラリも豊富です。

特にnumpyやscipyなどは研究をする上で非常に便利なツールとなります。

しかしながらPythonを使うとまた別の問題が発生します。

  • Pythonは簡単に動くコードをかけるが、言語仕様が悪い意味で柔軟すぎる
    結果的に本人しかわからないコードができてしまう
  • 値渡しなのか参照渡しなのか 見つけづらい副作用
  • 型がコードに書かれない
    intなのかfloatなのか(特に数値演算)
  • ライブラリが多機能ものが多く、学習コストがかかる
    • ソースコードがpipで隠蔽されており簡単に見ることができない (Goにくらべて)
    • 高度な言語使用を使用して作られている場合が多く、初心者にはコードが読みづらい
    • ライブラリがCで書かれていることもある
  • 個人的にnumpyは便利だが曖昧でも動いてしまう点が気に食わないです(転置せずとも行列演算ができてしまうなど)

Pythonはささっと書いて動かすプログラムを作るには非常に便利ですが、後に継承されていくような比較的大きめのプログラムを作るには不向きという印象があります。

ここまでで出てきたプログラムとして想定しているのは

  • 一連の実験の実行
  • 軽い数値演算
  • バッチ処理
  • ファイル整理

などです。

全てにおいてGoが最高というわけではなく、

  • ゴリゴリの数値演算やリアルタイム性が必要なものはCやC++。
  • データ処理やグラフ化ならPython

など、はっきりとした目的があるならば用途に合った言語が最適だと思います。

Goを使いましょう

私は、CやPythonの問題を解決するにはGoを使うのが手っ取り早いと考えています。

Goは

  • シンプルなので学習コストが低い
  • Cが分かればGoもだいたい分かるので教員も馴染みやすい
  • 動かないものはコンパイルされない
  • 誰が書いても同じコードになりやすい
  • Cのように強い型付けがある
  • Pythonのようにシンプルな記法
  • コンパイラのエラーメッセージが親切
  • ガーベッジコレクションがあるため、メモリ管理を基本的に考えなくて良い

といった特徴の言語です。

このような特徴から、学生にとっても教員にとっても学習しやすく、バランスの取れた言語であると思います。

また、特筆するべき点は誰が書いても同じコードになりやすいという点です。研究に使う上ではこれは非常に大きなメリットになります。

詳しくはGoについて非常によくまとめられたmattnさんのスライド( プログラミング言語 Goのススメ)が参考になります。

Goのライブラリ管理

また、ライブラリに関しても

  • Goでシンプルに書かれている事が多く、中身を参照しやすい
  • ライブラリと自作のソースコードが同じように管理される
  • 基本的にgithubからダウンロードして使うことになる

といった特徴があるので、ソースコードを参照するハードルが低く、プログラムの透明度が高くなります。

Goのクロスコンパイル

さらにGoの特徴の1つとしてクロスコンパイルが容易にできるというものがあります。

私は研究テーマの都合上Raspberry Piなどを使うことが多いのですが、プログラムをデスクトップで開発し、コンパイルしたものRaspberry Piに送って動かすということを良く行います。

Cの資産の活用

Goにはcgoという機能があり、Goのソースの中にCを埋め込むことが可能です。また、Cのライブラリを読み込むことができます。したがって、Cで書かれた資産を書き換える必要も(ほぼ)ありません。

パフォーマンスは落ちてしまいますが、速度が必要でなければ使用する価値は十分にあります。

Goのコミュニティについて

作ったライブラリはgithubで簡単に公開・利用できるので、他の研究者とノウハウの共有も簡単に行えるようになると思います。

また、Goのライブラリはまだまだ少ない状況です。これはOSSのコミッターになれるチャンスです。

是非、専門知識をコードにして貢献しましょう。

Goを研究に使用した例

蛇足ですが、私は適応アルゴリズムを利用した研究をGoを使用して行っています。

適応アルゴリズムとは簡単にいうと、過去の入力信号から次の信号を予測するアルゴリズムです。

demo

稚拙なプログラムですが、Githubでライブラリを公開しています。

今は基本のアルゴリズムしかありませんが、順次発展的なアルゴリズムを増やす予定です。また、Goの並行処理を活かしたプログラムも書きたいと考えています。興味のある方は見て頂けると幸いです。

github.com/tetsuzawa/go-adflib

まとめ

Goのコードが全く無く恐縮ですが、ここまで読んでいただきありがとうございました。

間違い・意見等あれば指摘していただけると幸いです。

教員・学生のみなさん、Goを書きましょう。

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

Go言語の crypto/xxx は New して Sum するより直接 Sum する方が速い

穴埋めで小さい Tips を紹介します。

ところでGoを書いてる皆さん、こんなコード良く見ますよね。

hash := md5.New()
r := hash.Sum(input)

hash に対して繰り返し Write して、最後に Sum する様な処理だとこれで良いのですが crypto の下にあるパッケージの多くは md5.Sum(input) の様に直接、合計ハッシュ値を得られる様になっています。

package sum

import (
    "crypto/md5"
    "testing"
)

var input = []byte("あめんぼあかいなあいうえお")

func BenchmarkHashNewSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        hash := md5.New()
        _ = hash.Sum(input)
    }
}

func BenchmarkHashSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = md5.Sum(input)
    }
}

しかも速いと来たもんだ。

goos: windows
goarch: amd64
pkg: github.com/mattn/sum-bench
BenchmarkHashNewSum-4        5239874           222 ns/op         176 B/op          2 allocs/op
BenchmarkHashSum-4           9022040           132 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/mattn/sum-bench  2.819s

標準パッケージだからこれ以上速くならないと思っておられるかもしれません。しかし使い方を変えるだけでもっと速くできる箇所が見つかったりするかもしれませんね。

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

HackerNewsをCUIで見たい!! (ライブラリ探しと実装)

最終的に作るもの

HackerNewsのTUIコマンドであるhnをGoで作成します。
hn_preview.gif

背景

hacker-news1.jpg

HackerNewsをわりと良く見るのですが、terminalでも見れないかなーと思っていた所、良い感じなのがありました。

言語 URL
Python https://github.com/donnemartin/haxor-news
Go https://github.com/andrewstuart/hn

最近少し、Goの勉強をしているということでGo製の方を動かしたい!と思っていたら。。。
依存しているライブラリがインストールできない?!見に行くと2年くらい更新されておらず。。。原因を突き止めるのも面倒だと思い、自分で作ることにしました。

ライブラリ調査

単なるcli操作でHackNewsの一覧を出すのでは、面白くないと思い。TUI(テキストユーザインタフェース)のライブラリを探しました。
TUIはCUIと違い、GUIのように画面全体を利用します。しかし、GUIとも異なり一般的なテキスト端末で表示できる記号や文字だけで画面を構成します。

こんな感じの見た目になります。↓↓
image.png
termuiのREADMEから引用

私自身ライブラリを探す時はawesome-goから見に行き、その後にGithub内を調査しています。今回探したのもawesome-goから見つけました。

以下は自分がHackerNews TUIを作成する際に見つけた候補ライブラリ集です。
それぞれのREADMEにはGIF画像が挿入されているのでよりわかりやすいです。ぜひ見てみてください:bow:

tui-go

GitHub - marcusolsson/tui-go: A UI library for terminal applications.
image.png

term-ui

GitHub - gizak/termui: Golang terminal dashboard
image.png

termbox-go

GitHub - nsf/termbox-go: Pure Go termbox implementation
image.png

tview

GitHub - rivo/tview: Rich interactive widgets for terminal-based UIs written in Go
image.png

termdash

GitHub - mum4k/termdash: Terminal based dashboard.

image.png

HackerNews CUIの実装

今回は、demo,exampleディレクトリなどの実装を見てしっくりきたtviewを利用しました。

HackerNews APIから記事一覧を取得

HackerNews CUIの実装を見ると、goqueryなどでスクレイピングをしている実装も多く見られましたが、今回はFirebaseで用意されているHackerNes APIを使用します。

hn_api.go
package main

import (
    "encoding/json"
    "github.com/otiai10/opengraph"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"
)

type HackerNews struct {
    By          string `json:"by"`
    Score       int    `json:"score"`
    Title       string `json:"title"`
    Type        string `json:"type"`
    Url         string `json:"url"`
    Description string
}

func GetHackerNews(n int) []HackerNews {
    res, err := http.Get("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty")
    if err != nil {
        log.Fatal(err)
    }

    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        log.Fatal(err)
    }
    var idHn []int
    json.Unmarshal(body, &idHn)

    var hns []HackerNews
    var hn HackerNews
    cnt := 0
    for _, s := range idHn {
        if cnt > n-1 {
            break
        }
        url := "https://hacker-news.firebaseio.com/v0/item/" + strconv.Itoa(s) + ".json?print=pretty"
        res, _ := http.Get(url)
        body, _ := ioutil.ReadAll(res.Body)
        json.Unmarshal(body, &hn)
        og, err := opengraph.Fetch(hn.Url)
        if err != nil {
            log.Fatal(err)
        }
        hn.Description = og.Description
        hns = append(hns, hn)
        cnt += 1
    }

    return hns
}

HackerNews APIのそれぞれの記事の詳細を取得すると以下の様なjsonが返ってくると思います。hn_api.goはこれから必要な情報を構造体にぶち込んで行き、その構造体の配列を返します。

{
  "by" : "dhouston",
  "descendants" : 71,
  "id" : 8863,
  "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ],
  "score" : 111,
  "time" : 1175714200,
  "title" : "My YC app: Dropbox - Throw away your USB drive",
  "type" : "story",
  "url" : "http://www.getdropbox.com/u/2/screencast.html"
}

HackerNewsの記事のタイトルだけでは、内容がよくわからないことがあるので、その記事のURLからog:descriptionを取得することにしました。

og:descriptionの取得には、otiai10/opengraphというOpen Graph Parserを利用しました。urlを指定するだけで戻り値として構造体が返却され、メソッドチェーンで使用できるため利用しました。

TUI作成

uiを作成するui.goです。

ui.go
package main

import (
    "fmt"
    ui "github.com/gizak/termui/v3"
    "github.com/gizak/termui/v3/widgets"
    "log"
    "strconv"
)

type nodeValue string

func (nv nodeValue) String() string {
    return string(nv)
}

func generateTreeNodes(n int) []*widgets.TreeNode {
    hns := GetHackerNews(n)
    var nodes []*widgets.TreeNode
    for _, hn := range hns {
        fmt.Println(hn.Score)
        node := widgets.TreeNode{
            Value: nodeValue(hn.Title),
            Nodes: []*widgets.TreeNode{
                {
                    Value: nodeValue("Score: " + strconv.Itoa(hn.Score)),
                    Nodes: nil,
                },

                {
                    Value: nodeValue("Type: " + hn.Type),
                    Nodes: nil,
                },
                {
                    Value: nodeValue("Author: " + hn.By),
                    Nodes: nil,
                },
                {
                    Value: nodeValue("cmd+click →  " + hn.Url),
                    Nodes: nil,
                },
                {
                    Value: nodeValue("Description"),
                    Nodes: []*widgets.TreeNode{
                        {
                            Value: nodeValue(hn.Description),
                            Nodes: nil,
                        },
                    },
                },
            },
        }
        nodes = append(nodes, &node)
    }

    return nodes
}

func hnUi(n int) {

    if err := ui.Init(); err != nil {
        log.Fatalf("failed to init")
    }
    defer ui.Close()

    nodes := generateTreeNodes(n)

    t := widgets.NewTree()
    t.Title = "Hacker News ClI"
    t.TextStyle = ui.NewStyle(ui.ColorYellow)
    t.WrapText = false
    t.SetNodes(nodes)
    x, y := ui.TerminalDimensions()
    t.SetRect(0, 0, x, y)
    ui.Render(t)
    Keybindings(t)
}

tviewのtreeview demoを参考に作成しました。

対応するキーバインドはkeybindings.goに記述しました。

keybindings.go
package main

import (
    ui "github.com/gizak/termui/v3"
    "github.com/gizak/termui/v3/widgets"
)

func Keybindings(t *widgets.Tree) {
    uiEvents := ui.PollEvents()
    for {
        e := <-uiEvents
        switch e.ID {
        case "q", "<C-c>":
            return
        case "k":
            t.ScrollUp()
        case "j":
            t.ScrollDown()
        case "E":
            t.ExpandAll()
        case "C":
            t.CollapseAll()
        case "<Enter>":
            t.ToggleExpand()
        }
        ui.Render(t)
    }
}

switch文に登録されているキーが入力されると、それに合わせた画面描画処理と再レンダリングが行われます。

cliのオプションを設定する

最後に、cliのオプションを設定するためにurfave/cliを使用します。
urfave/cliはGoでコマンドラインアプリを構築するためのシンプルで高速で楽しいパッケージです。

hn.go
package main

import (
    "github.com/urfave/cli"
    "os"
)

func main() {
    app := cli.NewApp()
    app.Name = "hn"
    app.Usage = "This is a tool to see 'Hacker News' made with Go"

    app.Flags = []cli.Flag{
        cli.IntFlag{
            Name:  "number, n",
            Value: 10,
            Usage: "option for number of Hacker News acquisitions",
        },
    }

    app.Action = func(c *cli.Context) error {
        hnUi(c.Int("number"))

        return nil
    }
    app.Run(os.Args)
}

並行処理で高速化

HackerNewsから各記事の詳細を取ってくる部分が遅いのでGoroutineを使って高速化しようと思います。

Goroutine無

現在のAPIクライアントはこんな感じです。記事のidの配列数文だけgetリクエストを回している。

measurement/hn_api.go
func GetHackerNewsDetail(ids []int) []HackerNews {
    var hns []HackerNews
    var hn HackerNews
    for _, s := range ids {
        url := "https://hacker-news.firebaseio.com/v0/item/" + strconv.Itoa(s) + ".json?print=pretty"
        res, _ := http.Get(url)
        body, _ := ioutil.ReadAll(res.Body)
        json.Unmarshal(body, &hn)
        og, err := opengraph.Fetch(hn.Url)
        if err != nil {
            log.Fatal(err)
        }
        hn.Description = og.Description
        hns = append(hns, hn)
    }
    fmt.Println()
    return hns
}

idの数を20個に設定し、計測してみると

❯ go run measurement/hn_api.go
18.335145秒

Goroutine有

Gorutineを使って書き換えて見ました。
各Gorutineで各記事の詳細urlのgetリクエストを並行処理させ、HackerNewsの構造体をチャネルで渡します。

measurement/hn_api_goroutine.go
func GetHackerNewsDetail(ids []int) []HackerNews {
    wg := new(sync.WaitGroup)
    var hns []HackerNews
    var chn = make(chan HackerNews, len(ids))
    for _, s := range ids {
        wg.Add(1)
        url := "https://hacker-news.firebaseio.com/v0/item/" + strconv.Itoa(s) + ".json?print=pretty"
        var hn HackerNews
        go func(url string) {
            res, _ := http.Get(url)
            body, _ := ioutil.ReadAll(res.Body)
            json.Unmarshal(body, &hn)
            if hn.Url != "" {
                og, err := opengraph.Fetch(hn.Url)
                if err != nil {
                    log.Fatal(err)
                }
                hn.Description = og.Description
            }
            chn <- hn
            wg.Done()
        }(url)
        hns = append(hns, <-chn)
    }
    defer close(chn)
    wg.Wait()
    fmt.Println()
    return hns
}

こちらもidの数を20個に設定し、計測してみると

❯ go run measurement/hn_api_goroutine.go
13.051096秒

なんと5秒も速くなっている!
Goは簡単に並行処理が実装できて良いですね。

※この速さはgetリクエストということもあり、ネットワーク速度やPCの性能に依存してしまうので環境によって秒数はことなると思います。

最後に

作成したものをGitHubにあげ、go getをすると使用できるようになります。

$ go get github.com/<Account Name>/hn

hnの実装リポジトリを載せておきます。

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

go による transaction-aware な repository 層の実装の一例

Clean Architecture などにおいては、DB などのアクセスを抽象化した repository 層を作成することと思います。この repository 層を自分が作るとほぼ全ての言語でいつも同じ方式になるのですが、あまり同じ形式を見ないので、その方法を共有します。今回の実装言語は go としますが、rust でも Java でも Scala でも毎回この方式になるので、実装は言語に依存したものではありません。なお、この形式は超大規模なものにはおそらく向いていないので、そこだけお断りしておきます。

以下では、適当な TODO アプリの様なデータを仮定します。RDB でいうと、users, tasks テーブルがあると思ってください。対応するモデルの定義は以下とします。

type UserID int
type User struct {
    ID   UserID
    Name string
}

type TaskID int
type Task struct {
    ID      TaskID
    UserID  UserID
    Content string
}

ID には独自の型を与え、ID の取り違いが検出できる様にしています。

Repository のインターフェース

ブログなどを読んでいる限り、users テーブルや tasks テーブルへのアクセスは、UserRepositroyTaskRepository などのインタフェースを作ってそれを実装するようにすることが多いように思います。その場合、DB へのコネクションをどう表現するかが問題になりがちです。UserRepository の中でコネクションを取る様にしてしまうと、トランザクションで複数テーブルを同時に更新する場合に、UserRepositoryusers 以外の テーブルを変更する必要が生じるなど、うまく表現できません。そこで、コネクションは、UserRepository の様なインタフェースの外から与えるような形にした方が良いでしょう。今回紹介する形式は、その辺りの問題を解消しています。

まず Repository として、新しい接続を取れる関数だけを持つ、という抽象化を行ないます。

type Repository interface {
    NewConnection() (Connection, error)
    MustConnection() Connection
}

必要なのは NewConnection だけなのですが、テストで使うために MustConnection を生やしていることが多いです。

Connection は、次の様なインタフェースになっており、実際に users テーブルや tasks テーブルにアクセスできるインタフェースを Connection 経由で取ることができます。

type Connection interface {
    Close() error
    RunTransaction(func(tx Transaction) error) error

    User() UserQuery
    Task() TaskQuery
}

type Transaction interface {
    User() UserCommand
    Task() TaskCommand
}

なお、users テーブルや tasks テーブルにアクセスできるインタフェースを適当な関数にしたり Repository 経由にしてしまうと、実際に UserQuery などを得るとき、次のパターンのいずれかの実装になると思われます。

r := NewRepository()
con := r.MustConnection()

// パターン A
user, err := UserRepository(con).Find(id)

// パターン B
user, err := r.User(con).Find(id)

// パターン C
user, err := r.User().Find(con, id)

ここで、repository 層をインタフェース経由で作るのは、その実際の実装が RDB にアクセスしてもいいし、テスト用の mock でもいいようにするため、ということを考慮に入れましょう。(A) は、con の実際の実装型によってUserRepository(con) が返す実装が異なるため、type switch が入りそうで NG (入らない様な実装にすることはできるが、結局それだったら con 経由と変わらない)。(B), (C) は、 rcon が一致しないようなコード (e.g. r はテスト用に作った fake な Repository なのに、con は本物の RDB 接続)を書くことができてしまうので、あまり好みではありません (もちろん実際にはそんなことは誰もしないのですが)。そもそも con から取れれば r はここでは不要なので、わざわざ r から取る理由がないような気がします。

Connection の説明に戻ります。

Close() はコネクションを閉じ、RunTransaction は transaction の実行を抽象化します。

User(), Task() メソッドは、users, tasks テーブルからデータを取ってくるためのインタフェースを返します。Transaction 型にも同様の User(), Task() メソッドが生えており、こちらは更新もできるような インタフェースを返します。具体的には次のようなものが考えられます。

type UserQuery interface {
    Find(id UserID) (*User, error)
    List(filter UserFilter) ([]*User, error)
}

type UserCommand interface {
    UserQuery

    UpdateName(id UserID, name string) error
}

type UserFilter struct {
    NameLike string
}

type TaskQuery interface {
    Find(id TaskID) (*Task, error)
    List(filter TaskFilter) ([]*Task, error)
}

type TaskCommand interface {
    TaskQuery

    UpdateContent(id TaskID, content string) error
}

type TaskFilter struct {
    UserID UserID
}

これは私的定型ですが、複数返すものは大体 List という名前にしており、その検索条件を Filter という形で書く様にしています。

実際の使い方としては、次に様になります。

// DB 用の repository を作成
r := db.NewRepository()
// テストでは r := fake.NewRepository() となるかもしれない

con, err := r.NewConnection()
if err != nil {
    return err
}
defer con.Close()

// User を取得
user, err := con.User().Find(UserID(1))
...

// Task を取得
tasks, err := con.Task().List(TaskFilter{
    UserID: UserID(1),
})
...

この方式の欠点としては、複数テーブルを JOIN してデータを返す様な場合が上手く表現しづらいです。主テーブルと思われるテーブル用のインタフェースに間借りしてメソッドを生やしていることが多いです。あるいは、JOIN したテーブル用の Query/Command interface を作ることもあります。

更新は必ず RunTransaction を経由して行ないます。Transaction インタフェースは、RunTransaction でしか得ることができず、更新系のメソッドは UserCommand などの Command 系にしか実装しない様にすることで、かならずトランザクション実行中にのみ更新が行なわれることを保証できます。

err = con.RunTransaction(func(tx Transaction) error {
    err := tx.Task().UpdateContent(TaskID(1), "new content")
    if err != nil {
        return err
    }

    return nil
})
if err != nil {
    // some error happened. internal error, commit error, etc.
    return err
}

実際の実装

さて、実際の実装を見てみましょう。ここでは、gorm を用いた実装を用意しました。すべての実装は、https://github.com/mayah/go-repository-sample にあげてあります。

Repository, Connection, Transaction は単に *gorm.DB を持つだけです。

func NewRepository(db *gorm.DB) repository.Repository {
    return &dbRepository{
        db: db,
    }
}

type dbRepository struct {
    db *gorm.DB
}

type dbConnection struct {
    db *gorm.DB
}

type dbTransaction struct {
    db *gorm.DB
}

NewConnectionClose も、gorm の場合、接続を隠蔽してくれているので、やることがありません。

func (r *dbRepository) NewConnection() (repository.Connection, error) {
    return &dbConnection{
        db: r.db,
    }, nil
}

func (con *dbConnection) Close() error {
    // We don't need to close *gorm.DB. No need to do anything.
    return nil
}

RunTransaction は、db.Begin() でトランザクションを作り、あとは関数を呼ぶだけです。関数内で error が返されれば Rollback し、そうでなければ Commit() すれば良いでしょう。

func (con *dbConnection) RunTransaction(f func(repository.Transaction) error) error {
    tx := con.db.Begin()

    err := f(&dbTransaction{db: tx})
    if err != nil {
        tx.Rollback()
        return err
    }

    err = tx.Commit().Error
    if err != nil {
        return err
    }

    return nil
}

UserQuery などを Connection から取るには次のようなメソッドを用意しておいて…

func (con *dbConnection) User() repository.UserQuery {
    return &dbUserRepository{db: con.db}
}
func (con *dbConnection) Task() repository.TaskQuery {
    return &dbTaskRepository{db: con.db}
}

func (tx *dbTransaction) User() repository.UserCommand {
    return &dbUserRepository{db: tx.db}
}
func (tx *dbTransaction) Task() repository.TaskCommand {
    return &dbTaskRepository{db: tx.db}
}

例えば次のように UserQuery 向けの Find を実装します。gorm は1つデータを検索して見つからなかった場合、RecordNotFound error を返すのですが、gorm のエラーを repository 層で見せたくないので、見つからない場合は単に nil, nil を返すようにしています。

type dbUserRepository struct {
    db *gorm.DB
}

func (r *dbUserRepository) Find(userID model.UserID) (*model.User, error) {
    db := r.db

    user := &model.User{
        ID: userID,
    }

    if err := db.First(user).Error; err != nil {
        if gorm.IsRecordNotFoundError(err) {
            return nil, nil
        }
        return nil, err
    }

    return user, nil
}

まとめ

repository 層の実装の一案を示しました。今のところ、この方式を使った場合に repository 層の実装に困ったことがありません。欠点としては、(1) join 時にどこにメソッド生やすかはちょっと困ることと、(2) repository があればどんなテーブルにもアクセスできるため、アクセスできるテーブルをちゃんと分けたければその分 repository を分ける必要がある、ことぐらいでしょうか。(2) は超大規模な開発では分けたくなるでしょうが、そこまで大きな開発であればマイクロサービス化したりと様々な対策が打たれていると信じているので、あまり問題にならないのではないかと思います。

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

GoでインストールからHello Golang!!

概要

MacにGoをインストールして、「Hello Golang!!」を表示させるところまでを解説していきます。
また、僕はシェルにfishを使っているので、fish向けの環境構築についても記載します。

環境

macOS Catalina 10.15.1
Homebrew 2.1.15
fish 3.0.2

Goインストール

$ brew install go
$ go version 
go version go1.13.3 darwin/amd64

Homebrew使うとコマンド一つでインストールできるので、とても便利です。

GOPATHの設定

GOPATHとは、Goのソースをまとめているディレクトリを指定している環境変数のことであり、
GOPATHを利用することで、外部ライブラリの導入やビルド作業を非常に簡単に行うことができます。

bashでGOPATHを指定するには、

$ vim ~/.bash_profile

で設定ファイルに以下追記をします。

export GOPATH=$HOME/go(←ワークスペースにしたい場所)
export PATH=$PATH:$GOPATH/bin

僕のようにシェルにfishを使っている人は、

vim ~/.config/fish/config.fish

で設定ファイルに以下追記をします。

set -x GOPATH $HOME/go
set -x PATH $PATH $GOPATH/bin

これでGOPATHの設定は完了です。

Hello Golang!!

ではいよいよ「Hello Golang!!」を表示させていきます。

ディレクトリ構成

最終的に以下構成になります。

go/
┣ bin/
   ┣ hellogolang 
┣ pkg/
┣ src/
   ┣ hellogolang/
              ┣ main.go 

以下ファイルのそれぞれの役割に関して記載します。
bin: 実行ファイルが格納されるディレクトリ
pkg: ビルドしたパッケージオブジェクトが格納されるディレクトリ
src: パッケージごとのソースが格納されるディレクトリ

main.goを作成する

go/src/hellogolang/main.goを作成したら、以下内容を記載する。

go/src/hellogolang/main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello Golang!!")
}

実行ファイルを作成する

go installで実行ファイルを作成することができます。

$ go install hellogolang

実行をすると、bin/hellogolangが作成されます。

Hello Golang!!を表示する

go/ディレクトリで以下コマンドを実行します。

$ bin/hellogolang
Hello Golang!!

Hello Golang!!が表示されました。

まとめ

Goは環境構築が簡単で勉強を始めやすくていいですね!!
あと、個人的にはfishはデフォルトでいい感じだし、コマンド履歴から自動で補完候補を表示させてくれたりと便利なので、ぜひ使ってみてください!!

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

Go x Clean Architecture の話

Ateam Hikkoshi Samurai Inc. & Ateam Connect Inc. Advent Calendar 2019 19日目は真のGopherになりたいルーキーGopher @lostfindがお送りします。

はじめに

println("みなさん、こんにちは!")

12日目の記事にも少しお話がありましたが, はい!そうです!

エイチーム引越し侍では言語はGo, 設計思想はClean Architecture, 開発方法はTDDでバックエンド側のリプレイスに取り組んでいます!

この記事ではGo x Clean Architectureでリプレイスしながら迎えた壁やまとまった考えについて, 少し共有ができたらなと思います!

まず, 結論から ?

Goとクリーンアーキテクチャの相性は?

個人的にはすごく良いと思います!
goのこのような特徴がクリーンアーキテクチャをするにはメリットだと思います。

  • ファイルやクラス単位ではなく, package単位での参照する
  • インタフェースで抽象化, 依存性逆転・注入ができる
  • 構造体にメソッドをつけてOOPのclassのようにも使えること
  • テストコードが書きやすい, テストがしやすい

Clean Architecture ?

最初にクリーンアーキテクチャの本を読んだときは, 「ふむふむ, なるほど・・」でしたが,
未だにQiita記事なども参考しながら, 本を何回か読み直すたびに, 「そういうことか!理解できていなかった!」と感じております。

この記事では各層の説明は割愛します!

概念についてはUncle Bobのブログ(英文), もしくは他の方のQiita記事をご参考ください!
書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」もおすすめです。

僕が考えるクリーンアーキテクチャの原則

現時点での自分なりに理解したクリーンアーキテクチャの原則をまとめると

  • クリーンアーキテクチャに正解はない
  • ビジネスルールに近いほど内側の層に置く
  • IO処理に近いほど, 外側の層に置く
  • 内側から外側への依存はしない = 内側は外側の挙動や存在を知らない, 知る必要がない
  • 修正の発生頻度で重要度や依存関係を判断してはいけない。

特に修正の発生頻度で依存性を判断しては行けないと思った理由は,
実際にビジネスルールに近いロジックの修正がDBやフレームワークの変更より頻繁に発生するからです。

メリット ?

  • テスト可能で, テストカバレッジを高く維持しやすい
  • 修正が発生する際, 他の部分に与える影響が低いことで, 安定な運用ができる
  • 新機能の追加時, 既に実装されている他の機能がベストプラクティスとして使える

この中で, 自分が思う最高のメリットは各層で「テスト可能」になることです!
テスト駆動開発にも良いと思います!

デメリット ?

最初, 理解して開発ができるまで勉強コストがかなりかかりました。
開発中の今でも二歩前進、一歩後退を繰り返しています。

コードの量もフレームワークを利用することに比べたら1.5倍以上増えます。

Goを書いて感じたこと ʕ◔ϖ◔ʔ

使わない変数とimportは許容しないので, ソースがきれいに維持できるのが好きです。
goは言語仕様がシンプルで勉強しやすかったと思います。

ただ, 実際に開発を進めていくと基本的に提供するメソッドが少なくて初期は不便でした。
特に, スライス関連の関数がほしかったですね。
一から作るか, 外部のものを取り入れる必要がありましたが, 実際に作っていく気がしていて楽しいです。

現れた壁 ⛰

Go x Clean Architectureで開発を進みながら大小の無数の壁に直面しました。
Goの壁とクリーンアーキテクチャの壁が交互に現れます。
場合によっては同時に現れるときもありましたね。

その中で, 幾つかをご紹介します!

Usecaseの肥大化

これはClean Architectureの壁です。

初期はEntity層にはDBテーブルとほぼ1:1に近いモデルだけ置き, ロジックはUsecase層に書きました。
その結果, 開発が進んでいくとアプリケーションロジックを配置する層として考えたUsecaseに, ビジネスロジックもたっぷりで, ほとんどのコードがUsecaseに入っている状況になっていました。

// 変更前の構成
domain(Entity層)
 model

usecase
 repository
 service
 presenter

既存のusecase/serviceに入っていたロジックを再検討して分離・移動しました。
アプリケーションロジックはusecase/interactorを作ってそこに置くことでusecaseの肥大化を解決しました。

// 変更後の構成
domain(Entity層)
 model
 repository
 service

usecase
 interactor
 presenter

依存性の注入(DI)で引数が増え続ける

内から外への依存性を逆転させるため, インタフェースを置き, 依存性の注入をしていて
外部のライブラリーが増えるほどconstructorの引数が増える構成をしていました。

これは, 暗号化ライブラリーEncrypterを使うpresenterの例です。
変更前はpresenter構造体のフィールドとしてEncrypterを持ちます。
ここでは引数が一つですが, 使われる外部ライブラリーが増えると引数も増やしていました。

interface/presenters/hoge_presener.go
type hogePresenter struct {
    Encrypter encrypt.Encrypter
}

// NewHogePresenter HogePresenterを生成します。
func NewHogePresenter(encrypter encrypt.Encrypter) presenter.HogePresenter {
    return &hogePresenter{
        Encrypter: encrypter,
    }
}

func (p *hogePresenter) Fuga(model []*model.HogeModel) []*presenter.ResponseHoge {
    ...
    p.Encrypter.IntToRandStr(int(r.ID)),
    ...
}

コードも長くなりますし, わかりにくくなっていましたので
このように外部のライブラリーのインタフェース変数をグローバル変数で使うように修正しました。

domain/library/encrypter.go
package library

var Encrypt Encrypter // このようにグローバル変数を使います。

// Encrypter 暗号化に関する関数を提供します。
type Encrypter interface {
    IntToRandStr(a int) string
    ...
}

// SetEncrypter Encrypterを生成します。
// 元々DIを行われている場所でグローバル変数へDIします。
func SetEncrypter(encrypt Encrypter) {
    Encrypt = encrypt
}
interface/presenters/hoge_presener.go
func (p *hogePresenter) Fuga(model []*model.HogeModel) []*presenter.ResponseHoge {
    ...
    library.Encrypt.IntToRandStr(int(r.ID)), // libraryパッケージのグローバル変数からメソッドを呼び出します。
    ...
}

グローバル変数使って大丈夫かなって感覚的は不安も少し感じましたが,
外側に依存しない規則は守られて, 置く場所だけ変わるので大丈夫かと思いましてこう決定しました。

エラーハンドリング設計

これはGoとクリーンアーキテクチャの両方の壁です。
Goの壁は例外処理がないので, 想定できるエラーに対応しておく必要があります。

Clean Architectureの壁は層を渡ってメソッドを呼び出しているので,
エラーが発生するとどの層までバケツリレーで渡して処理するべきなのかで悩みました。

考えた結果, 今は各エラーは各層で処理し, エラーログはグローバル変数でloggerを持たし, それを利用する形をとっています。

今後, 変わる可能性はすごくすごーくあります。

終わりに

記事を書いてから見るとGoについては少なく, ほぼクリーンアーキテクチャの話になりました。
クリーンアーキテクチャを取り入れなくても概念は勉強してみると
当たり前なことを言ってるようで, またそこで新しく気づくことも多いと思います。

みなさんも是非GoClean Architectureの世界へ入ってみるのはいかがでしょうか?

お知らせ ?

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!

Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能です!

明日 ?

明日は, 引越し侍にテストコードを書く文化を率先して取り組んでいる @ysysysys さんの記事です!
おーたのしみにー!?

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

MySQLのToo many connectionsの対処法 ~バルクインサートを添えて~

はじめに

CA Tech Dojo/Challenge/JOB Advent Calendar 2019の19日目はhmarfが書かせていただきます。私のアドベントカレンダーの担当日の前後に優秀swiftエンジニアの @ostk0069さんと@misakiagataさんがいるのでプレッシャーがすごいです。
この記事は@kenjiszkさんのMySQLでToo many connectionsが出た時の対応についての私なりの補足です。

MySQLで too many connectionsがでたら確認すべきこと

  • @kenjiszkさんの記事にもありますが、too many connectionがでたら、MySQLのmax connections, processlist, wait_timeout を確認します。
# max_connections の数を確認
mysql> show variables like "%max_connections%";
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 150   |
+-----------------+-------+
1 row in set (0.00 sec)

# 接続されているプロセスを確認
mysql> show processlist;
+----+------+------------------+------+---------+------+----------+------------------+
| Id | User | Host             | db   | Command | Time | State    | Info             |
+----+------+------------------+------+---------+------+----------+------------------+
| 37 | root | localhost        | NULL | Sleep   | 6090 |          | NULL             |
| 38 | user | xxx.xx.x.x       | NULL | Sleep   |  347 |          | NULL             |
| 39 | user | xxx.xx.x.x       | NULL | Sleep   |  347 |          | NULL                      |
| 55 | root | localhost        | NULL | Query   |    0 | starting | show processlist |
+----+------+------------------+------+---------+------+----------+------------------+
4 rows in set (0.01 sec)

# time が長いものを削除
mysql> kill 37;
Query OK, 0 rows affected (0.00 sec)

# wait timeout を確認
mysql> show global variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.01 sec)

考えられる対処方法

  • max connectionを上げる & wait timeout を制限する
  • クライアント側で張るconnectionsの数を制限する

max connectionを上げる & wait timeout を制限する

とりあえず、max connection, wait timeout を変更しています。しかし、安易な気持ちでConnectionの数を上げると、メモリ不足を引き起こします。

以下の式でメモリ計算ができます。

メモリ使用量 = グローバルバッファ + (スレッドバッファ * コネクション数)
  • 起動中のサーバー内で変更する場合
mysql> set global max_connections = 1000;
mysql> set global wait_timeout = 1800;
  • 設定ファイルで変更する場合
[mysqld]
max_connections = 1000
wait_timeout = 1800

[余談] 僕の記憶が正しければ、amazon RDS では max connection を上げることができなかったような気がします。AWSでは使用しているインスタンスのメモリから最適な max connection を自動で計算していたはず。一時的に上げることはできるが、どんどん減っていくはずです。( max connectionを変更できたら大嘘 )

clientで張るconnectionの数を制限する

Go言語の場合(一行でconnectionの数を制限できます)

DB.SetMaxOpenConns(100)

たったこれだけです。しかし、とても大切なので忘れないようにしましょう。

またDBにInsertする際にリアルタイム性が必要ないのであれば、「バルクインサート」など様々な方法があると思います。
とりあえず、今回はバルクインサートのサンプルを残したいと思います。

main.go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/carlescere/scheduler"
    _ "github.com/go-sql-driver/mysql"
)

type insertData struct {
    name      string
    createdAT time.Time
}

func initDB() *sql.DB {
    db, err := sql.Open("mysql", "user:password@tcp(0.0.0.0)/sampleDB?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    return db
}

func insertDB(ch *chan insertData, db *sql.DB) {

    rescInterface := []interface{}{}
    stmt := "INSERT INTO user(name, createdAt) VALUES"
    insertFlag := false
LOOP:
    for {
        select {
        case data, ok := <-*ch:
            if ok {
                insertFlag = true
                stmt += "(?,?),"
                rescInterface = append(rescInterface, data.name)
                rescInterface = append(rescInterface, data.createdAT)
            }
        default:
            break LOOP
        }
    }

    if insertFlag {
        stmt = strings.TrimRight(stmt, ",")

        _, err := db.Exec(stmt, rescInterface...)
        if err != nil {
            fmt.Println(err)
        }
    }
    return
}

func main() {

    DB := initDB()
    defer DB.Close()

    // connection数 の制限
    DB.SetMaxOpenConns(9)

    channel := make(chan insertData, 100000)

    _, _ = scheduler.Every(5).Seconds().NotImmediately().Run(func() { insertDB(&channel, DB) })

    rootHandler := func(w http.ResponseWriter, r *http.Request) {
        channel <- insertData{
            name:      "test user",
            createdAT: time.Now(),
        }
        w.WriteHeader(200)
    }

    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })

    // start server
    if err := http.ListenAndServe(":8081", nil); err != nil {
        log.Fatal(err)
    }

}

まとめ

コネクションは意識しながら開発していきましょう!
明日の@misakiagataさんの記事が楽しみです!

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

MySQLのToo many connectionsの対処法の補足 ~バルクインサートを添えて~

はじめに

CA Tech Dojo/Challenge/JOB Advent Calendar 2019の19日目はhmarfが書かせていただきます。私のアドベントカレンダーの担当日の前後に優秀swiftエンジニアの @ostk0069さんと@misakiagataさんがいるのでプレッシャーがすごいです。
この記事は@kenjiszkさんのMySQLでToo many connectionsが出た時の対応についての私なりの補足です。

MySQLで too many connectionsがでたら確認すべきこと

  • @kenjiszkさんの記事にもありますが、too many connectionがでたら、MySQLのmax connections, processlist, wait_timeout を確認します。
# max_connections の数を確認
mysql> show variables like "%max_connections%";
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 150   |
+-----------------+-------+
1 row in set (0.00 sec)

# 接続されているプロセスを確認
mysql> show processlist;
+----+------+------------------+------+---------+------+----------+------------------+
| Id | User | Host             | db   | Command | Time | State    | Info             |
+----+------+------------------+------+---------+------+----------+------------------+
| 37 | root | localhost        | NULL | Sleep   | 6090 |          | NULL             |
| 38 | user | xxx.xx.x.x       | NULL | Sleep   |  347 |          | NULL             |
| 39 | user | xxx.xx.x.x       | NULL | Sleep   |  347 |          | NULL                      |
| 55 | root | localhost        | NULL | Query   |    0 | starting | show processlist |
+----+------+------------------+------+---------+------+----------+------------------+
4 rows in set (0.01 sec)

# time が長いものを削除
mysql> kill 37;
Query OK, 0 rows affected (0.00 sec)

# wait timeout を確認
mysql> show global variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.01 sec)

考えられる対処方法

  • max connectionを上げる & wait timeout を制限する
  • クライアント側で張るconnectionsの数を制限する

max connectionを上げる & wait timeout を制限する

とりあえず、max connection, wait timeout を変更しています。しかし、安易な気持ちでConnectionの数を上げると、メモリ不足を引き起こします。

以下の式でメモリ計算ができます。

メモリ使用量 = グローバルバッファ + (スレッドバッファ * コネクション数)
  • 起動中のサーバー内で変更する場合
mysql> set global max_connections = 1000;
mysql> set global wait_timeout = 1800;
  • 設定ファイルで変更する場合
[mysqld]
max_connections = 1000
wait_timeout = 1800

[余談] 僕の記憶が正しければ、amazon RDS では max connection を上げることができなかったような気がします。AWSでは使用しているインスタンスのメモリから最適な max connection を自動で計算していたはず。一時的に上げることはできるが、どんどん減っていくはずです。( max connectionを変更できたら大嘘 )

clientで張るconnectionの数を制限する

Go言語の場合(一行でconnectionの数を制限できます)

DB.SetMaxOpenConns(100)

たったこれだけです。しかし、とても大切なので忘れないようにしましょう。

またDBにInsertする際にリアルタイム性が必要ないのであれば、「バルクインサート」など様々な方法があると思います。
とりあえず、今回はバルクインサートのサンプルを残したいと思います。

main.go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/carlescere/scheduler"
    _ "github.com/go-sql-driver/mysql"
)

type insertData struct {
    name      string
    createdAT time.Time
}

func initDB() *sql.DB {
    db, err := sql.Open("mysql", "user:password@tcp(0.0.0.0)/sampleDB?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    return db
}

func insertDB(ch *chan insertData, db *sql.DB) {

    rescInterface := []interface{}{}
    stmt := "INSERT INTO user(name, createdAt) VALUES"
    insertFlag := false
LOOP:
    for {
        select {
        case data, ok := <-*ch:
            if ok {
                insertFlag = true
                stmt += "(?,?),"
                rescInterface = append(rescInterface, data.name)
                rescInterface = append(rescInterface, data.createdAT)
            }
        default:
            break LOOP
        }
    }

    if insertFlag {
        stmt = strings.TrimRight(stmt, ",")

        _, err := db.Exec(stmt, rescInterface...)
        if err != nil {
            fmt.Println(err)
        }
    }
    return
}

func main() {

    DB := initDB()
    defer DB.Close()

    // connection数 の制限
    DB.SetMaxOpenConns(9)

    channel := make(chan insertData, 100000)

    _, _ = scheduler.Every(5).Seconds().NotImmediately().Run(func() { insertDB(&channel, DB) })

    rootHandler := func(w http.ResponseWriter, r *http.Request) {
        channel <- insertData{
            name:      "test user",
            createdAT: time.Now(),
        }
        w.WriteHeader(200)
    }

    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })

    // start server
    if err := http.ListenAndServe(":8081", nil); err != nil {
        log.Fatal(err)
    }

}

まとめ

コネクションは意識しながら開発していきましょう!
明日の@misakiagataさんの記事が楽しみです!

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

これで俺モ ゲンシジン

はじめに

こんにちはRIN1208です。この記事はITRCアドベントカレンダーの19日目の記事になります。

経緯

まず初めにこの記事を書いた経緯の説明をしたいと思います。
この記事で作った物こちらの記事、オレ プログラム ウゴカス オマエ ゲンシジン ナルを読み自分でも作ってみたくなったので作成した物です。
内容は似ていますが作成した言語やパッケージは異なります。
こちらの記事は個人的にとても好きなので是非見てみて下さい。

開発環境

  • Mac
  • goの実行環境
  • MeCabの環境

MeCabのインストール(Mac)

$ brew install mecab
$ brew install mecab-ipadic
$ brew install git curl xz
$ git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ ./bin/install-mecab-ipadic-neologd -n

上記のコマンドで一通りインストールできます。

また今回neologd辞書もインストールしています。詳しくはこちらを参照して下さい。

GoでMeCabを扱う

今回使用したGoのMeCabのラッパーはmecab-golangです。
先ほどMeCabをインストールしたのでこちらのコマンドを実行すれば準備完了です。

$ export CGO_LDFLAGS="`mecab-config --libs`"
$ export CGO_CFLAGS="-I`mecab-config --inc-dir`"
$ go get github.com/bluele/mecab-golang

これで準備完了です。

main.goと言うファイルを作成して下さい。

原始人になれるコードはこちら

package main

import (
    "fmt"
    "strings"

    "github.com/bluele/mecab-golang"
)

func parseToNode(m *mecab.MeCab, word string) {
    var key string
    tg, err := m.NewTagger()
    if err != nil {
        panic(err)
    }
    defer tg.Destroy()
    lt, err := m.NewLattice(word)
    if err != nil {
        panic(err)
    }
    defer lt.Destroy()

    node := tg.ParseToNode(lt)

    for {
        features := strings.Split(node.Feature(), ",")
        if features[0] != "助詞" && features[0] != "助動詞" {

            key = key + " " + features[7]
        }
        if node.Next() != nil {
            break
        }
    }
    fmt.Println(key)
}

func main() {
    var word string
    m, err := mecab.New("-Owakati")
    if err != nil {
        panic(err)
    }
    defer m.Destroy()
    fmt.Print("input keyword :")
    fmt.Scan(&word)
    parseToNode(m, word)
}

正直サンプルコードを少しいじっただけです。  
それでは実行結果をみてみましょう。

$ go build main.go 
$ go run main.go
input keyword :これでみんな原始人になれる

 * コレ ミンナ ゲンシ ジン ナレル *

このような感じです。

ですがこちらのコードではカタカナで入力された言葉があるとエラーが出てしまいます。(改善予定)

他には限界ヲタクするとき等にも使用可能です。

$ go run main.go
input keyword :水着まこと可愛すぎてやばい
 * ミズギ マコト カワイ スギ ヤバイ *
$ go run main.go
input keyword :この子かわいすぎないか。ほんと好みすぎてやばい
 * コノ コ カワイ スギ 。 ホント コノミ スギ ヤバイ *  

こんな感じで限界ヲタクするとき等にも使えます。

おわりに

自分は初めcotoha apiを使用してやってみたかったのですがいろいろと面倒だったのでMeCabを使ったらかなり簡単にできました。形態素解析自体このコードを書くまでが初めてですが面白いですね(コード書いたの半年以上前)。またここまで読んでくれた方ありがとうございました!

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

GoのTest学習

箇条書きで健忘録としてメモします。
・Goでテストを書くときはtestingパッケージを使う。
・テストはxxx_test.goというファイル名で作成する。
・テストメソッドはTestXxxという命名規則にする。
・テストはgo testコマンドで実行する。

// sum_test.go
package main

import (
    "testing"
)

func Sum(a, b int) int {
    return a + b
}

func TestSum(t *testing.T) {
    if Sum(1, 1) != 2 {
        t.Error("miss")
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む