20200322のGoに関する記事は15件です。

勉強会情報をpush通知するslackアプリを作った

はじめに

勉強会の情報を定期的にpush通知してくれるslackアプリを作った。

こんな感じでpush通知したい勉強会の条件を設定して、

study.gif

条件に合致した勉強会が、設定した時刻に定期的に通知される。

スクリーンショット 2020-03-22 21.39.24.png

モチベーション

大きめの勉強会や人気の勉強会の開催を、qiitaのトレンド記事やはてブの記事に挙げられてるのを見て、後から知ることが結構ある。そういう人気の勉強会をpush通知してくれるような仕組みがあれば「やってたの知らなかった」ってことを減らせそうだと思ったので作ってみた。

アーキテクチャは以下の理由からherokuを使うことにした。
- サーバー代が無料
- dockerでのデプロイに対応してる
- スケジューラの仕組みもある

需要があるかわからないが、他の人もこの仕組みを使えるように環境構築の手順を書いていく。

構築手順

0) 事前にやっておくこと

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コマンドの設定をしていく。

  • コマンドを新規作成
    スクリーンショット_2020-03-22_18_42_14.png

  • コマンド実行時のAPI先を設定
    スクリーンショット_2020-03-22_18_42_29.png

2-2) Interactive Componentsの設定

勉強会の条件設定をするダイアログをsubmitした際に叩くAPI先の設定。

スクリーンショット_2020-03-22_18_53_12.png

3) スケジューリングの設定

herokuの画面に行って、スケジューラーのアドオンをクリック

スクリーンショット_2020-03-22_19_04_21.png

/main connpass slack を実行するジョブを作成する。実行間隔はお好みで。
スクリーンショット 2020-03-22 19.06.34.png

4) 勉強会通知の設定をする

作成したslashコマンドを実行し、開いた設定ダイアログ上で通知条件の設定をする。

スクリーンショット 2020-03-22 19.10.57.png
※注意
herokuの無料プランだと、30分たつと自動でプロセスが落とされる。
なので初回のslashコマンドはサーバー起動の時間が掛かるのでタイムアウトで失敗する。その後あまり間を空けずに再度slashコマンドを実行すればダイアログが開く。

5) 設定完了

ここまでやれば、herokuの環境変数で設定したwebHookURL宛てに、勉強会情報が定期的にポストされるようになる。

アーキテクチャ

golang、gin、docker、mysql、slack api、connpass api、herokuな感じで作っている。

一つのイメージでapiサーバーとしてもコマンド実行サーバーとしても起動させたかったため、起動時にフラグを渡すことでapiサーバー、コマンド実行サーバーが切り替わるようにしている。

また、ホットリロードのツールに air を使っている。
goでホットリロードといえば realize が使われているのをよく見かけるが、realizeはmodulesに対応していなかったり、最近メンテもされなくなったりと微妙な感じ。いいのが他にないのかなと思っていたが、最近はairが流行ってきてる?ぽい。

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

ループ中にある無名関数の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.

https://golang.org/cmd/vet/

公式ドキュメントから和訳すると、「vet コマンドは問題になりえそうなものを発見してくれるが、それが必ず問題であるわけではない。ガイダンスとして利用すること」ということ。

今回のケースでは機械的に問題を発見できた。

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

実用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/

サイトのトップページには挑戦的な文言が:
jolojo.PNG

残念ながら、まだ、『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/

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

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 を実現している、ということになります。

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

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 の要素に対してソートしたところ、シェルソート以外は処理が終わりませんでした。
今回取り上げたシェルソート以外にもクイックソートやマージソートなど高速なソートアルゴリズムがありますので、いつか追記できたらと思います。

さいごに

普段は標準パッケージで用意されているアルゴリズムを使うので、アルゴリズムについて考える機会がなかなかなかったのですが、実際に自分で考えながら実装してみると各ソートアルゴリズムがどういった仕組みでソートしているのか理解でき、学ぶことが多かったです。また、ベンチマークを計測することで、同じサイズの要素を扱う場合でも、パフォーマンスにかなりの差が出てくることがわかりました。今後も、アルゴリズムについても学んで行きたいと思いました。

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

