- 投稿日:2019-12-19T23:32:43+09:00
Goでmarkdown内のhclソースコードをfmtするCLIツール「tffmtmd」を作る過程で学んだことをまとめていく
はじめに
こちらはGo4 Advent Calendar 2019の19日目として書いております。
ギリギリ間に合った!!!何を作ったか
gofmtmd というcliツールにinspireされて、tffmtmdというmarkdown内のhclソースコードをfmtするツールを作りました。
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.gofunc (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.gofunc (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ならデフォルトでついている)
また、ユーザーにとってTest Coverageが一目で見えることで、ツールとして安心して選びやすくなると思います。codecov.ioなんかを使用して、カバレッジをバッジ画像でREADMEにつけておくのが良さそうです。(これもgofmtmdからの受け売り。その他にGolangCIやCircleCIやgodocのバッジをつけておくととても可愛いし、ユーザーが安心して選びやすいと思う。)
設定方法の詳細はこちらを参考にしてください。 -> CircleCIとCodecovでGitHubにカバレッジバッジをつけよう! - VELTRA Engineering - Medium2. goreleaserで各プラットフォームへのバイナリを自動リリースする
goreleaserは、バイナリのクロスコンパイルと Github Releases へのデプロイ自動化ツールです。
直近のtagに対して、goreleaserコマンドを打つだけで.goreleaser.ymlファイルで指定したOSや環境に対応したバイナリをリリースしてくれます。(githubなら、tagではなく最新のコミットに紐付ける--snapshotふらぐもある)
なんて便利なんだ。。。。
詳しい設定方法は公式チュートリアルをご参照ください!最後に
ほぼほぼgofmtmdの受け売りだし簡易なツール作成でしたが、ほぼ初めてのパブリックな自家製ツールだったので、非常に勉強になることが多かったです。
あとはお仕事ではBitbucketを使っていることが多い(たまにGitlab)ので、久々にGithub使うと周辺ツールも相まって快適すぎてやばい。。今回の記事では、本当はterraformソースからUMLをジェネるツールか、terraform-qiita-providerをGoで書く話を書くつもりでしたが、諸所ゴタゴタしている関係で今日までに完成させることができませんでした。。
それらのツールが完成したら、また諸々まとめてアウトプットしようと思っています。その際にも今回学べたことが大きく活きる気がしています。
- 投稿日:2019-12-19T21:16:27+09:00
GoでUFOを引っ捕らえる
いいっすよね。UFO。
あの愛らしいフォルムとか、中から何でてくるかわかんないミステリアス感とか…最高やん?でも、皆さん実際に見たことあります?
もしかしたら………ないんじゃないですか?「言われてみれば一度も見たことがない…?」
「小さい頃に見たあれは僕らの妄想なのか…?」
「UFOなんて本当はいないんだ…!!!」と、不安な夜をお過ごしの方も多いと思います。
でも安心してください。
いるんです。
そう。アメリカならね。
※これは FOSS4G Advent Calendar 2019 の19日目の記事です。
todo
- UFOの目撃情報を追う
- データを取得する
- csvに吐き出す
- geocodingする(位置情報を取得する)
- csvを更新
- GeoJSONに吐き出す
- ブラウザの地図上に表示する
UFOの目撃情報を追う
アメリカには国立UFO情報センター(NUFORC)という1974年以来継続的に運用されている、アメリカ全土のUFO目撃情報が集約されたステキなサイトがあります。
すげえなアメリカ。
今回はこちらのデータセットを利用させてもらいましょう。
データはこちらに月ごとにまとめられており、最新のものは12月に2件ほど登録されていますが、今回は月ごとのデータが出揃った11月のものを拝借しましょう。
11/2019をクリックすると以下の様にデータが表形式で格納されていることがわかります。スクレイピングがイージーそうですね。
ちなみに一番古いデータは
西暦209年6月です。すげえな国立UFO情報センター。データを取得する
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.gomain.gopackage 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と入力するとこんな感じで緯度経度が取得できます。※こちらから試せます。
この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.tsimport 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/に接続してみましょう!UFOいっぱいおる!!!!!!!!!
結論
Goは楽しい
Vue.jsは楽ちん
UFOはかわいい
- 投稿日:2019-12-19T19:53:53+09:00
なぜDeNA 20 新卒 Advent Calendar 2019を企画したのか【+専用アプリの解説】
はじめに
ついに最終日?
「なぜ内定者のカレンダーを企画したのか?」の理由と感想を書いていきます。
合わせて専用アプリの実装の解説も付けました?また、この記事は@tocknとの合作記事です。
目次
章番号 タイトル 1章 内定者カレンダーを企画した理由 2章 全体の感想 3章 専用アプリ作成の発端 4章 専用アプリ作成のバックエンド実装 5章 専用アプリ作成のフロント実装 終章 さいごに 内定者カレンダーを企画した理由
「内定者で何かをしたい!」という思いと、「採用に貢献したい!」という思いが合わさった結果です。
この企画は来年で完成するもので、
- ??内定者のカレンダー
- ??新卒のカレンダー
- ??社員のカレンダー
の3つを作成することで
- ??内定者のカレンダー: どういう人が入社するのか
- ??新卒のカレンダー: 1年後の成長度、成長環境
- ??社員のカレンダー: 2年以降のキャリア
を一部ですが表現でき、DeNAという会社のことをより知ってもらえる機会になると考えました。
学生にとっては、どういう人が入社し、1年後の成長、その後のキャリアが見えるのは非常に有益な情報になると考えてます。
採用イベントで知ることが可能ですが記事という、より自然体で個人の思想がでる媒体での情報から感じ取れるものがあると私は思ってます。(就活中は各社の技術ブログはチェックしてました)以上のような思いから企画をしました。
また、今回は「内定者vs社員!?」という企画をしたように、普通のカレンダーとは違う楽しみ方を提供できると考えています。
全体の感想
企画して本当によかったです。
内定者同士で交流が生まれたのは勿論ですが、「この子(サービス)知ってる!」となる機会も多く、同期をより知るきっかけになりました。
記事の中で研究内容や興味ある分野などを知ることもできて交流が増えました。また、個人的にはイベントを企画する際のフローを今回で知れたのも良かったです。
申請などやり取りする中で、「弊社は頑張っている人を応援するとき、他の人を蹴落とすこと以外にNGを出すことはない」 と言っていただきましたが、本当にその通りだと感じました。
本当にいいカレンダーになったと思います。
申請など協力していただいた社員の方々、執筆者のみんな、読者の皆さん、本当に本当にありがとうございました。専用アプリ作成の発端
vsの企画をしていた時にみんなに意見を募ったのが発端でした。
そして何気なく送信したメッセージが始まりでした...。
...3時間後にtocknがAPIを作りました...!?
APIができたことでハッカソンが突如始まりましたw
(だって、APIあったら作るしかないじゃん)専用アプリ作成のバックエンド実装
バックエンド実装を担当した@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.gotype 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.gotype Likes interface { GetNew() (*model.Likes, error) Create(likes *model.Likes) error }qiita
QiitaのスクレイピングおよびAPIを叩いて値を取得する層です。以下は年、カレンダー名を引数に、そのカレンダーの総イイね数を返す関数です。
qiita/qiita.gofunc 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.gotype 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.gotype 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.gofunc 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.gotype 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.gofunc 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.yamlcron: - description: "update likes" url: /updatelikes schedule: every 10 minutes target: default timezone: Asia/Tokyoこんな感じで定義すれば、10分おきにクロールしてくれます。
以上、バックエンド実装のお話でした。
専用アプリ作成のフロント実装
フロントエンド実装を担当した@naro143です。
「毎日記事を見るきっかけになる」ということでアプリであることは必須だったのですが、いい機会なのでPWAを初めて触るNuxtで実装することにしました。
結構汚かったり無駄な実装があると思います。
是非アドバイスください本当に基礎的なことしかしてないので、基礎的でないところに絞って解説をします。
今回実装したコードはGitHubで公開しています。
アプリ紹介
サイトはこちら↓
https://vs-dena-advent-client.naro143.com/nuxtの設定
コードを見る
nuxt.config.jsexport 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/カレンダーまとめ
- 投稿日:2019-12-19T18:18:31+09:00
眺めて覚えるGo言語
眺めながら学習する超簡単言語!
いきなりGOLANG
ex1.gopackage main import ( "fmt" ) func main() { fmt.Println("hello world") } C:\Users\hirat\go-work\web\hello>go run ex1.go hello worldお決まりのコンソール出力
ex2.gopackage 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.gopackage 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=200Printfを使って出力
ex4.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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 秋葉原とりあえずこれだけ知っていたら書けます。
- 投稿日:2019-12-19T17:09:55+09:00
なぜ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を使用して行っています。
適応アルゴリズムとは簡単にいうと、過去の入力信号から次の信号を予測するアルゴリズムです。
稚拙なプログラムですが、Githubでライブラリを公開しています。
今は基本のアルゴリズムしかありませんが、順次発展的なアルゴリズムを増やす予定です。また、Goの並行処理を活かしたプログラムも書きたいと考えています。興味のある方は見て頂けると幸いです。
github.com/tetsuzawa/go-adflib
まとめ
Goのコードが全く無く恐縮ですが、ここまで読んでいただきありがとうございました。
間違い・意見等あれば指摘していただけると幸いです。教員・学生のみなさん、Goを書きましょう。
- 投稿日:2019-12-19T17:03:29+09:00
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標準パッケージだからこれ以上速くならないと思っておられるかもしれません。しかし使い方を変えるだけでもっと速くできる箇所が見つかったりするかもしれませんね。
- 投稿日:2019-12-19T14:35:31+09:00
HackerNewsをCUIで見たい!! (ライブラリ探しと実装)
最終的に作るもの
HackerNewsのTUIコマンドである
hnをGoで作成します。
背景
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とも異なり一般的なテキスト端末で表示できる記号や文字だけで画面を構成します。こんな感じの見た目になります。↓↓
termuiのREADMEから引用私自身ライブラリを探す時はawesome-goから見に行き、その後にGithub内を調査しています。今回探したのもawesome-goから見つけました。
以下は自分がHackerNews TUIを作成する際に見つけた候補ライブラリ集です。
それぞれのREADMEにはGIF画像が挿入されているのでよりわかりやすいです。ぜひ見てみてくださいtui-go
GitHub - marcusolsson/tui-go: A UI library for terminal applications.
term-ui
GitHub - gizak/termui: Golang terminal dashboard
termbox-go
GitHub - nsf/termbox-go: Pure Go termbox implementation
tview
GitHub - rivo/tview: Rich interactive widgets for terminal-based UIs written in Go
termdash
GitHub - mum4k/termdash: Terminal based dashboard.
HackerNews CUIの実装
今回は、demo,exampleディレクトリなどの実装を見てしっくりきたtviewを利用しました。
HackerNews APIから記事一覧を取得
HackerNews CUIの実装を見ると、goqueryなどでスクレイピングをしている実装も多く見られましたが、今回はFirebaseで用意されているHackerNes APIを使用します。
hn_api.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gofunc 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.gofunc 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
- 投稿日:2019-12-19T14:12:09+09:00
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テーブルへのアクセスは、UserRepositroyやTaskRepositoryなどのインタフェースを作ってそれを実装するようにすることが多いように思います。その場合、DB へのコネクションをどう表現するかが問題になりがちです。UserRepositoryの中でコネクションを取る様にしてしまうと、トランザクションで複数テーブルを同時に更新する場合に、UserRepositoryがusers以外の テーブルを変更する必要が生じるなど、うまく表現できません。そこで、コネクションは、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) は、rとconが一致しないようなコード (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/Commandinterface を作ることもあります。更新は必ず
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 }
NewConnectionもCloseも、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つデータを検索して見つからなかった場合、RecordNotFounderror を返すのですが、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) は超大規模な開発では分けたくなるでしょうが、そこまで大きな開発であればマイクロサービス化したりと様々な対策が打たれていると信じているので、あまり問題にならないのではないかと思います。
- 投稿日:2019-12-19T08:08:32+09:00
GoでインストールからHello Golang!!
概要
MacにGoをインストールして、「Hello Golang!!」を表示させるところまでを解説していきます。
また、僕はシェルにfishを使っているので、fish向けの環境構築についても記載します。環境
macOS Catalina 10.15.1
Homebrew 2.1.15
fish 3.0.2Goインストール
$ brew install go$ go version go version go1.13.3 darwin/amd64Homebrew使うとコマンド一つでインストールできるので、とても便利です。
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.gopackage 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はデフォルトでいい感じだし、コマンド履歴から自動で補完候補を表示させてくれたりと便利なので、ぜひ使ってみてください!!
- 投稿日:2019-12-19T00:29:48+09:00
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層) L model usecase L repository L service L presenter既存の
usecase/serviceに入っていたロジックを再検討して分離・移動しました。
アプリケーションロジックはusecase/interactorを作ってそこに置くことでusecaseの肥大化を解決しました。// 変更後の構成 domain(Entity層) L model L repository L service usecase L interactor L presenter依存性の注入(DI)で引数が増え続ける
内から外への依存性を逆転させるため, インタフェースを置き, 依存性の注入をしていて
外部のライブラリーが増えるほどconstructorの引数が増える構成をしていました。これは, 暗号化ライブラリー
Encrypterを使うpresenterの例です。
変更前はpresenter構造体のフィールドとしてEncrypterを持ちます。
ここでは引数が一つですが, 使われる外部ライブラリーが増えると引数も増やしていました。interface/presenters/hoge_presener.gotype 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.gopackage 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.gofunc (p *hogePresenter) Fuga(model []*model.HogeModel) []*presenter.ResponseHoge { ... library.Encrypt.IntToRandStr(int(r.ID)), // libraryパッケージのグローバル変数からメソッドを呼び出します。 ... }グローバル変数使って大丈夫かなって感覚的は不安も少し感じましたが,
外側に依存しない規則は守られて, 置く場所だけ変わるので大丈夫かと思いましてこう決定しました。エラーハンドリング設計
これはGoとクリーンアーキテクチャの両方の壁です。
Goの壁は例外処理がないので, 想定できるエラーに対応しておく必要があります。
Clean Architectureの壁は層を渡ってメソッドを呼び出しているので,
エラーが発生するとどの層までバケツリレーで渡して処理するべきなのかで悩みました。考えた結果, 今は各エラーは各層で処理し, エラーログはグローバル変数で
loggerを持たし, それを利用する形をとっています。今後, 変わる可能性はすごくすごーくあります。
終わりに
記事を書いてから見るとGoについては少なく, ほぼクリーンアーキテクチャの話になりました。
クリーンアーキテクチャを取り入れなくても概念は勉強してみると
当たり前なことを言ってるようで, またそこで新しく気づくことも多いと思います。みなさんも是非
GoとClean Architectureの世界へ入ってみるのはいかがでしょうか?お知らせ ?
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム、社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能です!
明日 ?
明日は, 引越し侍にテストコードを書く文化を率先して取り組んでいる @ysysysys さんの記事です!
おーたのしみにー!?
- 投稿日:2019-12-19T00:09:40+09:00
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.gopackage 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さんの記事が楽しみです!
- 投稿日:2019-12-19T00:09:40+09:00
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.gopackage 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さんの記事が楽しみです!
- 投稿日:2019-12-19T00:08:21+09:00
これで俺モ ゲンシジン
はじめに
こんにちは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を使ったらかなり簡単にできました。形態素解析自体このコードを書くまでが初めてですが面白いですね(コード書いたの半年以上前)。またここまで読んでくれた方ありがとうございました!
- 投稿日:2019-12-19T00:02:03+09:00
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") } }



























