- 投稿日:2020-03-22T19:32:46+09:00
勉強会情報をpush通知するslackアプリを作った
はじめに
勉強会の情報を定期的にpush通知してくれるslackアプリを作った。
こんな感じでpush通知したい勉強会の条件を設定して、
条件に合致した勉強会が、設定した時刻に定期的に通知される。
モチベーション
大きめの勉強会や人気の勉強会の開催を、qiitaのトレンド記事やはてブの記事に挙げられてるのを見て、後から知ることが結構ある。そういう人気の勉強会をpush通知してくれるような仕組みがあれば「やってたの知らなかった」ってことを減らせそうだと思ったので作ってみた。
アーキテクチャは以下の理由からherokuを使うことにした。
- サーバー代が無料
- dockerでのデプロイに対応してる
- スケジューラの仕組みもある需要があるかわからないが、他の人もこの仕組みを使えるように環境構築の手順を書いていく。
構築手順
0) 事前にやっておくこと
- コードをローカルへクローンしておく。
- slackアプリを作成し、ワークスペースへ追加しておく。 やり方はこの記事とかが参考になる
1) herokuへのリリース
事前にherokuのアカウントがない人は作っておく。
# 事前にアプリケーションのルートディレクトリに移動しておく $ heroku container:login # ログイン $ heroku create -a app_name # herokuアプリの作成。アプリ名は適当に決める。 $ heroku git:remote -a app_name # herokuリポジトリをgit登録。 $ heroku addons:create scheduler:standard # スケジューラのアドオンを追加 $ heroku addons:add cleardb:ignite # mysqlのアドオンを追加 $ heroku config # CLEARDB_DATABASE_URLが登録されていることを確認 $ heroku config:set DATABASE_URL="<ユーザー名>:<password>@tcp(<ホスト名>:3306)/<DB名>?parseTime=true" # CLEARDB_DATABASE_URLの値を元にsql.Open()に渡す用の文字列に整形 $ heroku config:set SLACK_TOKEN="<slackのトークン>" # slackアプリのトークンを環境変数に設定 $ heroku config:set WEB_HOOK_URL="<slackのwebHookURL>" # slackのwebHookURLを環境変数に設定 $ heroku stack:set container # heroku.ymlを使う時はこれがいるぽい $ git push heroku master # リリース2) slackの設定
作成しておいたslackアプリの設定画面に移動しておく。
2-1) slashコマンドの設定
通知する勉強会の条件設定は、slashコマンド経由で開くダイアログから行うようにしている。
そのslashコマンドの設定をしていく。2-2) Interactive Componentsの設定
勉強会の条件設定をするダイアログをsubmitした際に叩くAPI先の設定。
3) スケジューリングの設定
herokuの画面に行って、スケジューラーのアドオンをクリック
/main connpass slack
を実行するジョブを作成する。実行間隔はお好みで。
4) 勉強会通知の設定をする
作成したslashコマンドを実行し、開いた設定ダイアログ上で通知条件の設定をする。
※注意
herokuの無料プランだと、30分たつと自動でプロセスが落とされる。
なので初回のslashコマンドはサーバー起動の時間が掛かるのでタイムアウトで失敗する。その後あまり間を空けずに再度slashコマンドを実行すればダイアログが開く。5) 設定完了
ここまでやれば、herokuの環境変数で設定したwebHookURL宛てに、勉強会情報が定期的にポストされるようになる。
アーキテクチャ
golang、gin、docker、mysql、slack api、connpass api、herokuな感じで作っている。
一つのイメージでapiサーバーとしてもコマンド実行サーバーとしても起動させたかったため、起動時にフラグを渡すことでapiサーバー、コマンド実行サーバーが切り替わるようにしている。
また、ホットリロードのツールに air を使っている。
goでホットリロードといえば realize が使われているのをよく見かけるが、realizeはmodulesに対応していなかったり、最近メンテもされなくなったりと微妙な感じ。いいのが他にないのかなと思っていたが、最近はairが流行ってきてる?ぽい。
- 投稿日:2020-03-22T18:55:47+09:00
ループ中にある無名関数のgoroutineではキャプチャに気をつける
ループ中のgoroutineでは束縛に気をつける
ほかのプログラミング言語の非同期処理でもよく起こしてしまう問題が、レキシカルスコープとダイナミックスコープの理解不足によるキャプチャのミスだ。
今回は、 goroutine でやりがちなコードでキャプチャのミスを
go vet
で機械的に見つける方法を紹介する例題
goroutine を使って1,2,3とプリントする関数を3並列で処理したいとする[^1]。以下のコードを書いてみた。どういう動作をするだろうか?
[^1] 今回はプリントする関数としたが、実用であれば重い処理を実行する関数になるはず。
package main import ( "fmt" "sync" ) func main() { integers := make([]int, 3) for i := range integers { integers[i] = i } wg := &sync.WaitGroup{} for _, v := range integers { wg.Add(1) go func() { defer wg.Done() fmt.Println(v) }() } wg.Wait() }これを実行すると、2が3回プリントされる。
$ go run main.go 2 2 2回避
fmt.Println
の引数として与えられているv
がループ変数のv
にキャプチャされているから起こる。これを回避するには以下のように書き換える。無名関数の部分に引数を渡す形にすればいい。wg := &sync.WaitGroup{} for _, v := range integers { wg.Add(1) - go func() { + go func(v int) { defer wg.Done() fmt.Println(v) - }() + }(v) } wg.Wait() }こうすることで期待した動作になる。
$ go run ./main.go 2 1 0機械的に見つけ出す
go vet
を使う。$ go vet # _/your/path/ ./main.go:19:16: loop variable v captured by func literal「ループ変数にキャプチャされているよ」と
go vet
が教えてくれた。ループ変数にキャプチャされていると、無名関数が実行されたとき(goroutineがスケジュールされたとき)にループ変数の値を読みに行ってしまう。goroutineがいつスケジュールされるか未定であるから、出力される数字も未定になってしまう。ということで、ループ変数をキャプチャしないように引数を渡してやれば解消する。
Note that the tool does not check every possible problem and depends on unreliable heuristics, so it should be used as guidance only, not as a firm indicator of program correctness.
公式ドキュメントから和訳すると、「vet コマンドは問題になりえそうなものを発見してくれるが、それが必ず問題であるわけではない。ガイダンスとして利用すること」ということ。
今回のケースでは機械的に問題を発見できた。
- 投稿日:2020-03-22T16:23:02+09:00
実用HeadlessCMSになりそうな『Jolojo』の紹介と、『SveltElm』な開発スタイル。
三行で
- Svelteを使えるHeadlessCMSのJolojoがもうすぐオープンソースになるってよ。
- JolojoのAPIはGoで記述する模様。Svelteでなくても呼び出せるよ。
- JolojoのクライアントサイドをElm+Svelteとすることで硬め・大規模なCMS開発にも使えそう(=>個人の感想)。
...後半、話が広がっていっちゃうのだけれど、Fringe81さんのAdvent Calendar 2018の記事『組織の新言語導入(JS→TypeScript→Elm)で成したかった組織づくりの話』を読んでおいてもらえると話がつながるかな、と。
背景
JavaScript ベスト・オブ・ザ・イヤー 2019『フロントエンド フレームワーク』にて、以下のように紹介されたSvelte。
2019年はSvelteが躍進してAngularを追い抜き、
Vue.jsとReactの背後を突く3位に上昇しました。
これはBIG-3がBIG-4になったことを意味するのでしょうか?
Svelteは他のフレームワークと大きく異なっていて、実際にはフレームワークではなく、ビルド時に全てを構築するコンパイラのようなものです。
... コンパイラは、開発者が作成したコンポーネントを、DOMを直接操作する命令型コードに変換します。
そのため、ブラウザに渡されるコードは小さくなり、非常に高速に実行されます。Svelteをここ数日触っているが、Vue.jsやReactに比べさらに簡潔な記述が可能なところが気に入っている。当然、今風にSSR(サーバ・サイド・レンダリング)にも容易に対応できるようになっており、フロントエンドで何かを書く時の生産性を上げてくれそうだ。
さて、フロントエンド界隈の方々は、Vue.jsやReactを活用したSSR(サーバ・サイド・レンダリング)によるHeadlessCMSでサイトを構築することが一つのトレンドになっている。
2年近く前の先達のエントリ(ReactベースのHeadlessCMS構築)を以下に引用させてもらう。
出典 Headless CMSと静的サイトジェネレータ「Gatsby」を使ったサイトの構築
当然、Svelteでもやるからには、Headless CMSでサイト構築と行きたいところ。
ということで、Svelteについて、以下を軽く確認してみた。私としては、Svelteは、Headless CMSでのサイト構築に活用できるとという印象を得た。
他方、Svelteを実開発で使うには、以下が課題になるであろうことも分かった。
- サーバ側のフレームワークSapperが開発途上である。
- 一定規模以上の開発では素のJavascriptを書くことによる問題が出そう。
- Typescriptには現時点では未対応
近年はサーバサイドの仕事ばかりしている私だが、HTML5が出てきた頃には、coffeescriptなどを使ったフロントエンド開発をしていたことがある。その時に味わったmutable(状態変更可)なJavascript/AltJsの辛みは、SSR(サーバ・サイド・レンダリング)を行うシステムにおいては、サーバ側にも拡がりかねない。
vue.js向けのフロントサーバnuxt.jsに相当する役割を、現時点のSapperに期待するのはまだ難しそうだ(少なくともフロントエンド素人の私にとっては)。
出典 20 年代のフロントエンド『フロントエンドのアーキテクチャ上の分解点はどこになるか』ということで、『近い将来に、Svelteをこうしたフロントエンド・アーキテクチャに載せたい場合には何か選択肢はないのかな?』という思いで、headlesscms svelteでググって見たところ、既にSvelteベースでHeadlessCMSサイトを構築している取り組みが見つかったので、以下に紹介しておく。
HeadlessCMS『Jolojo』
公式サイト: https://www.jolojo.com/
残念ながら、まだ、『Get Jolojo』することはできない。Jolojoは、2020年内(Q1と書いているのでもうすぐかも)にオープンソースのCMSとする予定で開発中とのこと。ただし、このサイト自体がJolojoで開発中の構築されているとのことなので、サイトからクライアント側のソースコードを追うことはできる。ざっと見る限り、たしかにSvelteから作られたらしい素のJavascriptが見られる。
サイトから、Jolojoの概要を引用しておく:
Jolojo is a headless application which doesn't provide its own front end. Traditionally web applications have been built as 'monolith applications', where all of the functionality, including the front end, is bundled up in the same project.
The trouble with a monolithic application is that after years of work it becomes very difficult to make substantial changes. In contrast to that, a headless application provides a simple way to decouple the different parts of an application from each other, which in turn makes the application more flexible, open to experimentation and constant evolution.
(仮訳)
Jolojoは、独自のフロントエンドを提供しないheadless application(HeadlessCMS)です。従来、Webアプリケーションは、フロントエンドを含むすべての機能が同じプロジェクトにバンドルされる「モノリス」として構築されてきました。
モノリスなアプリケーションの問題は、何年もの作業の後、有意な変更を行うことが非常に困難になることです。これとは対照的に、headless application(HeadlessCMS)は、アプリケーションのさまざまな部分を互いに分離するための単純な方法を提供します。採用言語についての記述は以下の通り:
(出典 : https://www.jolojo.com/cms-platform-for-front-end-back-end-developers):Why Go and Svelte*?
Go is used to build the core headless API which drives the entire CMS. The reasons for choosing Go are that it's easy to pick up and learn, it's a strongly typed, compiled language, it's very stable, and it's secure.
In the same vein as Go, Svelte is also an easy to pick up and learn JavaScript framework that makes building complex front ends for web applications a much more manageable task. Svelts is also compliled has a small payload and is super fast :-)
(仮訳)
なぜ、GoとSvelteか?
Goは、CMS全体を駆動するコアのヘッドレスAPIを構築するために用いられます。Goを選択した理由は、簡単に選択して学ぶことができ、強く型付けされ、コンパイルされた言語であり、非常に安定しており、安全だからです。
Goと同じように、SwelteもJavaScriptフレームワークを簡単に習得して学ぶことができ、Webアプリケーションの複雑なフロントエンドの構築をはるかに管理しやすくしている。Svelteもコンパイルされており、ペイロードが小さく、超高速です :-)サーバ側がGo、クライアント側がJSベースのSvelteだとすると、SSRはできないのではと思われるかもしれないが、別の箇所にはnode.jsも動いている旨が書かれている。おそらくはJolojoでは、SSRが必要な箇所ではnode.jsによるレンダリングが行われているものと推測する(ソースコードが公開されてから確かめるべきところだが)。データベースは標準ではsqliteがバンドルされるということで、おそらくは開発用サーバ1台にすぐにデプロイできるようになるのだろう。CMSの機能拡張のためAPIで作る場合にはGoを書いてね、ということだろう。おそらくはシングルバイナリになるであろうGoでのAPI作成は現実解と言えるだろう。
Jolojoの機能は、以下の通り。なかなか意欲的に列挙されている(2020年のQ1/Q2で順次リリースしていくとのこと。)
https://www.jolojo.com/features開発元の、イギリスのCMS会社barkwebは既に実案件で、Jolojoを活用している模様。
(以下の事例紹介では、カレンダー機能などを組み込んだCMSが紹介されている。)
https://www.barkweb.co.uk/portfolio/heathfield-and-waldron-parish-council大規模なCMS開発の際には、Jolojoに精通したメンバーをアサインするのでコンタクトしてねとのこと。
...Jolojoが覚えにくいというひとは、ジョジョっぽい名前のCMSと頭に入れておこうか...!?Jolojoで硬めのCMSを開発することを夢想してみるに...
さて、ふだん処方系の情報を扱っている私は、近い将来に医薬系のオープンデータであるリアルワールドデータ(RWD)を活用した災害時医療支援向けのCMSのプロトタイプを作ってみたいと考えている(さわり的な記述)。地震や豪雨、さらにはパンデミックの等の発生時に各地の自治体や病院等による災害への対策を支援する会員制のCMSというイメージ(コンテンツは、匿名化された地域医療情報を含むリアルワールドデータから被災地のニーズや個々人の医療情報など)。非常時であっても、個人情報は可能な限り保護されなければならない。すなわち、公的機関が使える『硬めの』CMSでなければならないということだ。
まだ、オープンソースになっていないJolojoでの開発は、ただの夢想に過ぎないが、Jolojoの作りがこうした硬めの用途には良さそうに思えてきたので以下に追記していく。硬めにする解:Jolojoでのフロントエンド開発にSvelteに加えElmも用いる。
前述のように、私がSvelteに感じる不安は型付けが弱く実行時例外を出しがちなJavascriptベースであるところ。このことは、大規模開発やオープンソースでの開発では、個々人の力量によって脆弱性などを生み出すことに帰結しかねない(含、フロントエンド素人の私)。
ということで、十分に成熟していているimmutableなAltJSであるelmを組み合わせることが良いと考えた。
すなわち、以下の通り。
- フロントエンド側で副作用が必要ない処理は、(API呼び出しを含め全て)Elmで書く。
- (ローカルストレージへの永続化など)副作用ある操作が必要な時は、Elmからport経由でsvelte(すなわち、素のJS)を呼び出す。
Immutableに振っているelmは学習コストが低い(はず)。Elmに馴染みの無い方は、『ワイ』でおなじみのヤメタロウさんの『JavaScriptとElmを比べてみた〜前編〜』、『JavaScriptとElmを比べてみた〜後編・Vue.jsとも比べてみた〜』が楽しんで学べて良いだろう。ただし、現実には、既存のJSライブラリを遣いたい場合や、ローカルに変数を持ちたい場合があるだろう。その際には、潔く、Svelte(Javascript)を使う、と。
ElmもSvelteもJavascriptへとトランスパイルされる仕組みだが、同一のプロジェクト内でそれぞれ独立にトランスパイルできている(Jolojoを試す際により良い方法を探りたい。)SveltElm(スベルテルム)と勝手に呼んでおく。
既にさりげなく(?)タイトルに入れておいたが、Svelte+Elmという組み合わせを勝手にSveltElmと呼んでおきたい。なんとなくゴロがいいから。フロントエンド技術は採用言語に加えフレームワークと切り離せない。
私は比較的大規模な開発て採用されるtypescriptは、(関数型言語からすると)中途半端な型有り言語であるがゆえに学習コストは低くないと考えている。そのため、Haskellでなくとも関数型言語をサーバ側で使っているエンジニアの多くにとっては、学習コストは
reactjs + typescrit ≧ vuejs + typescrit > Svelte + Javascript + Elm
となるのではないかと考えているなので、名前をつけておこうと。
SveltElmの開発スタイルはハイブリッド関数型言語に近いはず。
さて、SveltElmだが、前述のように可能な限り、副作用のないコードをelmで書くことが前提となる。これは、私が日頃書いているScalaに近しいスタイルだ。
Scala界のご意見番的な存在である水島さんは、Scalaでwhile(ループ)やvar(変数)を使ってもいい理由を書いている。これは、Scalaでは、varやwhileは「使うべきではない」のではなくて、「使わなくていい」ことが大多数であることを前提に、ローカルな関数内では必要に応じvar変数を使ってループを回してもいいという話である。これは、私の日常感覚に近しい。付記 Elmでの大規模開発事例あるFringe81さんのElm入門記事
サーバ側にScalaエンジニアの多いFringe81さんは数年かけて、Elmチームを育て上げたという。
紹介記事『UniposチームにおけるElmエンジニア達の成長環境』が非常に参考になる(本番のコード7万行は、簡潔に書けるelmとしては相当量なのだろう)。
そのFringe81のエンジニアの方がElm記事
Elm入門と実践を書いてくれているので、最後にご紹介。HeadlessCMSが普及するためには、クライアント側により堅牢なコードが必要なんだってことだよね。
ということで、原則Elm、必要に応じSvelteという解がありなのではと、私は思ふ(どっちも、現時点ではreact/vueよりも速く動くらしいしね)。...その後は、ヤギさんが教えてくださっているように、公式ガイドの日本語訳、を併せて見る、と。
https://guide.elm-lang.jp/
- 投稿日:2020-03-22T14:54:47+09:00
PHP で Go の defer っぽいものを実装する
はじめに
Go には defer という return などの前に共通で呼び出してくれる機能があります。
package main import ( "fmt" ) func main() { defer func() { fmt.Printf("Hello World 1\n") }() fmt.Printf("Hello World 2\n") }上記の結果は
Hello World 2 Hello World 1です。この記事は、これを PHP でやる手段はあるかどうかについて書きます。
Q. PHP で defer はできるのか?
A. できます。
try { echo "Hello World2\n"; } finally { echo "Hello World1\n"; }または
class A { public function __destruct() { echo "Hello World!\n"; } } $_ = new A(); echo "Hello World1\n";上記のようにすれば PHP でも defer を実現することができます。
ちょっと待てよ?
すごく分かりづらくないですか?特に finally の場合だと複雑に defer の登録がされた場合、とても面倒くさいです。
Go の場合、package main import ( "fmt" ) func main() { defer func() { fmt.Printf("Hello World 1\n") }() if err == nil { return } defer func() { fmt.Printf("Hello World 3\n") }() fmt.Printf("Hello World 2\n") }のように条件文の後に defer を書くこともできます。 PHP の場合だと、実行用の変数に一度格納してから〜… つまり
try { $deferredFunctions = []; $deferredFunctions[] = function () { echo "Hello World1\n"; }; echo "Hello World2\n"; } finally { foreach ($deferredFunctions as $deferredFunction) { $deferredFunction(); } }上記のように書かないといけないわけです。実現できるっちゃできますが、非常に面倒くさいですね。 defer な処理をしたいものを増やすたびに上記のようなコードを無限大に書かなくてはいけないわけです。
defer の処理をするために、インデントを深くする必要があるのが辛い。そう、私達が求めているのは PHP でも Go の defer っぽく書けることです。defer(function () { echo "Hello World1\n"; }); echo "Hello World2\n";こうしたいわけですよね。
既に php-defer/php-defer がある
なるほど...
defer($_, function () { echo "goodbye\n"; });
$_
は書かなきゃいけないのか…なんとかならないのか?ということで、自作をしました。
php-deferrable
上記のライブラリは
$_
を書かなくても済みます。use function PHPDeferrable\defer; use function PHPDeferrable\deferrable; class MyClass { public function doSomething1() { defer(function () { echo "Three!\n"; }); defer(function () { echo "Two!\n"; }); echo "One!\n"; } public function doSomething2() { defer(function () { echo "NyanNyan!\n"; }); echo "Wanwan!\n"; } } /** * @var MyClass $myClass */ $myClass = deferrable(MyClass::class, ...$somethingArguments); $myClass->doSomething1(); $myClass->doSomething2();のように直感的に書けます。そして、クラスだけではなく callable なものに対しても defer を登録できるようにしています。
use function PHPDeferrable\defer; use function PHPDeferrable\deferrable; deferrable(function () { defer(function () { echo "0: deferred call\n"; }); echo "0: first call\n"; })();上記は下記のように出力されます。
0: first call 0: deferred callこれで PHP でも defer な処理ができますね!やったね?
どう実装しているのか?
詳しく知りたい方はコードを読んでみてほしいです。
クラスの場合における触りだけ解説をします。deferrable(MyClass::class)とすることにより
deferrable
関数からmakeContextManipulator
に渡されます。ここでは既存のコンテキストに対して defer が使えるようになるようにコードに多少小細工をしています。
ReflectionClass
を使って対象のクラスに登録されているメソッドを取り出して、各メソッドに try-finally とコンテキスト内で登録された defer を走らせる仕組みになっています。foreach ($reflection->getMethods() as $method) { $methodName = $method->getName(); if ($method->isAbstract()) { continue; } $body[] = static::makeMethodSignature($method) . ' { ' . '$deferContext = \\' . __NAMESPACE__ . '\\Deferrable::createDeferContext(' . $scope->getScopeType() . '); ' . 'try{' . '$result = parent::' . $methodName . '(...func_get_args()); ' . '} finally {' . '\\' . __NAMESPACE__ . '\\Deferrable::consume($deferContext);' . '}' . 'return $result; ' . '}'; }
abstract
の場合はそもそも呼べないものなので、スルーをしています。そしてこれらの値を元々のクラスを継承した新しいクラスにします。$temporaryClassName = Runtime::DEFER_ANONYMOUS_CLASS_PREFIX . (static::$temporaryClassCounter++); eval( 'class ' . $temporaryClassName . ' extends ' . $scope->getClassName() . ' implements \\' . __NAMESPACE__ . '\\Contracts\\DeferrableInterface' . '{' . implode($body) . '}' );こうすると、下記のようなコードが生成されます。
class __defer__anonymous_0 extends MyClass implements \PHPDeferrable\Contracts\DeferrableInterface { public function doSomething1() { $deferContext = \PHPDeferrable\Deferrable::createDeferContext(1); try { $result = parent::doSomething1( ...func_get_args()); } finally { \PHPDeferrable\Deferrable::consume($deferContext); } return $result; } public function doSomething2() { $deferContext = \PHPDeferrable\Deferrable::createDeferContext(1); try { $result = parent::doSomething2( ...func_get_args()); } finally { \PHPDeferrable\Deferrable::consume($deferContext); } return $result; } }このコードで defer を実現している、ということになります。
- 投稿日:2020-03-22T14:26:08+09:00
Go言語でソートのベンチマークを計測してみた
はじめに
今回使用するソートアルゴリズムは「バブルソート」「選択ソート」「挿入ソート」「シェルソート」です。ソートアルゴリズムによってどれくらいの差がでるのかベンチマークを計測したら、面白かったので各ソートアルゴリズムの特徴と合わせて紹介したいと思います。
※ 今回使用していない「クイックソート」「マージソート」についてもいつか追記できたらと思います。ベンチマークの計測について
ベンチマークの計測は go の標準パッケージの testing を使用します。
100,000 個のランダムな数値を持つスライスを生成し、各ソートアルゴリズムで生成されたスライスのソートが完了するまでの時間を計測します。
- 計測に使うPCのスペック
機種: MacBook Pro プロセッサ名: Dual-Core Intel Core i5 プロセッサ速度: 2.3 GHz メモリ: 16 GB
- ベンチマーク計測に使用するコード
// 1,000,000 以下の n 個のランダムなスライスを生成する func GenerateSlice(n int) []int { input := make([]int, n) for i := range input { rand.Seed(time.Now().UnixNano()) input[i] = rand.Intn(1000000) } return input } // 挿入ソートの場合 // ベンチマークを取るコードは以下の通り func BenchmarkInsertSort(b *testing.B) { // スライスを生成 input := GenerateSlice(100000) // b.ResetTimer()でスライスの生成時間をリセットする b.ResetTimer() for i := 0; i < b.N; i++ { // ソートを実行 InsertSort(input) } }バブルソート
バブルソートは配列の末尾から隣接する要素を順番に比較し、大小関係が逆だったら入れ替え、順番が逆になっている要素がなくなるまで入れ替えるソートです。
Wikipedia
計算量 O(N^2)コードは以下の通り
// input は int のスライス // 例) input := []int{2,7,8,3,1} func BubbleSort(input []int) []int { for i := 0; i < len(input); i++ { j := len(input) - 1 for j > 0 && j > i { if input[j] < input[j-1] { input[j], input[j-1] = input[j-1], input[j] } j-- } } return input }実行結果
BenchmarkBubbleSort-4 1 14395273888 ns/op 0 B/op 0 allocs/opバブルソートで100,000の要素を並び替えた場合にかかる時間は 約 14.4 秒
選択ソート
選択ソートは、各計算ステップですべての要素の中から最小値を選択し、先頭からソート済みの部分列を決定していくソートアルゴリズムです。
Wikipedia
計算量 O(N^2)
コードは以下の通り// input は int のスライス // 例) input := []int{2,7,8,3,1} func SelectionSort(input []int) []int { for i := 0; i < len(input); i++ { minj := i for j := i + 1; j < len(input); j++ { if input[j] < input[minj] { minj = j } } if i != minj { input[i], input[minj] = input[minj], input[i] } } return input }実行結果
BenchmarkSelectionSort-4 1 4726739202 ns/op 0 B/op 0 allocs/op選択ソートで100,000の要素を並び替えた場合にかかる時間は 約 4.7 秒
挿入ソート
整列してある要素に追加要素を適切な場所に挿入するソートアルゴリズムで、ほぼ整列されている要素に対しては高速に動作する特徴を持っています。
Wikipedia
計算量 O(N^2)
コードは以下の通り// input は int のスライス // 例) input := []int{2,7,8,3,1} func InsertSort(input []int) []int { for i := 1; i < len(input); i++ { for j := i - 1; j >= 0; j-- { if input[j+1] < input[j] { input[j], input[j+1] = input[j+1], input[j] } else { break } } } return input }実行結果
BenchmarkInsertSort-4 1 3211718521 ns/op 0 B/op 0 allocs/op挿入ソートで100,000の要素を並び替えた場合にかかる時間は 約 3.2 秒
シェルソート
シェルソートは、挿入ソートを応用して、数列を組み合わせたソートアルゴリズムです。数列を組み合わせることで、挿入ソートの「ほぼ整列したデータにおいては高速に動作する」という特徴を活かすことができます。
Wikipedia
計算量 O(N^2) だけど使用する数列によって平均計算量が O(N^1.25)
コードは以下の通りfunc ShellSort(input []int) []int { var l int = len(input) var n int = 0 sequence := []int{} // 数列 sequence[n+1] = 3 * sequence[n] + 1 を生成 for n < l { n = 3*n + 1 sequence = append(sequence, n) } // 数列 sequence を降順にする sort.Sort(sort.Reverse(sort.IntSlice(sequence))) // sequence[n]の分、離れた要素に対して挿入ソートを行う for _, g := range sequence { for i := g; i < l; i++ { for j := i - g; j >= 0; j -= g { if input[j] > input[j+g] { input[j], input[j+g] = input[j+g], input[j] } else { break } } } } return input }実行結果
BenchmarkShellSort-4 2000 835694 ns/op 296 B/op 7 allocs/opシェルソートで100,000の要素を並び替えた場合にかかる時間は 約 0.0008 秒
実行結果まとめ
すべてのベンチマークを並べると以下の通りです。
BenchmarkBubbleSort-4 1 14575993594 ns/op 0 B/op 0 allocs/op BenchmarkSelectionSort-4 1 4723814506 ns/op 0 B/op 0 allocs/op BenchmarkInsertSort-4 1 3247207398 ns/op 0 B/op 0 allocs/op BenchmarkShellSort-4 2000 835784 ns/op 296 B/op 7 allocs/opシェルソートが桁違いに高速!!
試しに今回計測に使用した 10 倍の 1,000,000 の要素に対してソートしたところ、シェルソート以外は処理が終わりませんでした。
今回取り上げたシェルソート以外にもクイックソートやマージソートなど高速なソートアルゴリズムがありますので、いつか追記できたらと思います。さいごに
普段は標準パッケージで用意されているアルゴリズムを使うので、アルゴリズムについて考える機会がなかなかなかったのですが、実際に自分で考えながら実装してみると各ソートアルゴリズムがどういった仕組みでソートしているのか理解でき、学ぶことが多かったです。また、ベンチマークを計測することで、同じサイズの要素を扱う場合でも、パフォーマンスにかなりの差が出てくることがわかりました。今後も、アルゴリズムについても学んで行きたいと思いました。
- 投稿日:2020-03-22T14:06:58+09:00
趣味グラマーが5日で5個GitHubリポジトリを作ってみた
趣味グラマーですが、GitHub リポジトリを5個作ってみました。せっかく作ったので誰かしらに見てもらえたら嬉しいなということと、初心者が GitHub を始める敷居を下げられたらいいなという思いで Qiita 投稿してみます。
※ ちなみに普段は運用系のお仕事をしているので、一応 IT 系ではあります。
GitHub のリポジトリってどうやって作ればいいんですか?
わかります、、、その疑問。でも大丈夫!そこは親切な GitHub さんなので、新しくブラウザからリポジトリを作ると以下みたいな画面を出してくれます。画面のとおりにコマンドを実行すると、作ったリポジトリに push(アップロード)してくれます。
そもそも Git がよくわからない!という人は、ひとまず git init 等のコマンドを、プロダクトを作ったディレクトリ上で実行してみると、イメージがつかめると思います。そのうえで、Git は別途勉強されたほうがいいかもしれません(私もadd / commit / pushしかわかりませんが、、、)。
- Git の参考 URL: https://backlog.com/ja/git-tutorial/
なんで GitHub を始めたんですか?
プログラマーかっけえ、GitHub かっけぇ、スター貰えたらすげぇ、というだけの精神です。
また、運用のお仕事をしていると技術的な知識は多少つくので、その知識を使ってなにか作ってみたいなぁと思いました。
Git については add / commit / push の知識がおぼろげにある程度だったので、GitHub は少し遠い存在だったのですが、公開するだけならそんなに難しくないことがわかってよかったです。
作ったもの(宣伝)
タイトルどおり、5個のリポジトリを作ってみました。それぞれ順々に宣伝していきます!
dtmu/ut
Unix time と通常の日時(UTC)を変換するツールです。以下のように使用します。Unix timeはミリ秒単位で扱います。
$ ut 2020-03-18 1:25:00.123 1584494700123 $ ut 1584494700123 2020-03-18 1:25:00.123dtmu/gip
現在使用しているグローバル IP アドレスを取得します。以下のように使用します。外部サイトをスクレイピングしたりして取得しています。
$ gip 12.34.56.78
dtmu/ppp
インスタント的に使える Web サーバーです。以下のように使用します。
# ポート 80 で "/" にて待受けする Web サーバー起動 $ ppp # 何も指定しないと、定形のJSONが返ります。 $ curl localhost {"Message":"Hello ppp!"} # HTTP リクエストヘッダーが出力されます。 $ ppp Request Header * 2020-03-20 22:04:03.915 < User-Agent: curl/7.58.0 < Accept: */*待受ポートや定型文を変えることができるほか、リクエストの応答までに遅延(sleep)を発生させることもできます。色々機能を追加する予定です。
dtmu/eniha
AWS のルートテーブルを使った HA クラスター作成用の Go ライブラリ(フェイルオーバー機能のみ)です。
ルートテーブルでは CIDR の向け先を特定の ENI に向けることができますよね。プロキシや VPN を使用している場合などでありそうです。以下はその例です。
192.168.24.0/24 => eni-XXXXXXXXXXXXXXXこういう場合、eni-XXXXXXXXXXXXXXX が死んだときに eni-YYYYYYYYYYYYYYY へフェイルオーバーしたいことがあると思います。
192.168.0.0/24 => eni-XXXXXXXXXXXXXXX | | failover!! V 192.168.0.0/24 => eni-YYYYYYYYYYYYYYY
- 参考 URL: http://aws.clouddesignpattern.org/index.php/CDP:Routing-Based_HA%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
そのときに使うライブラリが dtmu/eniha です。長くなりました。実装例は以下のような感じです。なお、このライブラリはフェイルオーバーの実行のみなので、トリガ等は別途用意する必要があります(ping 監視、CloudWatch 監視などがあるかなと思います。)
import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/dtmu/eniha" ) func main() { c := eniha.Cluster{ RouteTableId: "rtb-AAAAAAAAAAAAAAA", CidrBlock: "192.168.0.0/24", // クラスターに使用する ENI の ID を優先順に指定します。 // フェイルオーバー前に ENI が正常か評価する判定を func として追加します(正常な場合は nil で返す)。 // この例では、ENI が正常かの評価を実施しないので、現在の ENI でないもののうち、一番優先度の高いものに無条件でフェイルオーバーする設定です。 Enis: []eniha.Eni{ { "eni-XXXXXXXXXXXXXXX", func() error {return nil} , }, { "eni-YYYYYYYYYYYYYYY", func() error {return nil}, }, }, } // AWS SDK で使うセッション情報は利用者が設定する必要があります。 s := session.New(&aws.Config{ Region: aws.String("ap-northeast-1"), }) // FailOver でフェイルオーバーが試行されます。 result := c.FailOver(s) if len(eniha.GlobalErrors) != 0 { // eniha.GlobalErrors というグローバル変数に []errors の形でエラーが格納されます。 fmt.Println(eniha.GlobalErrors) return } // ENI の ID の before / after が Map[string]stringで返ります。 fmt.Println("failover: " + result["before"] + " => " + result["after"]) }バグ等あったら教えてください。まんま使わなくても、中身見ていただいて、コピって使っていただいてもOKです。
dtmu/xgo
これは駄作です。。。クロス環境のバイナリを生成するツールですが、色々欠陥があるので、mitchellh/gox を使ったほうがいいでしょう。
どうして Go 言語なんですか?
CLI ツールを作りやすかったり、何かとシンプルで開発に集中できる点がいいと思いました。趣味グラマーにおすすめだと思います。
以上、ここまで読んでいただき、ありがとうございました。作り出すと意外とハマりましたので、また色々量産していきたいと思います。
- 投稿日:2020-03-22T13:08:13+09:00
Next.js(React)でのCORSエラー解決(Next.js + Golang)
Next.jsでのCORSエラー解決(Next.js + Golang)
React + GoでSPAを作りたく、ReactフレームワークのNext.jsを使ってみた。
SPAの最初で躓きがちなCORSエラーについて、案の定ハマったのでメモ。(Angularでの開発の際もだいぶハマった思い出)CORSエラーが出る
今回のようにフロントエンド、バックエンドでサーバを分けて開発する際に、そのままだとCORS(Cross-Origin Resource Sharing)についてのエラーが発生する。
これはブラウザの機能として、クロスドメイン(ドメインの違うサーバへの)アクセスが制限されているため起きます。
例えば、フロントhttp://localhost:3000
, バックhttp://localhost:5000
とした時、フロントからバックのapiを叩こうとするとcorsエラーとなります。(ドメイン違うので)正常にapi叩くにはこれを解決する必要があります。
Next.jsではカスタムサーバを利用し、プロキシする
Next.jsでカスタムサーバを使用するため、 プロジェクトルートに
server.js
を作成します。また、
http-proxy-middleware
というパッケージを使い、任意のリクエストをプロキシします。server.jsconst express = require('express'); const next = require('next'); const { createProxyMiddleware } = require('http-proxy-middleware'); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV !== 'production' const API_URL = process.env.API_URL || 'http://localhost:5010' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare().then(() => { const server = express(); server.use( '/api', createProxyMiddleware({ target: API_URL, pathRewrite: { "^/api": "" }, changeOrigin: true }) ); server.all('*', (req, res) => { return handle(req, res) }); server.listen(port, err => { if (err) throw err console.log(`> Ready on http://localhost:${port}`) }); });カスタムサーバを利用してサーバ起動するので
package.json
を修正します。package.json... "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" }, ...これで
yarn dev
でカスタムサーバで立ち上がります。リクエストがプロキシされる
上記により、
/api
へのリクエストはhttp://localhsot:5010
にプロキシされます。
例えば/api/auth
へリクエストした場合、http://localhost:5010/auth
にプロキシされます。
pathRewrite
の記述を消せばhttp://localhost:5010/api/auth
にアクセスします。まとめ
Next.jsのcorsエラーの解決は、カスタムサーバと
http-proxy-middleware
パッケージの利用で解決できます。また忘れてハマりそうなのでメモしておいた。
参考
- 投稿日:2020-03-22T12:11:37+09:00
Go言語でnull値を含むParquetファイルを生成する
環境
- macOS Catalina
- Go 1.14
- parquet-go 1.5.1
- pyarrow 0.16.0(確認用)
- pandas 1.0.3(確認用)
Parquetファイルの生成
このソースコードを
main.go
として保存する。package main import ( "log" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/writer" ) type Example struct { Col *string `parquet:"name=col, type=UTF8"` } func main() { fw, err := local.NewLocalFileWriter("example.parquet") if err != nil { log.Fatal(err) } pw, err := writer.NewParquetWriter(fw, new(Example), 4) if err != nil { log.Fatal(err) } row0 := "foo" row2 := "bar" rows := []*string{&row0, nil, &row2} for i := 0; i < 3; i++ { ex := Example{Col: rows[i]} if err := pw.Write(ex); err != nil { log.Fatal(err) } } if err = pw.WriteStop(); err != nil { log.Fatal(err) } fw.Close() }null値を含むParquetファイルを生成する上で重要なのは、下記行である。
Col *string `parquet:"name=col, type=UTF8"`つまり、
string
ではなく*string
を指定する。作成したファイルに対して
go run main.go
を実行すると、example.parquet
が生成される。Parquetファイルの確認
下記のPythonスクリプトを使えば、確かにnull値を含むParquetファイルが生成されたことを確認できる。
import pyarrow.parquet as pq table = pq.read_table('example.parquet') print(table.to_pandas())このファイルを
read.py
として保存し、python3 read.py
を実行するとcol 0 foo 1 None 2 barと出力される。
もちろんGo言語でも、この
example.parquet
を読み込める。package main import ( "fmt" "log" "github.com/xitongsys/parquet-go-source/local" "github.com/xitongsys/parquet-go/reader" ) type Example struct { Col *string `parquet:"name=col", type=UTF8` } func main() { fr, err := local.NewLocalFileReader("example.parquet") if err != nil { log.Fatal(err) } pr, err := reader.NewParquetReader(fr, new(Example), 4) if err != nil { log.Fatal(err) } num := int(pr.GetNumRows()) ex := make([]Example, num) if err := pr.Read(&ex); err != nil { log.Fatal(err) } for i := 0; i < num; i++ { col := ex[i].Col if col != nil { fmt.Println(*col) } else { fmt.Println("<nil>") } } pr.ReadStop() fr.Close() }このファイルを
main.go
として保存し、go run main.go
を実行するとfoo <nil> barと出力される。
- 投稿日:2020-03-22T10:23:19+09:00
Golang入門.1 -http.HandleでHello World!-
はじめに
他の言語でweb開発の経験がある人 (筆者) がgolangに入門する際に勉強したことを一連の記事でまとめていきます。
やること
記事においてはweb開発において必要な機能をはじめとした、ある程度実践的な範囲を対象とします。基本的には対象とする機能を実装したコードを記載します。またコードだけではなく、ドキュメントを踏まえた概念的な説明を挟んでいきます。
やらないこと
開発環境の構築方法や基本文法の説明は省略します。
今回のテーマ
http.Handleを用いてHello World!を表示させるまでの流れを説明します。
概念編
以下はnet/httpのdocumentから抜粋しています。
golangでは他の言語と同様にHTTP requestに応じてresponseを生成します。ざっくりとした流れは以下の通りです (簡単のため、今回はURLにのみ注目します) 。
1. HTTP requestの中身を確認する
2. URLをparseし、事前に登録しておいたpatternとmatchするものを探す
3. matchするpatternがあれば、そのpatternに応じたhandlerを元にresponseを生成 (なければerror処理)
上記の流れで必要なpatternとのmatchを行なったり、patternとhandlerの対応を登録したりするものをマルチプレクサと言います。net/httpにおいてはfunc Handleを呼び出した際に、引数として渡したhandlerがtype ServeMuxに登録されています。そして、2や3においてServeMuxの情報を用いて処理が実行されます。コード編
mux.gopackage main import ( "fmt" "net/http" ) type HelloHandler struct{} func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!") } func main() { hello := HelloHandler{} server := http.Server{ Addr: "127.0.0.1:8080", } http.Handle("/", &hello) server.ListenAndServe() }コードの実行・確認
ターミナルから実行できます。
go run mux.goブラウザ上でlocalohost:8080と入力すれば結果が見れます。また、
curl localhost:8080/としても確認できます。
コードの解説
上記のコードのうち、以下の部分がやや奇妙な挙動をしているように感じます。
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!") }fmt.Fprintfという部分からprint関数を呼び出していることがわかります。print関数といえば、ターミナルに出力を返すものとして使用することが多いと思います。例えばお馴染みのpython3では以下のように書きます。
print('Hello World!')両者の違いは何でしょうか?ポイントはFprintf関数の第1引数として指定されているhttp.ResponseWriterです。
fmt.Fprintfのdocumentによると以下のように説明がなされています。func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
Fprintf formats according to a format specifier and writes to w. It returns the number of bytes written and any write error encountered.第1引数で指定したinput/output writerに出力先を指定していることが分かります。また、実際のコードでは以下のように記述されています。
// Fprintf formats according to a format specifier and writes to w. // It returns the number of bytes written and any write error encountered. func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return }全てのコードを掲載するのは冗長なので流れを説明します。Fprintfで行なっている操作は以下の通りです。
1. printer (p) の生成
2. doPrintfを実行し、printerのbufferに情報を格納
3. printerのbufferから情報を読み取りi/o writerが示す出力先に出力
4. printerの解放
このように、i/oなどにおいて一時的に情報を格納する領域をbufferと言います。fmtにおいてはprinterをtype pp structという構造体で定義した上で上記のようなbufferを用いた出力を可能にしています。なお、golangと対比させる形で紹介したpythonでも省略されている第3引数fileに明示的に出力先を指定することができます。これにより、stdoutつまりターミナルから別の場所へと出力先を変更できます。最後に
今後もライブラリのソースコードやdocumentの記載を用いる形で解説を行なっていきたいと思います。
- 投稿日:2020-03-22T09:22:52+09:00
Golangで、デザインパターン「Builder」を学ぶ
GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Builder」を学ぶ"今回は、Pythonで実装した” Builder”のサンプルアプリをGolangで実装し直してみました。
■ Builder(ビルダー)
Builderパターン(ビルダー・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。
UML class and sequence diagram
UML class diagram
■ "Builder"のサンプルプログラム
Builderパターンとは、複雑な構造のものを一気に組み立てるのは難しいので、事前に全体を構成する各部分をつくって、段階を踏んで構造を持ったインスタンスを組み上げていくものらしいです。
実際に、Builderパターンを使って、「文書」を作成するサンプルプログラムを動かしてみて、動作の様子を確認したいと思います。
plain
モードで、動作させると、プレーンテキスト形式の文書が出力されます。html
モードで、動作させると、テーブル形式のリンク集のHTMLファイルが生成されます。(1)
plain
モードで動かしてみるまずは、プレーンテキスト形式の文書が出力するコードを動かしてみます。
$ go run Main.go plain ====================== # Greeting *** From the morning to the afternoon *** - Good morning - Hello *** In the evening *** - Good evening - Good night - Good bye ======================所謂、プレーンテキストの文書が出力されました。
(2)
html
モードを動かしてみるつづいて、TableベースのWebページを作成するコードを動かしてみます。
$ go run Main.go html [Greeting.html] was created.
Greeting.html
というファイルが生成されました。
Webブラウザで、見た目を確認してみると、こんな感じでした。
■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Builder
- ディレクトリ構成
. ├── Greeting.html ├── Main.go └── builder ├── builder.go ├── director.go ├── html_builder.go └── text_builder.go(1) Builder(建築者)の役
Builder
役は、インスタンスを作成するためのインタフェースを定めます。
Builder
役には、インスタンスの各部分を作るためのメソッドが用意されます。
サンプルプログラムでは、builder
インタフェースが、この役を努めます。builder/builder.gopackage builder type builder interface { makeTitle(title string) makeString(str string) makeItems(items []string) close() }(2) ConcreteBuilder(具体的建築者)の役
ConcreteBuilder
役は、Builder
役のインタフェースを実装しているクラスです。実際のインスタンス作成で呼び出されるメソッドが、ここで定義されます。また、最終的にできた結果を得るためのメソッドが用意されます。
サンプルプログラムでは、TextBuilder
構造体や、HTMLBuilder
構造体が、この役を努めます。builder/text_builder.gopackage builder import "fmt" // TextBuilder is struct type TextBuilder struct { buffer string } // NewTextBuilder func for initializing TextBuilder func NewTextBuilder() *TextBuilder { return &TextBuilder{} } func (t *TextBuilder) makeTitle(title string) { t.buffer += "======================\n" t.buffer += fmt.Sprintf("# %s\n", title) t.buffer += "\n" } func (t *TextBuilder) makeString(str string) { t.buffer += fmt.Sprintf("*** %s ***\n", str) } func (t *TextBuilder) makeItems(items []string) { for _, i := range items { t.buffer += fmt.Sprintf("- %s\n", i) } } func (t *TextBuilder) close() { t.buffer += "======================\n" } // GetResult func for fetching all from buffer func (t *TextBuilder) GetResult() string { return t.buffer }builder/html_builder.gopackage builder import ( "fmt" "os" ) // HTMLBuilder is struct type HTMLBuilder struct { buffer, filename string f *os.File makeTitleCalled bool } // NewHTMLBuilder func for initializing HTMLBuilder func NewHTMLBuilder() *HTMLBuilder { return &HTMLBuilder{} } func (h *HTMLBuilder) makeTitle(title string) { h.filename = fmt.Sprintf("%s.html", title) h.f, _ = os.Create(h.filename) h.f.Write([]byte(fmt.Sprintf("<html><head><title>%s</title></head></html>", title))) h.f.Write([]byte(fmt.Sprintf("<h1>%s</h1>", title))) h.makeTitleCalled = true } func (h *HTMLBuilder) makeString(str string) { if !h.makeTitleCalled { os.Exit(1) } h.f.Write([]byte(fmt.Sprintf("<p>%s</p>", str))) } func (h *HTMLBuilder) makeItems(items []string) { if !h.makeTitleCalled { os.Exit(1) } h.f.Write([]byte("<ul>")) for _, i := range items { h.f.Write([]byte(fmt.Sprintf("<li>%s</li>", i))) } h.f.Write([]byte("</ul>")) } func (h *HTMLBuilder) close() { if !h.makeTitleCalled { os.Exit(1) } h.f.Write([]byte("</body></html>")) h.f.Close() } // GetResult func for replying filename func (h *HTMLBuilder) GetResult() string { return h.filename }(3) Director(監督者)の役
Director
役は、Builder
役のインタフェースを使って、インスタンスを生成します。
ConcreteBuilder
役に依存したプログラミングは行いません。ConcreteBuilder
役が何であってもうまく動作するように、Builder
役のメソッドのみを使います。
サンプルプログラムでは、Director
タイプが、この役を努めます。builder/director.gopackage builder // Director is struct type Director struct { builder builder } // NewDirector func for initializing Director func NewDirector(builder builder) *Director { return &Director{ builder: builder, } } // Construct func for conducting some methods of builder func (d *Director) Construct() { d.builder.makeTitle("Greeting") d.builder.makeString("From the morning to the afternoon") d.builder.makeItems([]string{"Good morning", "Hello"}) d.builder.makeString("In the evening") d.builder.makeItems([]string{"Good evening", "Good night", "Good bye"}) d.builder.close() }(4) Client(依頼人)の役
Builder
役を利用する役です。
サンプルプログラムでは、startMain
メソッドが、この役を努めます。Main.gopackage main import ( "flag" "fmt" "./builder" ) func startMain(opt string) { if opt == "plain" { builderObj := builder.NewTextBuilder() director := builder.NewDirector(builderObj) director.Construct() result := builderObj.GetResult() fmt.Println(result) } else if opt == "html" { builderObj := builder.NewHTMLBuilder() director := builder.NewDirector(builderObj) director.Construct() result := builderObj.GetResult() fmt.Printf("[%s] was created.\n", result) } } func main() { flag.Parse() startMain(flag.Arg(0)) }■ 参考URL
- 投稿日:2020-03-22T08:12:47+09:00
パケットフィルタの仕組みを理解しつつGoで特定のプロトコルパケットを取得(フィルタ)する
理解しておくと良い前提知識
- カーネル空間/ユーザー空間の違い
- ネットワークの基礎
- ソケットプログラミングの基礎
環境
$ go version go version go1.13.5 linux/amd64
パケットフィルタの仕組み
特定のプロトコルパケットをフィルタする前にパケットフィルタの仕組みについて記す。
※cBPFとeBPFの違い、現在のBPFの機能がパケットフィルタだけではないとか、BPFの歴史/変遷などについてはこの記事では触れていませんのでご了承ください例としてtcpdumpでのパケットフィルタを用いる。
tcpdumpなどのパケットキャプチャツールはソフトウェアとしてユーザ空間で動作します。
また、tcpdumpでキャプチャされているデータはカーネル空間からコピーされたデータとなっており、直接アプリケーションなどがやり取りするパケットを覗いているわけではありません。(そんなことしてたらオーバーヘッドがやばい)
ユーザランドでフィルタをしていてはカーネル空間から実際にフィルタしたいプロトコルのパケット以外のパケットも全てコピーしてしまう。
これを回避するためにカーネルのBPF(Berkeley Packet Filter)と呼ばれるパケットフィルタリング機能を使用している。
カーネル空間で仮想マシン(仮想的なレジスタマシン)を実行しフィルタをカーネル空間で実現している。Goを用いてネットワークに流れるパケットを取得する
比較を行うために以下のコードを用いてフィルタを行わずパケットを取得します。
イーサネットフレームのタイプを用いて出力を制御。package main import ( "encoding/binary" "flag" "fmt" "net" "os" "syscall" ) // 他にも上位のプロトコルタイプは存在するがわかりやすくするため3種類のみ使用する const ( EthTypeArp uint16 = 0x0806 EthTypeIpv4 uint16 = 0x0800 EthTypeIpv6 uint16 = 0x86dd ) // MACヘッダ構造体 type EtherHeader struct { DstMacAddr net.HardwareAddr SrcMacAddr net.HardwareAddr ProtoType uint16 } // お約束のビッグエンディアン変換 func htons(host uint16) uint16 { return (host&0xff)<<8 | (host >> 8) } // イーサネットヘッダのタイプを見て出力の内容を制御 func analyzePacket(data []byte) { // パース dstMacAddr := data[:6] srcMacAddr := data[6:12] protoType := binary.BigEndian.Uint16(data[12:14]) eh := &EtherHeader{ DstMacAddr: dstMacAddr, SrcMacAddr: srcMacAddr, ProtoType: protoType, } // ここでイーサネットヘッダーのタイプを判別 switch eh.ProtoType { case EthTypeArp: fmt.Println("ARP Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) case EthTypeIpv4: fmt.Println("IPv4 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) case EthTypeIpv6: fmt.Println("IPv6 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) default: fmt.Println("Another Type Packet") } } func main() { // 引数のパース flag.Parse() // リンクレイヤ(L2)のヘッダを含むすべてのプロトコルパケットの生データを取得する fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL))) if err != nil { fmt.Println(err.Error()) } // 終了したときにファイルディスクリプタを閉じる defer syscall.Close(fd) // インターフェースのインデックスを取得する ifIndex, err := net.InterfaceByName(flag.Arg(0)) if err != nil { fmt.Println(err.Error()) } // インターフェースにソケットをバインドする addr := syscall.SockaddrLinklayer{Protocol: htons(syscall.ETH_P_ALL), Ifindex: ifIndex.Index} if err := syscall.Bind(fd, &addr); err != nil { fmt.Println(err.Error()) } // プロミスキャス・モードで受信 if err := syscall.SetLsfPromisc(flag.Arg(0), true); err != nil { fmt.Println(err.Error()) } // ファイルの作成 file := os.NewFile(uintptr(fd), "") // パケットをforループで読み続け出力 for { buf := make([]byte, 2048) num, err := file.Read(buf) if err != nil { fmt.Println(err.Error()) break } else { analyzePacket(buf[:num]) } } }出力が制御できることを確認する。
$ sudo ./packet-analyzer eth1 # ネットワークインターフェースの名前 ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> a0:56:f3:cc:89:31 dst-> 01:00:5e:00:00:fb IPv6 Packet: src-> a0:56:f3:cc:89:31 dst-> 33:33:00:00:00:fb IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> d4:f5:47:26:8d:7f IPv4 Packet: src-> d4:f5:47:26:8d:7f dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> d4:f5:47:26:8d:7f IPv4 Packet: src-> a0:56:f3:cc:89:31 dst-> 01:00:5e:00:00:fb IPv6 Packet: src-> a0:56:f3:cc:89:31 dst-> 33:33:00:00:00:fb IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv6 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 IPv6 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80特定のプロトコルをフィルタしてみる
先程記述したプログラムからARPパケットのみを出力するようBPF(LSF)を用いてフィルタしてみる。
プログラムに少し追記します。package main import ( "encoding/binary" "flag" "fmt" "net" "os" "syscall" ) const ( EthTypeArp uint16 = 0x0806 EthTypeIpv4 uint16 = 0x0800 EthTypeIpv6 uint16 = 0x86dd ) type EtherHeader struct { DstMacAddr net.HardwareAddr SrcMacAddr net.HardwareAddr ProtoType uint16 } func htons(host uint16) uint16 { return (host&0xff)<<8 | (host >> 8) } func analyzePacket(data []byte) { dstMacAddr := data[:6] srcMacAddr := data[6:12] protoType := binary.BigEndian.Uint16(data[12:14]) eh := &EtherHeader{ DstMacAddr: dstMacAddr, SrcMacAddr: srcMacAddr, ProtoType: protoType, } switch eh.ProtoType { case EthTypeArp: fmt.Println("ARP Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) case EthTypeIpv4: fmt.Println("IPv4 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) case EthTypeIpv6: fmt.Println("IPv6 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr) default: fmt.Println("Another Type Packet") } } func main() { flag.Parse() fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL))) if err != nil { fmt.Println(err.Error()) } defer syscall.Close(fd) ifIndex, err := net.InterfaceByName(flag.Arg(0)) if err != nil { fmt.Println(err.Error()) } addr := syscall.SockaddrLinklayer{Protocol: htons(syscall.ETH_P_ALL), Ifindex: ifIndex.Index} if err := syscall.Bind(fd, &addr); err != nil { fmt.Println(err.Error()) } if err := syscall.SetLsfPromisc(flag.Arg(0), true); err != nil { fmt.Println(err.Error()) } // ここでカーネルにフィルタを適用する // ARPパケットのみを扱う際の命令を定義し、適用する(ARP以外のパケットはアプリケーションで受信しない) rawInstructions := []syscall.SockFilter{ {0x28, 0, 0, 0x0000000c}, {0x15, 0, 1, 0x00000806}, {0x6, 0, 0, 0x00040000}, {0x6, 0, 0, 0x00000000}, } if err := syscall.AttachLsf(fd, rawInstructions); err != nil { fmt.Println(err.Error()) } file := os.NewFile(uintptr(fd), "") for { buf := make([]byte, 2048) num, err := file.Read(buf) if err != nil { fmt.Println(err.Error()) break } else { analyzePacket(buf[:num]) } } }出力がARPパケットのみになっていることを確認する。
$ sudo ./packet-analyzer eth1 ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> d4:f5:47:26:8d:7f dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27 ARP Packet: src-> 3c:28:6d:d0:6f:e2 dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80 ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27追記したコードについての説明です。
下記でBPFの命令セットを定義します。
この命令セット(packet-matching code)はtcpdump -dd arp
で得ることができます。rawInstructions := []syscall.SockFilter{ {0x28, 0, 0, 0x0000000c}, {0x15, 0, 1, 0x00000806}, {0x6, 0, 0, 0x00040000}, {0x6, 0, 0, 0x00000000}, }その後、
syscall.AttachLsf
を用いて命令をソケットにアタッチすることでフィルタすることができます。if err := syscall.AttachLsf(fd, rawInstructions); err != nil { fmt.Println(err.Error()) }google/gopacketでの実装
今回は
syscall
を直接叩きましたがgoogle/gopacketではCGoを用いて実装されているようです!
libpcap
のPCAP_API
を呼び出して使用しています。最後に
理解を深めるために実装を読んだり、BPFの歴史について調べたり、なかなか面白みがありました!
間違っている所などもあるかもしれませんが、その時はご教授いただけると幸いです。
- 投稿日:2020-03-22T01:50:28+09:00
goenvのコマンドメモ
pyenvと似たような感じですね。
command 内容 goenv commands 使用可能なgoenvコマンドを全て表示 goenv completions --completeを指定して呼び出すことにより、自身および他のコマンドの自動補完を提供します。 goenv exec 指定した仮想環境のGoバージョンでファイルを実行する。
例)goenv exec go run main.gogoenv global すべてのシェルで使用されるGoのグローバルバージョンを設定する。 goenv help コマンドのヘルプを表示
例)goenv help execとか、goenc help installなど、helpのあとにコマンドを入力すると、当該コマンドのヘルプを表示してくれる。goenv hooks 特定のgoenvコマンドのフックスクリプトを一覧表示する。 goenv init goenvのシェル環境を構成する。 goenvをシェルと統合する場合に使う。
.bashrcや.zshrc、.bash_profileに「eval "$(goenv init -)"」を追記しておく必要がある。goenv install Goバージョンのインストール(インストール可能なバージョンは「goenv install -l」で表示できる。) goenv local ローカルのアプリケーション固有のGoバージョンを設定する。
グローバルのGoはv1.10.0だが、特定のフォルダだけv1.11.0にしたりとかできる。goenv prefix Goバージョンがインストールされているディレクトリを一覧表示。 goenv rehash goenvが認識するすべてのGoバイナリ(つまり、〜/.goenv/versions//bin/)にshimをインストールする。
Goの新しいバージョンをインストールした後、またはバイナリを提供するパッケージをインストールした後に、このコマンドを実行するらしい。goenv root Goバージョンとshimが保持されているルートディレクトリを表示する。 goenv shell シェルでGOENV_VERSION環境変数を設定して、シェル固有のGoバージョンを設定する。
アプリケーション固有のバージョンとグローバルバージョンをオーバーライドするので、一番強い(小並感)。
解除は「goenv shell --unset」goenv shims goenvのshimを一覧表示する。 goenv uninstall Goバージョンの削除。
例)goenv uninstall 1.11.0goenv version アクティブなGoバージョンを一覧表示する。 goenv --version goenv自体のバージョンを表示する。 goenv version-file Goバージョンを設定しているファイルを一覧表示する。 goenv version-file-read 指定されたgo-versionファイルが存在する場合、そのファイルを読み取る。
例)goenv version-file-read ./go-versiongoenv version-file-write go-versionファイルのバージョンを書き換える。 goenv version-name 現在のGoバージョンを表示 goenv version-origin Goバージョンの設定方法を表示する。 goenv versions 全てのGoバージョンを一覧表示し、現在のアクティブバージョンの横には「*」を表示する。 goenv whence 指定されたコマンドがインストールされたすべてのGoバージョンを一覧表示。 goenv which 指定したコマンドを実行したときにgoenvが呼び出す実行可能ファイルのフルパスを表示する。
- 投稿日:2020-03-22T01:50:28+09:00
goenvのコマンドチートシート
pyenvと似たような感じですね。
コマンドチートシート
command 内容 goenv commands 使用可能なgoenvコマンドを全て表示 goenv completions --completeを指定して呼び出すことにより、自身および他のコマンドの自動補完を提供します。 goenv exec 指定した仮想環境のGoバージョンでファイルを実行する。
例)goenv exec go run main.gogoenv global すべてのシェルで使用されるGoのグローバルバージョンを設定する。 goenv help コマンドのヘルプを表示
例)goenv help execとか、goenc help installなど、helpのあとにコマンドを入力すると、当該コマンドのヘルプを表示してくれる。goenv hooks 特定のgoenvコマンドのフックスクリプトを一覧表示する。 goenv init goenvのシェル環境を構成する。 goenvをシェルと統合する場合に使う。
.bashrcや.zshrc、.bash_profileに「eval "$(goenv init -)"」を追記しておく必要がある。goenv install Goバージョンのインストール(インストール可能なバージョンは「goenv install -l」で表示できる。) goenv local ローカルのアプリケーション固有のGoバージョンを設定する。
グローバルのGoはv1.10.0だが、特定のフォルダだけv1.11.0にしたりとかできる。goenv prefix Goバージョンがインストールされているディレクトリを一覧表示。 goenv rehash goenvが認識するすべてのGoバイナリ(つまり、〜/.goenv/versions//bin/)にshimをインストールする。
Goの新しいバージョンをインストールした後、またはバイナリを提供するパッケージをインストールした後に、このコマンドを実行するらしい。goenv root Goバージョンとshimが保持されているルートディレクトリを表示する。 goenv shell シェルでGOENV_VERSION環境変数を設定して、シェル固有のGoバージョンを設定する。
アプリケーション固有のバージョンとグローバルバージョンをオーバーライドするので、一番強い(小並感)。
解除は「goenv shell --unset」goenv shims goenvのshimを一覧表示する。 goenv uninstall Goバージョンの削除。
例)goenv uninstall 1.11.0goenv version アクティブなGoバージョンを一覧表示する。 goenv --version goenv自体のバージョンを表示する。 goenv version-file Goバージョンを設定しているファイルを一覧表示する。 goenv version-file-read 指定されたgo-versionファイルが存在する場合、そのファイルを読み取る。
例)goenv version-file-read ./go-versiongoenv version-file-write go-versionファイルのバージョンを書き換える。 goenv version-name 現在のGoバージョンを表示 goenv version-origin Goバージョンの設定方法を表示する。 goenv versions 全てのGoバージョンを一覧表示し、現在のアクティブバージョンの横には「*」を表示する。 goenv whence 指定されたコマンドがインストールされたすべてのGoバージョンを一覧表示。 goenv which 指定したコマンドを実行したときにgoenvが呼び出す実行可能ファイルのフルパスを表示する。 goenv導入(Homebrew)
$ brew update $ brew install goenv
.bash_profile
に追記(環境によっては.bashrc
や.zshrc
)echo 'eval "$(goenv init -)"' >> ~/.bash_profile忘れずに反映
source ~/.bash_profilegoenvの最新バージョン取得
brew installしたgoenvは新しいGoバージョンが出ても反映してくれません。
新しいものは全てgoenv 2.0.0bataXXに反映されていくので、それをインストールします。brew install --HEAD goenv以下のようなエラーが出た場合は、エラーメッセージに従って「brew unlink goenv」をしてから再度実行しましょう。
Error: goenv 1.23.3 is already installed To install HEAD, first run `brew unlink goenv`. Warning: Skipping (old) /usr/local/Cellar/goenv/1.23.3 due to it being linked注意
--HEADでインストールしたものはbrew upgradeで更新してくれない
ので、--fetch-HEAD
で更新する。brew upgrade --fetch-HEAD goenvめんどくさい(;ω;)
- 投稿日:2020-03-22T00:00:02+09:00
Docker Compose upでMySQLが起動するまで待つ方法(2種類紹介)
docker-compose upで立ち上がらないGoのコンテナがありまして
docker-compose up -dでコンテナを立ち上げる際、
なぜかWeb側のコンテナが立ち上がらないという現象が発生していました。docker-compose up -d!
docker-compose up -d Creating network "api_network" with the default driver Creating redis ... done Creating mysql ... done Creating api ... doneしかし調べてみると、、、
docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api app/build/api Exit 2webのコンテナがExitとなっており、立ち上がっていません..
docker-compose logs api api | panic: dial tcp 172.28.0.3:3306: connect: connection refusedなぜだーーー
これは割とあるあるな話のようで、
MySQLコンテナの立ち上がりが遅く、起動する前に接続しにいこうとしてしまい、
コケてしまうようです。ま、もう1回docker-compose upすれば済む話なのですが、
1発でなんとかせよ! というミッションがありまして、達成に向けて試行錯誤してみる事に。今回はシェルスクリプトを使う方法と、Goのコードから起動確認する方法を試してみました。
それぞれご紹介します!問題解決に到るまで、5名以上の方からアドバイスを頂けまして、
そのおかげで達成できました! 本当にありがとうございます!!下記の記事を参考にさせてもらいました ↓
http://docs.docker.jp/compose/startup-order.html
https://qiita.com/shiena/items/47437f4f7874bf70d664シェルスクリプトを使ってpingで接続を確認して繋ぎにいく
以下のようなシェルスクリプトを書きます
start.sh# !/bin/.sh # MySQLサーバーが起動するまでループで待機する until mysqladmin ping -h mysql --silent; do echo 'waiting for mysqld to be connectable...' sleep 2 done echo " go app is started!" exec app/build/apistart.shをwebのコンテナ内に入れます。今回はmntに入れました。
docker-compose.ymlを以下のように書き換えます。docker-compose.ymlvolumes: - ./api/scripts:/mnt entrypoint: mnt/start.shentrypointを.shにして、ループ処理をさせてから
1番最後のexecでコンテナを立ち上げるようにします。ただし、mysqladmin pingを実行する為には、
mysql-clientをインストールさせる必要があります。
Dockerfileにそれ用の記述を書き加えます。DockerfileRUN apk update \ && apk add mysql-client時は来た!
docker-compose up -d!docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api ./api/scripts:start.sh Up 0.0.0.0:8080->8080/tcpやった! 成功です!
GoのコードでMySQLの立ち上がりを確認してから接続するようにする
下記の記事を参考にさせてもらいました ↓
https://qiita.com/Bmouthf/items/d3cfdbee74caeda77e3f
https://kleinblog.net/docker-golang-mysql-min/上の方法で上手くいったのですが、その為だけにmysql-clientをインストールさせるのは
ちょっと... という事で、GoのコードでMySQLコンテナの立ち上がりを待つようにします!mysqlへの接続は接続用の関数を呼び出して繋いでいたのですが、
少し書き加えてMySQLの立ち上がりを確認してから接続させるようにします。↓ mysqlの関数を呼び出して接続させている
main.godb := mysql.ConnectDB() db.LogMode(true) defer db.Close()importにgormを追加
import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" )以下のように書き足しました。
main.godb, connectcheck := gorm.Open("mysql") if connectcheck != nil { db = mysql.ConnectDB() db.LogMode(true) defer db.Close() }docker-compose up -d!
docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api app/build/api Up 0.0.0.0:8080->8080/tcpよっしゃー!
今回は、Dokcer + Go + MySQLコンテナの組み合わせでした。
同じような現象に悩まれてる方が何らかの参考になりましたら幸いです!著者自身、DockerもGoも全然分かっていない未熟者でして不備もあるかと思います。
批評、マサカリ大歓迎です!
何かありましたら、ぜひご意見アドバイス等等下さいませ〜!!
- 投稿日:2020-03-22T00:00:02+09:00
docker-compose upでMySQLが起動するまで待つ方法(2種類紹介)
docker-compose upで立ち上がらないGoのコンテナがありまして
docker-compose up -dでコンテナを立ち上げる際、
なぜかWeb側のコンテナが立ち上がらないという現象が発生していました。docker-compose up -d!
docker-compose up -d Creating network "api_network" with the default driver Creating redis ... done Creating mysql ... done Creating api ... doneしかし調べてみると、、、
docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api app/build/api Exit 2webのコンテナがExitとなっており、立ち上がっていません..
docker-compose logs api api | panic: dial tcp 172.28.0.3:3306: connect: connection refusedなぜだーーー
これは割とあるあるな話のようで、
MySQLコンテナのMySQL実行完了が遅く、起動する前に接続しにいこうとしてしまい、
コケてしまうようです。ま、もう1回docker-compose upすれば済む話なのですが、
1発でなんとかせよ! というミッションがありまして、達成に向けて試行錯誤してみる事に。今回はシェルスクリプトを使う方法と、Goのコードから起動確認する方法を試してみました。
それぞれご紹介します!問題解決に到るまで、5名以上の方からアドバイスを頂けまして、
そのおかげで達成できました! 本当にありがとうございます!!シェルスクリプトを使ってpingでMySQLを確認して繋ぎにいく
下記の記事を参考にさせてもらいました ↓
http://docs.docker.jp/compose/startup-order.html
https://qiita.com/shiena/items/47437f4f7874bf70d664以下のようなシェルスクリプトを書きます
start.sh# !/bin/.sh # MySQLサーバーが起動するまでループで待機する until mysqladmin ping -h mysql --silent; do echo 'waiting for mysqld to be connectable...' sleep 2 done echo " go app is started!" exec app/build/apistart.shをwebのコンテナ内に入れます。今回はmntに入れました。
docker-compose.ymlを以下のように書き換えます。docker-compose.ymlvolumes: - ./api/scripts:/mnt entrypoint: mnt/start.shentrypointを.shにして、ループ処理をさせてから
1番最後のexecでコンテナを立ち上げるようにします。ただし、mysqladmin pingを実行する為には、
mysql-clientをインストールさせる必要があります。
Dockerfileにそれ用の記述を書き加えます。DockerfileRUN apk update \ && apk add mysql-client時は来た!
docker-compose up -d!docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api ./api/scripts:start.sh Up 0.0.0.0:8080->8080/tcpやった! 成功です!
GoのコードでMySQLの立ち上がりを確認してから接続するようにする
下記の記事を参考にさせてもらいました ↓
https://qiita.com/Bmouthf/items/d3cfdbee74caeda77e3f
https://kleinblog.net/docker-golang-mysql-min/上の方法で上手くいったのですが、その為だけにmysql-clientをインストールさせるのは
ちょっと... という事で、GoのコードでMySQLコンテナの立ち上がりを待つようにします!mysqlへの接続はそれ用の関数を呼び出して繋いでいたのですが、
少し書き加えてMySQLの立ち上がりを確認してから接続させるようにします。↓ mysqlの関数を呼び出して接続させている
main.godb := mysql.ConnectDB() db.LogMode(true) defer db.Close()importにgormを追加
import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" )以下のように書き足しました。
main.godb, connectcheck := gorm.Open("mysql") if connectcheck != nil { db = mysql.ConnectDB() db.LogMode(true) defer db.Close() }docker-compose up -d!
docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------- mysql .sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp redis .sh redis ... Up 0.0.0.0:6380->6380/tcp api app/build/api Up 0.0.0.0:8080->8080/tcpよっしゃー!
今回は、Dokcer + Go + MySQLコンテナの組み合わせでした。
同じような現象に悩まれてる方が何らかの参考になりましたら幸いです!著者自身、DockerもGoも全然分かっていない未熟者でして不備もあるかと思います。
批評、マサカリ大歓迎です!
何かありましたら、ぜひご意見アドバイス等等下さいませ〜!!