趣味グラマーが5日で5個GitHubリポジトリを作ってみた

 趣味グラマーですが、GitHub リポジトリを5個作ってみました。せっかく作ったので誰かしらに見てもらえたら嬉しいなということと、初心者が GitHub を始める敷居を下げられたらいいなという思いで Qiita 投稿してみます。

※ ちなみに普段は運用系のお仕事をしているので、一応 IT 系ではあります。

GitHub のリポジトリってどうやって作ればいいんですか?

 わかります、、、その疑問。でも大丈夫!そこは親切な GitHub さんなので、新しくブラウザからリポジトリを作ると以下みたいな画面を出してくれます。画面のとおりにコマンドを実行すると、作ったリポジトリに push(アップロード)してくれます。

qiita1.png

そもそも Git がよくわからない!という人は、ひとまず git init 等のコマンドを、プロダクトを作ったディレクトリ上で実行してみると、イメージがつかめると思います。そのうえで、Git は別途勉強されたほうがいいかもしれません(私もadd / commit / pushしかわかりませんが、、、)。

なんで 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.123

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

そのときに使うライブラリが 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 ツールを作りやすかったり、何かとシンプルで開発に集中できる点がいいと思いました。趣味グラマーにおすすめだと思います。

以上、ここまで読んでいただき、ありがとうございました。作り出すと意外とハマりましたので、また色々量産していきたいと思います。

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

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.js
const 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 パッケージの利用で解決できます。

また忘れてハマりそうなのでメモしておいた。

参考

Next.js 公式ドキュメント日本語翻訳プロジェクト - カスタムサーバーとルーティング

Nextで特定のAPI リクエストをproxyする方法 - Qiita

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

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

と出力される。

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

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の情報を用いて処理が実行されます。

コード編

github

mux.go
package 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の記載を用いる形で解説を行なっていきたいと思います。

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

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

W3sDesign_Builder_Design_Pattern_UML.jpg

UML class diagram

2880px-Builder_UML_class_diagram.svg-2.png
(以上、ウィキペディア(Wikipedia)より引用)

■ "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ブラウザで、見た目を確認してみると、こんな感じでした。
tablehtml.png

■ サンプルプログラムの詳細

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

type builder interface {
    makeTitle(title string)
    makeString(str string)
    makeItems(items []string)
    close()
}

(2) ConcreteBuilder(具体的建築者)の役

ConcreteBuilder役は、Builder役のインタフェースを実装しているクラスです。実際のインスタンス作成で呼び出されるメソッドが、ここで定義されます。また、最終的にできた結果を得るためのメソッドが用意されます。
サンプルプログラムでは、TextBuilder構造体や、HTMLBuilder構造体が、この役を努めます。

builder/text_builder.go
package 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.go
package 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.go
package 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.go
package 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

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

パケットフィルタの仕組みを理解しつつGoで特定のプロトコルパケットを取得(フィルタ)する

理解しておくと良い前提知識

  • カーネル空間/ユーザー空間の違い
  • ネットワークの基礎
  • ソケットプログラミングの基礎

環境

$ go version
go version go1.13.5 linux/amd64

パケットフィルタの仕組み

特定のプロトコルパケットをフィルタする前にパケットフィルタの仕組みについて記す。
※cBPFとeBPFの違い、現在のBPFの機能がパケットフィルタだけではないとか、BPFの歴史/変遷などについてはこの記事では触れていませんのでご了承ください:bow:

例として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を用いて実装されているようです!
libpcapPCAP_APIを呼び出して使用しています。

最後に

理解を深めるために実装を読んだり、BPFの歴史について調べたり、なかなか面白みがありました!
間違っている所などもあるかもしれませんが、その時はご教授いただけると幸いです。:bow:

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

goenvのコマンドメモ

pyenvと似たような感じですね。

command 内容
goenv commands 使用可能なgoenvコマンドを全て表示
goenv completions --completeを指定して呼び出すことにより、自身および他のコマンドの自動補完を提供します。
goenv exec 指定した仮想環境のGoバージョンでファイルを実行する。
例)goenv exec go run main.go
goenv 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.0
goenv version アクティブなGoバージョンを一覧表示する。
goenv --version goenv自体のバージョンを表示する。
goenv version-file Goバージョンを設定しているファイルを一覧表示する。
goenv version-file-read 指定されたgo-versionファイルが存在する場合、そのファイルを読み取る。
例)goenv version-file-read ./go-version
goenv version-file-write go-versionファイルのバージョンを書き換える。
goenv version-name 現在のGoバージョンを表示
goenv version-origin Goバージョンの設定方法を表示する。
goenv versions 全てのGoバージョンを一覧表示し、現在のアクティブバージョンの横には「*」を表示する。
goenv whence 指定されたコマンドがインストールされたすべてのGoバージョンを一覧表示。
goenv which 指定したコマンドを実行したときにgoenvが呼び出す実行可能ファイルのフルパスを表示する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goenvのコマンドチートシート

pyenvと似たような感じですね。

コマンドチートシート

command 内容
goenv commands 使用可能なgoenvコマンドを全て表示
goenv completions --completeを指定して呼び出すことにより、自身および他のコマンドの自動補完を提供します。
goenv exec 指定した仮想環境のGoバージョンでファイルを実行する。
例)goenv exec go run main.go
goenv 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.0
goenv version アクティブなGoバージョンを一覧表示する。
goenv --version goenv自体のバージョンを表示する。
goenv version-file Goバージョンを設定しているファイルを一覧表示する。
goenv version-file-read 指定されたgo-versionファイルが存在する場合、そのファイルを読み取る。
例)goenv version-file-read ./go-version
goenv 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_profile

goenvの最新バージョン取得

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

めんどくさい(;ω;)

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

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 2

webのコンテナが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/api

start.shをwebのコンテナ内に入れます。今回はmntに入れました。
docker-compose.ymlを以下のように書き換えます。

docker-compose.yml
volumes:
      - ./api/scripts:/mnt
    entrypoint: mnt/start.sh

entrypointを.shにして、ループ処理をさせてから
1番最後のexecでコンテナを立ち上げるようにします。

ただし、mysqladmin pingを実行する為には、
mysql-clientをインストールさせる必要があります。
Dockerfileにそれ用の記述を書き加えます。

Dockerfile
RUN 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.go
db := mysql.ConnectDB()
    db.LogMode(true)
    defer db.Close()

importにgormを追加

import (
"github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

以下のように書き足しました。

main.go
db, 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も全然分かっていない未熟者でして不備もあるかと思います。
批評、マサカリ大歓迎です!
何かありましたら、ぜひご意見アドバイス等等下さいませ〜!!

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

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 2

webのコンテナが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/api

start.shをwebのコンテナ内に入れます。今回はmntに入れました。
docker-compose.ymlを以下のように書き換えます。

docker-compose.yml
volumes:
      - ./api/scripts:/mnt
    entrypoint: mnt/start.sh

entrypointを.shにして、ループ処理をさせてから
1番最後のexecでコンテナを立ち上げるようにします。

ただし、mysqladmin pingを実行する為には、
mysql-clientをインストールさせる必要があります。
Dockerfileにそれ用の記述を書き加えます。

Dockerfile
RUN 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.go
db := mysql.ConnectDB()
    db.LogMode(true)
    defer db.Close()

importにgormを追加

import (
"github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

以下のように書き足しました。

main.go
db, 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も全然分かっていない未熟者でして不備もあるかと思います。
批評、マサカリ大歓迎です!
何かありましたら、ぜひご意見アドバイス等等下さいませ〜!!

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