20191215のGoに関する記事は30件です。

UnityとGo言語でAPI通信する

はじめに

ゲーム開発エンジン「Unity」と、Googleにより開発されたプログラミング言語「Go」を用いて、凄くシンプルなREST APIを実装したいと思います。

Unityについて:https://unity.com/ja
Goについて:https://golang.org/

開発環境

  • MacBook Pro (15-inch, 2018)
  • macOS Catalina
  • Unity 2019.1.1f1 Personal
  • Go 1.13.5 darwin/amd64

システム構造

Unityをクライアントサイド、APIサーバ(Goで実装)をサーバサイドとして作りました。

内容

サーバサイドで保存されているデータに対して、Unity側からデータのID(一意なもの)を指定して対象データを取得します。データに関しては、予めいくつか用意しておきます。

今回はDBを使わないので、オンメモリのデータストア(リストを使います)に格納しておこうと思います。なので、サーバを停止させるとデータは吹き飛びますw

実装

サーバサイド

まずサーバサイドから実装していきます。
といっても、Goには便利なパッケージがたくさんあり、かつネットには参考になる情報が大量にあるため、それらをめっちゃ活用しました。

コードは以下の通りです。

package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/ant0ine/go-json-rest/rest"
)

type Monster struct {
    ID   int
    Name string
}

// オンメモリのデータストア.
var dataStore = map[int]*Monster{}

func main() {
    api := rest.NewApi()
    api.Use(rest.DefaultDevStack...)

    router, err := rest.MakeRouter(
        rest.Get("/getAllData", GetAllData),
        rest.Post("/postData", PostData),
        rest.Get("/getData/:id", GetData),
    )
    if err != nil {
        log.Fatal(err)
    }
    api.SetApp(router)
    log.Printf("Server Started.")

    // APIサーバを起動.
    log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

// データを新しく作成する.
func PostData(w rest.ResponseWriter, r *rest.Request) {
    monster := Monster{}
    err := r.DecodeJsonPayload(&monster)

    if err != nil {
        rest.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    dataStore[monster.ID] = &monster

    w.WriteJson(&monster)
}

// 指定IDのデータを取得する.
func GetData(w rest.ResponseWriter, r *rest.Request) {
    id, _ := strconv.Atoi(r.PathParam("id"))

    var monster *Monster
    if dataStore[id] != nil {
        monster = &Monster{}
        *monster = *dataStore[id]
    }

    if monster == nil {
        rest.NotFound(w, r)
        return
    }

    w.WriteJson(monster)
}

// 全データを取得する.
func GetAllData(w rest.ResponseWriter, r *rest.Request) {
    allData := make([]Monster, len(dataStore))

    i := 0
    for _, data := range dataStore {
        allData[i] = *data
        i++
    }
    w.WriteJson(&allData)
}

こちらのコードを作成するために、以下の資料を大変参考にさせていただきました!
golangでREST APIをやってみた①

今回は、簡単にRESTfulなAPIサーバを構築することができる以下のパッケージを使っています。
Go-Json-Rest

コードに関して大事なところは、以下の箇所かと思います。

    router, err := rest.MakeRouter(
        rest.Get("/getAllData", GetAllData),
        rest.Post("/postData", PostData),
        rest.Get("/getData/:id", GetData),
    )

ここでは、go-json-restのrestパッケージに実装されているMakeRouterを使って、ルーティングパスを3つ設定しています。それぞれ以下のハンドラと結びつきます。

  • /getAllData
    • GetAllData : データストアに格納されている全データを取得するハンドラ.
  • /postData
    • PostData : データストアに新しくデータを格納するハンドラ.
  • /getData/:id
    • GetData : 指定idを持つデータを取得するハンドラ.

今後新しくルーティングを作成していきたい場合は、MakeRouterにHTTPリクエストメソッドに対して、ルーティングパスとハンドラを結び付けて定義してあげればいいということですね。

クライアントサイド

サーバサイドに対して処理を要求するクライアントサイドの実装を行います。
クライアントはUnityを使うので、最初はEditorで以下の画面を作成しました。

unity_editor.png

Hierarchyビューを見ていただくと、InputAreaOutputAreaというオブジェクトがあります。これらはそれぞれ、Gameビューにおける下側の要素と上側の要素を指しています。

また、とても重要なのがHierarchyビューのClientオブジェクトです。これはEmpty Objectなのですが、以下のスクリプトがアタッチされています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class ApiClient : MonoBehaviour
{
    public InputField inputField;
    public Text outputText;
    public Image monsterImage;

    private string baseURL = "http://127.0.0.1:8080";

    public void GetDataFromAPIServer()
    {
        StartCoroutine(GetData());
    }

    IEnumerator GetData()
    {
        string url = baseURL + "/getData/" + int.Parse(inputField.text);
        UnityWebRequest request = UnityWebRequest.Get(url);
        yield return request.SendWebRequest();

        if (request.isNetworkError)
        {
            Debug.Log(request.error);
        }
        else
        {
            if(request.responseCode == 200)
            {
                string rawData = request.downloadHandler.text;
                MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);

                string imgPath = "monster_" + monsterData.ID;

                outputText.text = monsterData.Name;
                monsterImage.sprite = Resources.Load<Sprite>(imgPath);
            }
        }
    }
}

上記コードは以下の資料を参考にさせていただきました!
UnityでHTTPに接続する

このスクリプトでは、GetDataFromAPIServer()メソッドが実行されるとGetData()が動きます。
GetData()内で、リクエスト対象のURLを作成後、以下の箇所でサーバ側への送信とレスポンス受付けを行います。

        yield return request.SendWebRequest();

レスポンスのステータスコードが200ならば、返ってきたJSONデータ(string型)をプログラム内の変数に格納します。
その後、JSONデータをパースして対応するクラスインスタンスに情報を入れていきます。以下の箇所です。

                MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);

このMonsterDataクラスですが、以下のようにしました。データを一意に保つためのIDと、データの名前Nameを持ちます。なので返却されるJSONは、この構造を持っている必要があります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class MonsterData
{
    public int ID;
    public string Name;
}

上のソースコード(ApiClientクラスのほう)をClientオブジェクトにアタッチしたのが、以下の状態です。

client_set.png

いくつかInspectorビューから紐づけているコンポーネントがあります。

  • InputField
    • クライアント側から指定するIDを入力するフィールド.
  • OutputText
    • サーバ側から返ってきたデータのName変数を格納する.
  • MonsterImage
    • 取得データのIDを使ってクライアント側に置いてある対応する画像をセットする.

これらは以下のように紐づけています。

client_link.png

最後に、今回はGUIのボタンが押されたらサーバ側に処理を依頼するようにしようと思ったので、Hierarchyビュー上に置かれているボタンオブジェクトに対して、ClientオブジェクトにアタッチしたApiClientクラスが持つGetDataFromAPIServer()メソッドを割り当てています。

client_btn.png

これで準備OKです!

動作確認

まずAPIサーバを立てておきます。以下の状態で待機します。

$ go run server.go
2019/12/15 23:25:10 Server Started.

ターミナルで別タブを開いて、いくつかデータを格納(POST)しておきます。

$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 1, "Name": "スライム"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 39

{
  "ID": 1,
  "Name": "スライム"
}%

$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 2, "Name": "ソルジャー"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: yyyyy
Content-Length: 42

{
  "ID": 2,
  "Name": "ソルジャー"
}%

以下のREST APIを叩くと、結果が全件返ってきてくれました!

$ curl -i http://127.0.0.1:8080/getAllData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 103

[
  {
    "ID": 1,
    "Name": "スライム"
  },
  {
    "ID": 2,
    "Name": "ソルジャー"
  }
]%

次にクライアントサイドを起動して動作をみてみます。
まず、IDが1のデータを取得してみます。(格納しておいたデータでいうと、スライムが該当します)

以下のように入力して「データを取得」を押すと、、、

execute_1.png

無事データを取得して、画面に反映されました!(画像データはあらかじめ自分で作ったものを参照してますw)

result_1.png

同じように、IDを2にしてやってみます。以下のようにして「データを取得」を押すと、、、

execute_2.png

IDが2のデータ(ソルジャー)が取得できました!

result_2.png

終わりに

今回、UnityとGoを用いて簡単なAPI通信を実装してみました。
今後は今回学んだことも踏まえながら、より発展的な内容にも取り組んでいけたらなと思っています!

ありがとうございました!

参考

golangでREST APIをやってみた①
UnityでHTTPに接続する
UnityWebRequest
UnityWebRequestの使い方【Unity】
Go 言語の値レシーバとポインタレシーバ

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

GDG DevFest Tokyo 2019 観戦記

はじめに

12/14(土)開催のGDG DevFest Tokyo 2019行ってきました。
公式サイト

この記事は個人の備忘録になります。

参加したセッション

ちょっと大きくて見にくいですが、全体のセッションはこんな感じでした。
公式ページのガイド

今回は、普段業務で使うものはもちろん、
普段触れていなくても来年使いそうな技術のキャッチアップ目的で参加してきました。

11:30~ 遅刻+企業ブース周り

前日の忘年会で夜遅くまで飲んでいて、当日朝も油断して 遅刻して参加しました。
開会式は参加したかったんですが、のんびりする時間が出来たのでのんびり企業ブースを周り、
ノベルティグッズを収集しました。
(CircleCI Tシャツ いただきました)

LINEの企業ブースでも結構ゆっくり説明を聞くことができて、いくつか自社公開のOSSを
紹介してもらいました。
https://github.com/line/feature-flag-android
https://github.com/line/lich

feature-flagはこの後のセッションでも語られていた & DroidKaigi2019でも
セッションテーマになっていて、非常に興味を持ったテーマだったので、
実務でも使えるか試してみたいです。
lichはまだあまり実務に投入するイメージが沸かない(そのプロジェクト特有になりそうで)ですが、
結局車輪の再発明になるから後々これにたどり着くことになるのかな...
(間違ってる可能性大ですが、聞いたイメージはAndroid Jetpackのutility)

スピーチに多くの方が参加していたので、お昼ご飯も並ばずに食べれたのは良かったです。
(セッションが終わった時間くらいから行列出来てたので...)

12:20~ パネルディスカッション

えーじさん、及川さん、田中さんの3名によるキャリアについてのディスカッションでした。
3人ともなかなかインパクトのある経歴でした(そのまま、参考にはならないよね..)

印象に残ったのは、

ソフトウェアファーストっていう本に書いてあるので~(及川さん)
ソフトウェアファースト

実際に、ソフトウェアファースト片手にトークしてたので。
「宣伝すごいな〜笑」と思いましたが、本の中身の一部を使いながら説明をしてもらって、
実際中身はすごくいいんじゃないかと思いました!
今、カイゼンジャーニー読んでるから、次で..

14:10~ Goの10年の道のりとその変遷

資料は見つからず...印象に残ったのは...
- 言語の祖先にALGOLというのがある
- ALGOLからアメリカ(C)かヨーロッパ(PASCAL)かでいくつかの言語の分派が生まれた
- それぞれの分派のエキスパートが作ったのがGO
- 元はC++のビルドが遅く、言語が大きくなりすぎたため
- GOはプログラミングするための重要な要素に絞って成長した

話についていけない場面もいくらかありましたが、最後の部分が今GOが流行に乗ってる理由なのかなぁって思いました..
学習環境( https://go.dev/ ) もあるので、来年は手を付けてみたいです。

15:10~ 来年に備えるために Android の知識を網羅する

資料
(qiitaでspeakerdeckって埋め込みできないんですかね...)

Android Jetpack から Kotlin まで紹介が広く、個人的には一番刺激を頂いたセッションでした。
薄々感じてましたが、MVVM + Coroutines は今後圧倒的に主流になりそうですね..
MVVM と 何もなし(良くはないけど、時間がないとかで導入出来ない場合) のどちらかが増える予感...

以下、メモ。

Android10 highlights

DarkTheme / GestureNavigation / LocationControls

  • LocationControls -> BackGroundのLocationはAndroid10で必要になる
  • GestureNavigation -> Android10 NavigationBarの透過
  • マルチウィンドウ -> 割と以前からある機能、両方は動画流さないとかの制御が必要。

AndroidStudio4.0 highlights

  • Jetpack Compose -> 後述
  • Motion Editor -> AnimationのGUI実装が可能になる(Stableじゃない)
  • MultiPreview -> 開発者に取っては嬉しい機能

Kotlin

  • 2019 Kotlin First
  • Coroutines RxJava -> Coroutines
  • 2020年は Kotlin Flow で Rxの置き換えが主流になるかも

Coroutines メモ

  • 軽量スレッド
  • cancelが簡単
  • 失敗時のハンドリギングが簡単
  • launch = Coroutines Builder
  • launch の戻り値は Job
  • Job同士は親子関係を作ることができる
  • 親のキャンセルが子供にも伝播される
  • launch は Scope の中で呼び出さないといけない
  • Dispatchers.Defaultがいい感じにしてくれる
  • UIはMain
  • DB、ネットワーキングはIO
  • scopeにスレッドも与えることができる

(個人的に Coroutines は今年実務で使い切れなかったので、来年は使い倒したい)

Architecture(MVVM)

  • Googleの推奨アーキテクチャとしてMVVMが取り上げられた
  • MVVMを設計しやすくするためにjetpack
  • Model = ViewModel with LiveData
  • Repository = LocalSource / RemoteDataSource

Jetpack

  • AppCompat = 古いOSを吸収してくれる
  • Android KTX = Kotlinの拡張機能Set
  • AAC(AndroidArchitectureComponents) = 堅牢でテストと麺店アンスが簡単なアプリの設計を支援するライブラリ

Navigation

  • 画面遷移を先鋭を視覚的に実装できる(Story Board)
  • XMLだから自分で定義、修正することもできる
  • 画面間のデータを型安全に渡すことができる

ViewModel+LiveData

  • ライフサイクルを意識した方法でUI関連のデータ田を保存及び管理できる
  • ViewModel - Fragment間のデータ共有、画面回転で破棄されない
  • LiveData - Fragment~(メモ取れず...)
  • ViewModelのデータをObserveすることが一般的。ViewModelでCoroutinesやFlowが登場する

Retrofit+OKHttp

  • OS5.0以上じゃないと動かないとようになっている
  • Retrofitは GraphQLには対応していない

DI

  • Dagger / Koin / Kodein --- Daggerは難しい

(GDEの方が難しいと言われると、深みが強い...速いは魅力的だけど..)

Test Lab

  • クラウド上の端末でテストできるサービス
  • instrumentの実行も出来る
  • Appiumとかには対応していないし、するつもりもないらしい

16:05~ 休憩

3時間くらい講演を聴き続けたので、休憩&PCの充電...
(充電し忘れて参加したので)

後から、これ -> Yearly Web 2019 に参加すれば良かったと気付きましたが..
ハンズラボも周ってみましたが、途中参加むりっぽかったので、すごすごとその場を後に..

17:00~ Flutter Overview

資料を熱望します...
以下、資料の写です。

自分に取って、重要な一文は、
「FlutterはUIフレームワークである。UIをたくさん、早く作りたい時に有利」です。
UIをFlutterに任せるので、既存のiOSデザインに寄せたい時は選択肢に入れない方がいいと思いました。
新規開発でマテリアルデザインを進めれるなら、有力だと思いました。

クロスプラットフォーム、アプリを作るためのUIフレームワーク
Flutter Interactにてアナウンス v1.12 Stable
Flutter 2年半(2017/5 αリリース)

<Flutterの特徴>
Dart言語
クロスプラットフォーム
宣言的UI
高い開発効率(ホットリロード)
高い実行パフォーマンス

IDE is VSCode

高い実行パフォーマンス
-> ネイティブ・コンパイル => ARM(スマホは全部これ) x86

★★★Flutterの大きな特徴★★★
GPU活用
 Skia(2D Graphicsのライブラリ)

 Dart
  version 2.7(最新は)
  Static typed(コンパイル型言語に近い特徴、文法もJavaに似ている)
  JIT + VM
  AOT
  Javascriptの置き換えを目指した

Flutter向きなアプリとは?
UI(部品や画面)の多いもの
 FlutterのUIの作りやすさの恩恵が大きく受けられる
 ex. MediaNewsのアプリ、SNS etc...

向かないもの
少数画面、機能特化のもの
 カメラ、動画、地図...など?
 ゲーム
  作れはするが、ゲーム特化のフレームワークではない

Flutterの検討シーン
新規アプリの開発(既存のコード資産がない場合)
プロトタイプ

既存アプリへの追加
Add-to-App
Flutter部分とネイティブ部分は別れてしまう、ネイティブで実装したコードの利用/流用がやりにくい

Flutterむきなアプリとは?
 FlutterはUIフレームワークである。UIをたくさん、早く作りたい時に有利。

Flutterのアーキテクチャ
フルスタック
 レイアウト計算から画面描画までFlutterがやる
ネイティブのUIフレームワークを利用しない

Flutterのアーキテクチャ
Pros: プラットフォーム間の違いが小さい
Cons: ネイティブとの混在が難しい

ネイティブ連携
プライグイン機構
DartからAndroid/iOSを呼び出せる。
デバイス依存のもの(カメラ、GPSなど)を使うことができる

flutter.dev/showcase
in Japan -> CARTUNE

Web
アプリをそのままWebに持っていく時に使う
インタラクティブ性が高く、グラフィックを多用したもの
HTMLのドキュメントの構造化の置き換えではない(SEOには対応していない、難しそう)

まとめ

こういう記事書くなら(当初そんなに書こうと思ってなくて..)予め写真もっと撮影しておけば良かったです。

とはいえ、初めて参加したイベントでしたが、自分の業務や興味とも関連していて満足度高かったです!
場所が大学だったので、懐かしい気持ちになれました!

今年のまとめを聞いてましたが、もう追い切れないくらい知りたいこと、興味あることが多いですね。
イベントでその道のトップクラスの人から知見や情報を一気に得ることが出来たのは、
このイベントに参加して良かったと思いました。

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

MacにGoをインストールする

とりあえずGoを始めようとして、A Tour of Go をやってみました。
が、その事前準備のインストールで躓きました…。
これから同じような経験をする方がいらっしゃるのではないかと思い、備忘録も兼ねて投稿します。
よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

環境

  • macOS
  • Homebrew
  • Go (v1.13.4)

やってみたこと

インストール

とりあえず、HomebrewでGoをインストールします。

$ brew install go

確認

バージョン確認

インストールが完了したら、バージョンの確認をします。

$ go version
go version go1.13.4 darwin/amd64

実行環境の確認

次に、公式のスタートガイド から、実行環境のテストを進めます。

# 実行環境のディレクトリを作成
$ mkdir -p ~/go/src/hello

# 作成したディレクトリへ移動
$ cd ~/go/src/hello

# テスト用のファイルを作成
$ touch hello.go

# テスト用のファイルを編集
$ vi hello.go

hello.goの中身は以下のように編集します。

※もしVimコマンドの操作が分からないという方がいれば、下記のリンクを参照してください。

hello.go
package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

編集が完了したら、ビルドしてバイナリファイルを生成します。

$ go build
hello.go:3:8: cannot find package "fmt" in any of:
        /Users/username/go/src/fmt (from $GOROOT)
        /usr/local/go/src/fmt (from $GOPATH)
package hello
        imports runtime: cannot find package "runtime" in any of:
        /Users/username/go/src/runtime (from $GOROOT)
        /usr/local/go/src/runtime (from $GOPATH)

...........( ̄ω ̄;)エートォ...

何か様子がおかしいぞ。

調べてみる

Homebrewでインストールした場合、/usr/local/Cellar/go/ 配下にバージョン毎にディレクトリが作成されるはずです。
そうすると、エラーの中にある /usr/local/go/src/fmt (from $GOPATH) ってそもそも存在しないのでは?
となったので、その辺を調べてみます。

$ cd /usr/local/go
bash: cd: /usr/local/go: No such file or directory

やっぱりない!!こいつか!!!!

(゜∇゜)(。_。)(゜∇゜)(。_。)ウンウン♪

環境変数を確認

go env でGo言語関連の環境変数を確認できるので、一応確認します。

$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/username/Library/Caches/go-build"
GOENV="/Users/username/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/usr/local/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/username/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/username/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
...

やはり、GOPATH が存在しないディレクトリを指定していますね。
この辺を変更すれば解決しそうです。

修正する

環境変数の変更

下記コマンドで変更が必要な環境変数を追加します。

# 環境変数の追加
$ echo 'export GOPATH=$HOME/go' >> ~/.bash_profile
$ echo 'export GOROOT=/usr/local/Cellar/go/1.13.4/libexec' >> ~/.bash_profile
$ echo 'export GOTOOLDIR=/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64' >> ~/.bash_profile
$ echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bash_profile

# bash_profileを更新
$ source ~/.bash_profile

変更の反映を確認

GOPATH GOROOT GOTOOLDIR の3箇所が正常に修正されていることを確認します。

$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/username/Library/Caches/go-build"
GOENV="/Users/username/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/username/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/Cellar/go/1.13.4/libexec"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
...

それぞれ、下記のように変更されていることを確認してください。

GOPATH="/Users/username/go"
GOROOT="/usr/local/Cellar/go/1.13.4/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64"

再び実行してみる

修正できたので、再度ビルドしてみます。

# 作成したディレクトリへ移動
$ cd ~/go/src/hello

# ビルドしてバイナリファイルを生成
$ go build

特に問題がなければ、エラーが出ずに完了します。
念の為、問題なくビルドできたことを確認するために、バイナリファイルを実行します。

$ ./hello
hello, world

上記のように hello, world と表示されれば完了です。

これで無事、Goのインストールと実行環境のテストまでが完了しました!
今後は、A Tour of Go を進めるもよし、プロジェクトを作成してガシガシ開発を進めるもよし、
素敵なGoライフを楽しんでください!!

参考

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

動画ファイルをWeb経由で取得して、そのままCloud Storageにアップロード

お題

表題の通り。AWSのS3に署名付きURLでアクセスして取得した動画ファイルをそのままCloud Storageにアップロードする要件があったので、事前にちょっとお試しで実装してみる。
今手元に持ってるのがGCPのアカウントだけなので、「S3から署名付きURLで」という部分は端折って、GCPのCloud Storageに別バケット用意してそこに取得対象の動画ファイルを置いて(パーミッション付けて)おくことにする。

前提

  • GCPの何たるかについては説明しない。
  • gcloudコマンドの使い方やローカルへのクレデンシャル取得方法などは説明しない。

開発環境

# OS - Linux(Ubuntu)

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"

# 言語 - Go

$ go version
go version go1.13.3 linux/amd64

# パッケージマネージャ - Go Modules

# IDE - Goland

GoLand 2019.2.5
Build #GO-192.7142.48, built on November 8, 2019

実践

適当なバケット内にサンプル動画ファイルを準備

※本来の要件的にはAWSのS3に署名付きURLでアクセスだったけど、趣旨としては同じなので。
screenshot-console.cloud.google.com-2019.12.15-22_05_30.png
バケット内のオブジェクトに対する(誰でも参照可能にする)公開アクセス制御については以下参照。
https://qiita.com/sky0621/items/60c654fd1817bbf085ae#08なので一般公開してみる

上記からダウンロードした動画ファイル(sample.mp4)のアップロード先も作っておく。
screenshot-console.cloud.google.com-2019.12.15-22_09_08.png
※アップロード前なので当然バケット内は空。

プログラム

プロジェクト全体は下記。
https://github.com/sky0621/tips-go/tree/568b40bd824d9e0f7bd5eb8bdce07565555768bb/try/mp4_download_and_upload

[cmd/main.go]
package main

import (
    "context"
    "flag"
    "io"
    "log"
    "net/http"

    "github.com/pkg/errors"

    "google.golang.org/api/option"

    "cloud.google.com/go/storage"
)

func main() {
    if err := exec(); err != nil {
        log.Fatal(err)
    }
}

func exec() (e error) {
    credentialPath := flag.String("c", "credential file path", "~/keyfile/gcp.json")
    mpath := flag.String("m", "download url path", "http://localhost:9000/sample.mp4")
    writeBucket := flag.String("b", "write bucket", "http://localhost:7000/bucket")
    objectName := flag.String("o", "object name", "sample.mp4")
    flag.Parse()

    res, err := http.Get(*mpath)
    if err != nil {
        return err
    }
    defer func() {
        e = close(res.Body, e)
    }()

    ctx := context.Background()
    client, err := storage.NewClient(ctx, option.WithCredentialsFile(*credentialPath))
    if err != nil {
        return err
    }
    wc := client.Bucket(*writeBucket).Object(*objectName).NewWriter(ctx)
    if _, err = io.Copy(wc, res.Body); err != nil {
        return err
    }
    defer func() {
        e = closeWriter(wc, e)
    }()

    return nil
}

func close(c io.Closer, e error) error {
    if c == nil {
        return e
    }
    err := c.Close()
    if err == nil {
        return e
    }
    if e == nil {
        return err
    }
    return errors.Wrap(err, e.Error())
}

func closeWriter(w *storage.Writer, e error) error {
    if w == nil {
        return e
    }
    err := w.Close()
    if err == nil {
        return e
    }
    if e == nil {
        return err
    }
    return errors.Wrap(err, e.Error())
}

プチ解説

    res, err := http.Get(*mpath)

欲しいものがテキストだろうとバイナリだろうと、これだけで事足りる。

    ctx := context.Background()
    client, err := storage.NewClient(ctx, option.WithCredentialsFile(*credentialPath))

あらかじめローカルから自分のGCPプロジェクトのCloud Storageにアクセス可能なクレデンシャルを作ってあるので、そのファイルをオプションとして渡す。

    wc := client.Bucket(*writeBucket).Object(*objectName).NewWriter(ctx)
    if _, err = io.Copy(wc, res.Body); err != nil {
        return err
    }

どのバケットにどういうオブジェクト名で書き込むかを指定したら、あとはhttp.Get結果のレスポンスボディを渡すだけ。
res.Bodyio.Readerインタフェースを実装しているゆえ。

試行結果

アプリ起動

$ go run main.go -c=/home/sky0621/gcp/key_file/credential.json -m=https://storage.googleapis.com/download-from-03fa-481a-a325/sample.mp4 -b=upload-to-9618-489b-b030 -o=sample.mp4

結果

アップロード先バケット「upload-to-9618-489b-b030」に動画がアップロードされている。
screenshot-console.cloud.google.com-2019.12.15-22_13_56.png

まとめ

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

Go langでfwを使わずにcrudのappを作成しました 2

part 1
https://qiita.com/dossy/items/721db0ab74afd8e78599
さぁ、part 1でhttp requestとdb 接続ができるようになりましたので、本格的にcrudの処理を行いましょう。
main.goにそれぞれのrequestが呼べるように記述しておきましょう。

main.go
http.HandleFunc("/new", handle.New)
http.HandleFunc("/create", handle.Create)
http.HandleFunc("/edit", handle.Edit)
http.HandleFunc("/update", handle.Update)
http.HandleFunc("/delete", handle.Delete)

createを定義

createするときは
formが必要なのでformがあるnew.htmlを呼び出します。これは/newのリクエストを送ってhtmlを表示させます。

handle/handle.go
func New(w http.ResponseWriter, r *http.Request) {

    tem, _ := template.ParseFiles("new.html")
    tem.Execute(w, "")
//どうやら、Executeは引数を2つ必要とするみたいです。
//new.htmlが呼び出せるように追記。


func Create(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        r.ParseForm() // Bodyデータを扱うには、事前にパースを行う

        // Formデータを取得
        form := r.PostForm
        body := form["body"][0]
        image := form["image"][0] //form is map value is []string of slice
        serv.Create(body, image) //insert to db

        http.Redirect(w, r, "/", http.StatusMovedPermanently) //code 301
       //rootにredirectさせてます
    }
}
}
new.html
<form action="/create" method="POST">
      <div>
        <label for="say">Let's write a comment!</label>
        <input class="textarea" name="body" id="say" placeholder="input commnet" maxlength="100" required>
      </div>
      <div>
        <label for="to">imageのurlを貼り付け</label>
        <input class="textarea" name="image" id="to" placeholder="input image" required>
      </div>
      <div>
        <button>Send my greetings</button>
      </div>
    </form>

serv/data.go
func Create(body string, image string) {
    db := database.ConnectDB()
    defer db.Close()

    create_time := time.Now() //time.Time型で現時刻を取得
    rows, err := db.Prepare("INSERT INTO posts(body,image,created_at,updated_at) VALUES(?,?,?,?)")
    if err != nil {
        log.Fatal(err)
    }
    rows.Exec(body, image, create_time, create_time)
    // Exec()にプリペアードステートメントを指定してSQLを実行する

}

formから情報を送れるようになりました。formに情報を入れてsubmitすればpost のリクエストがきます。

(注)この辺りから、ゴリゴリの自己実装です。あくまで参考程度でお願いします。何かいい方法などありましたら、コメントまで!!

さて、create funcではpost methodかどうかを判別するために条件分岐を入れています。

*http.Requestの情報をもつ?? rに対してparseをかけて情報を読める状態にします。
その後、formの中身を取り出しますが、どうやら中身はmapとsliceの二重構造になっているらしく、form["image"][0]みたいな取り方になりました。
そして、serv.Create funcが動いて取得したformの情報をinsertを使って、dbに入れ込みます。

edit deleteを定義

editとdeleteがかなり詰まったのを覚えています。
なぜなら、どのidの情報を編集、削除するのかを機械にどうやって渡すかがかなり悩ましかったですね。
httpのリクエストには/edit/2みたいなidをつけて渡すことができなかったんです。

探しあぐねましたが、しかし、見つけました。?をつけるとリクエストに値をつけれるみたいです。
http://cai.cs.shinshu-u.ac.jp/sugsi/Lecture/php/http/2-arg.html

handle/handle.go
func Edit(w http.ResponseWriter, r *http.Request) {
    tem, _ := template.ParseFiles("edit.html")
    params := r.URL.Query()       //Queryで取得 map
    e := serv.Edit(Getid(params)) //return []serv.Vertex
    tem.Execute(w, e[0])
//newとほぼ一緒

func Update(w http.ResponseWriter, r *http.Request) {
    if r.PostFormValue("_method") == "PUT" {
        r.ParseForm()

        form := r.PostForm
        params := r.URL.Query()
        id := Getid(params)
        body := form["body"][0]
        image := form["image"][0]

        serv.Update(id, body, image)
        http.Redirect(w, r, "/", http.StatusMovedPermanently)
    }
}

func Delete(w http.ResponseWriter, r *http.Request) {
    if r.Method == "DELETE" {
        params := r.URL.Query()    //to use Getid
        serv.Delete(Getid(params)) //use sql delete func

        http.Redirect(w, r, "/", http.StatusMovedPermanently)
    }
}

func Getid(params url.Values) int { //get id from httprequest
    var num string
    for k, _ := range params { //get map_key
        num = k
    }

    i, _ := strconv.Atoi(num) //string to int
    return i
}

}

苦労したのが、
params := r.URL.Query()
id := Getid(params)

この部分でしょうか。

rのURLの情報をQueryで取得。ただ、そのままだと扱えないのでidの情報だけを取るGetid funcを作成しました。
urlの情報はurl.Valuesという型だそうです。

https://golang.org/pkg/net/url/
http://golang.jp/pkg/strconv
https://qiita.com/nakabonne/items/2720bac7027115b1d004

暇があったらまた書きます。

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

Go langでfwを使わずにcrudのappを作成しました 1

go langを勉強し始めて、何かappを作れないかと思っていたので簡単なcrudのapp作成に打ち込みました。

下準備

まず、始めるに当たって、go langの基礎を学んでおく必要があるかと思います。

go langのinstallはやっておいてください
goの構文や概念の理解

slice,struct,型決め,変数の定義などなど。基礎的なところで大丈夫かと思います。(自分もそんなになので)

tree構造

todo/ (root)
├handle
┃ └ handle.go
┣serv
┃ └ data.go
┣database
┃ └ db.go.go

└ main.go
└ index.html
└ new.html

こんなんです。

handleはhttpのリクエスト処理の役割
servはdbから情報を引っ張ってきたりする役割
databaseはdbと繋げる役割

serverを立ち上げる

main.go
package main

import (
   "net/http"
   "./handle"  //同じ構造上を参照するときには./で表現
)

func main() {
    http.HandleFunc("/", handle.Showindex) //index.htmlを表示

err := http.ListenAndServe(":3000", nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }

}
handle/handle.go
package handle

import (
    "net/http"
    "net/url"
    "text/template"
    "../serv"     //一つ上の構造を参照するには../で表現。
)

func Showindex(w http.ResponseWriter, r *http.Request) {
    tem, _ := template.ParseFiles("index.html")

//  p := serv.Connected() databaseの接続で使いますので今はコメントアウト。


p:= 1 (仮置き)
tem.Execute(w, p)
//execute templateを動かす。
}

net/http packageを使って、serverを立ち上げています。
もし、立ち上げられなかったら、qiitaや、公式を見てもいいかもしれません。
https://golang.org/pkg/net/http/

go run main.goを打ち込んで実行。
urlはlocalhost:3000です。

これで、index.htmlに書いてあるものが表示されればokです。

余談ですが、cssを当てるときには、
http.Handle("/stylesheet/", http.StripPrefix("/stylesheet/", http.FileServer(http.Dir("stylesheet/"))))

のような記述が必要になってきます。以下参照
https://qiita.com/Sekky0905/items/fca9d9118ef23bf24791

databaseから情報を引っ張ってくる

みなさん、まずはdatabaseを作ってください。(マグマスパゲッティ風)

今回は、mysqlの接続方法を採用しました!
db nameはmydbです。 passwordは""です。空です。
カラムはid,body,image,created_at,updated_atです。

主にservとdatabaseが重要ですかね。

handle/handle.goで p := serv.Connected()があると思います。
これはserv packageのConnected functionを呼び出しています。コメントアウトを外してConnected funcを呼び出しましょう。
以下は、そのserv.Connected funcの定義です。

serv/data.go
package serv

import (
    "log"
    "time"
  "fmt"
"../database"
)

type Vertex struct { //for save
    Id           int
    Body         string
    Image        string
    Created_time time.Time
    Updated_time time.Time
}

func Connected() []Vertex { //2重slice 全件取得
    db := database.ConnectDB()
    defer db.Close()

    //sql
    rows, err := db.Query(`SELECT id,body,image,created_at,updated_at FROM posts`)
    if err != nil {
        log.Fatal(err)
    }

    var sli []Vertex
    var v1 Vertex //structをv1で使用する宣言

    for rows.Next() { //nextはscanを使う為に必要

        if err := rows.Scan(&v1.Id, &v1.Body, &v1.Image, &v1.Created_time, &v1.Updated_time); err != nil {
            log.Fatal(err)
        }
//rowsに入れたクエリをscanで実行。scanしたものをVertex structに入れ込む処理。

        sli = append(sli, v1)
    }
    fmt.Println(sli)
    return sli
//sliは{[id:1,body:bbb,image:fff,created_at:2019-0101,updated_at:2019-0101]}とかで帰ってきたはず。
}

database/db.go
package database

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
    //sql: unknown driver "mysql" (forgotten import?)と言われるので _を使用
)
var db *sql.DB

func ConnectDB() *sql.DB {
    db, err := sql.Open("mysql", "root:@/mydb")
//userはroot, passwordはなし,dbnameはmydb

if err != nil {
        log.Fatalf("Could not open db: %v", err)
}

    //if err != nil {
    //  log.Fatal(err)
    //}
    //return db  上のエラーハンドリングとほぼ同義。
}

Connected funcがかけました。
ここではstruct(構造体)を定義し、その構造体にdatabaseから引っ張ってきた情報を入れて返す処理を行なっています。
db := database.ConnectDB()
defer db.Close()

これは、dbと繋げる処理を担うもので変数dbにdatabseの情報を入れます。database/db.goにfuncの定義があります。dbはopenしたらcloseするのを忘れずに。
https://github.com/go-sql-driver/mysql

db.QueryはQuery methodを用いてクエリ処理を行います。
https://golang.org/pkg/database/sql/#DB.Query

しかし、ここではあくまでクエリの準備段階です。
実際にクエリが走るのは
if err := rows.Scan(&v1.Id, &v1.Body, &v1.Image, &v1.Created_time, &v1.Updated_time)

この部分です。
構造体に入れたものをsliceで囲み、そのsliceを返り値として返します。
これで、dbから情報を引っ張って来れましたね。
取得した情報をtemplateに表示させるためには{{.}}が必要です。
https://qiita.com/tetsuzawa/items/0d043ad76b9705cdbb79

part 2へ
https://qiita.com/dossy/items/17cd49bd654e59dd26e2

色々書くと、見ずらくなったりするので分割することにしました。

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

Goで経過時間を計算するパッケージを作った

動機

年月を計算する処理がなかったから。

X は Y から A 年 B ヶ月 C 日 D 時間 F 分 E 秒 経過しています

というのをやろうとすると、

C 日 D 時間 F 分 E 秒

はタイムスタンプの差から計算することができますが、それ以降はうるう年などの関係から単純に計算することができません

作ったもの

https://github.com/usk81/tiff

使い方

d := New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), time.Date(2001, 12, 10, 11, 10, 20, 0, time.Local))

// 時間差の取得
years, months, days, hours, mins, sec := d.Term()
// years:  1
// months: 11
// days:   9
// hours:  11
// mins:   10
// sec:    20

// 年単位で計算で経過時間を計測
years = d.Years()
// 1

// 月単位で計算で経過時間を計測
months = d.Months()
// 23

// 日単位で計算で経過時間を計測
days = d.Days()
// 709

// 時間単位で計算で経過時間を計測
hours = d.Hours()
// 17027

// 分単位で計算で経過時間を計測
mins = d.Minutes()
// 1021630

// 秒単位で計算で経過時間を計測
sec = d.Seconds()
// 61297820
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goでcliツールを作ってみる

この記事はFusic Advent Calendar15日目の記事です。

こばんは。お久しぶりです。まっさんです。
アドベントカレンダー何を書こうか迷っていたら、こんな時間になっていまいました。良い子はちゃんと事前に準備しときましょう。

とくに何も閃かなかったので、弊社の一部のメンバーで行っている朝の15分輪読会で、絶賛勉強中のgo言語について書いてみようと思います。(golang初学者ですので、誤り等ありました。ご指摘いただければ幸いです)

社内のニコニコカレンダーのcliツールを作ってみる

弊社には、@Y_uuu が作成した、nicoleというニコニコカレンダーがあります。
社員の20〜50%が日々投稿する人気ツールです。
そんなニコニコカレンダーのcliツールを作ってみました。リファクタリングとか、してないので、色々ツッコミどころ満載ですが、こんな感じです。

package main

import (
    "bufio"
    "bytes"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)

type PostData struct {
    Mood    string `json:"mood"`
    Message string `json:"message"`
    Date    string `json:"date"`
}

func main() {
    postJson := createJson()
    if postJson == nil {
        return
    }
    key := getEnvKey()

    req, err := http.NewRequest("POST", "{{ ニコニコカレンダーのAPIのURL }}", bytes.NewBuffer(postJson))
    req.Header.Set("API-KEY", key)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Sprintf("req err: %s", err)
    }
    defer resp.Body.Close()

    respBody, err := ioutil.ReadAll(resp.Body)
    if err == nil {
        fmt.Sprintf("resp err: %s", err)
    }
    fmt.Println(string(respBody))
}

func createJson() []byte {
    t := time.Now()
    defaultMood := "normal"
    defaultMessage := "普通"
    mood := flag.String("mood", defaultMood, "気分")
    key := flag.String("api_key", "", "あなたのApiキーを入力して下さい。")
    message := flag.String("message", defaultMessage, "今日の一言")
    date := flag.String("date", t.Format("2006-01-02"), "日時")
    flag.Parse()

    if *key != "" {
        setEnvKey(*key)
        return nil
    }
    postData := PostData{*mood, *message, *date}
    outputJSON, err := json.Marshal(&postData)
    if err != nil {
        fmt.Sprintf("json err: %s", err)
    }
    return outputJSON
}

func setEnvKey(key string) {
    file, err := os.Create(`./.env`)
    if err != nil {
        fmt.Sprintf("create file err: %s", err)
    }
    defer file.Close()
    output := key
    file.Write(([]byte)(output))
}

func getEnvKey() string {
    file, err := os.Open("./.env")
    if err != nil {
        fmt.Sprintf("open file err: %s", err)
    }
    defer file.Close()

    var envKey string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        envKey = line
    }
    if err := scanner.Err(); err != nil {
        fmt.Sprintf("read file err: %s", err)
    }
    return envKey
}

躓いたポイント

社内のニコニコカレンダーのAPIを叩くために、マイページで作成した API_KEY が必要になります。
ファイルに書き出すよりも、環境変数にセットしたかったので、ググったところ、osパッケージをインポートすると、いい感じに環境変数の読み取り、書き込みができるとのこと。
こんな感じ。

読み取り

package main

import (
    "fmt"
    "os"
)

func main() {
    val := os.Getenv("HOGE")
    fmt.Println(val)
}

設定されてない場合は、空文字が返ります。

書き込み

package main

import (
    "fmt"
    "os"
)

func main() {
    os.Setenv("HOGE", "hoge_value")
}

とても、簡単ですね。しかし、いざ使ってみると、上手くAPI_KEYが取得できない。

どうも、Goの動いているプロセスに対して、環境変数がセットされるようで、そのプロセスが死ぬの環境変数も消えてしまう?っぽい。(間違っていたらすみません。)

外部コマンドに頼る

osパッケージを使って環境変数にセットしよう作戦はあえなく、失敗したので、Goからexport HOGE=hogeのようにosのコマンドを呼び出して無理やり環境変数を設定したいと思います。
os/exec パッケージを使用して外部コマンドを実行できるようです。こんな感じ。

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    out, err := exec.Command("ls", "-la").Output()
    if err != nil {
        fmt.Println(err)
    }

    fmt.Printf(string(out))
}

exec.Command で環境変数をセットしようとしたのですが、こちらは

exec: "export": executable file not found in $PATH

とエラーになり上手く行かず。(理由もまだわかって何ので、わかったら追記します。。。)

おとなしくドットファイルに書き込む

環境変数にセットするのは、上手く行かなかったので、おとなしくドットファイルを作成して、そこで書き込みと読み取りを行うことにしました。

終わりに

まだまだ、Goっぽい書き方とかGoの良さが活かせていないので、日々の輪読をとして精進しようと思います。

明日は、 @yoshitake_1201 の投稿ですので、皆様お楽しみにー!!!

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

[爆速]Go入門

この記事は2019新卒 エンジニア Advent Calendar 2019の15日目の記事です。

今回は、爆速でGoに入門していきます。

この記事を読み終えれば、Goの基礎は身についたと言っても過言ではないでしょう。

Hello World

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}
$ go run main.go
Hello World

Packages

Packageは名前空間を分けるための仕組みです。

同パッケージ内のメンバは無制限に参照することができますが、他のパッケージのメンバにアクセスするにはimport文が必要になります。

上のHelloWorldのコードでは、以下のようなfmtパッケージをimportしています。

package fmt

func Println(...) {
    ...
}

Exported names

最初の文字が大文字で始まる名前は、外部のパッケージから参照できるエクスポート(公開)された名前(exported name)です。

小文字から始まる名前は、外部から参照することができません。

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.Pi) //参照できる
    fmt.Println(math.pi) //参照できない
}
$ go run main.go
# command-line-arguments
./main.go:10:14: cannot refer to unexported name math.pi
./main.go:10:14: undefined: math.pi

Imports

複数のimport文をまとめて書くことができます。

package main

// import "fmt"
// import "math"

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.Pi)
}
$ go run main.go
3.141592653589793

Functions

足し算をするadd関数を定義します。

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(46, 9))
}
$ go run main.go
55

2つ以上の引数が同じ型の場合は、省略して記述できます。

package main

import "fmt"

// func add(x int, y int) int {
//  return x + y
// }

func add(x, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(46, 9))
}

複数の戻り値を返すことができます。

package main

import "fmt"

func swap(x, y int) (int, int) {
    return y, x
}

func main() {
    fmt.Println(swap(46, 9))
}
$ go run main.go
9 46

戻り値の変数に名前をつけることができます。戻り値に名前をつけると、関数の最初で定義した変数名として扱われます(var sum intしたのと同じことになる)。

名前をつけた戻り値の変数を使うと、return文に何も書かずに戻すことができます。これを"naked return"と呼びます。

package main

import "fmt"

func add(x, y int) (sum int) {
    // var sum int
    sum = x + y
    return
}

func main() {
    fmt.Println(add(46, 9))
}
$ go run main.go
55

Variables

var文で変数を宣言します。変数名の後ろに型名を書くことに注意してください。

package main

import "fmt"

var hoge, piyo bool

func main() {
    var i int
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
0 false false

var宣言では、変数ごとに初期化子(initializer)を与えることができます。

また、初期化子が与えられている場合は型宣言を省略でき、その変数は初期化子が持つ型になります。

package main

import "fmt"

var hoge, piyo bool = true, false // 初期化子を与える

func main() {
    var i = 46 // 初期化子を与え、型宣言を省略
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
46 true false

import文と同様に、まとめて書くこともできます。

package main

import "fmt"

var (
    i    = 46
    hoge = true
    piyo = false
)

func main() {
    fmt.Println(i, hoge, piyo)
}

関数の中では、var宣言の代わりに、 := と書くことで暗黙的な型宣言ができます。

ただし、関数の外では、キーワードではじまる宣言(var, func など)必要で、 := での宣言はできません。

package main

import "fmt"

// i := 46 関数の外ではムリ

func main() {
    i := 46 // 暗黙的な型宣言
    fmt.Println(i)
}

型変換の際には、明示的な変換が必要です。

package main

import "fmt"

func main() {
    var i int = 46
    var hoge float64 = float64(i)
    var piyo float64 = i // エラーになる
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
# command-line-arguments
./main.go:8:6: cannot use i (type int) as type float64 in assignment

明示的な型宣言をしない場合(:= や var = のいづれか)、変数の型は右側の変数から型推論されます。

右側の変数が型宣言された変数の場合、左側の新しい変数は右と同じ型になります。

右側が型を指定しない数値である場合、左側は右側の定数の精度に基づいた型になります。

package main

import "fmt"

func main() {
    var i int
    j := i
    fmt.Printf("j is of type %T\n", j)
    hoge := 46
    piyo := 46.123
    fmt.Printf("hoge is of type %T\n", hoge)
    fmt.Printf("piyo is of type %T\n", piyo)
}
$ go run main.go
j is of type int
hoge is of type int
piyo is of type float64

Constants

定数は、constキーワードを使って変数(var)と同じように宣言します。型宣言は省略できます。

なお、 := を使っての宣言はできません。

package main

import "fmt"

func main() {
    // const Name string = "gunsoo"
    const Name = "gunsoo" // 型宣言は省略できる
    fmt.Println("Hello", Name)
    Name = "heichoo" // エラーになる
}
$ go run main.go
# command-line-arguments
./main.go:8:7: cannot assign to Name

For

C言語やJavaと違い、初期化; 条件式; 後処理の部分をくくる括弧()はありません。中括弧{}は必要です。

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()
}
$ go run main.go
0 1 2 3 4 5 6 7 8 9

なお、初期化と後処理の部分は省略可能です。

package main

import "fmt"

func main() {
    i := 0
    for i < 10 {
        fmt.Print(i, " ")
        i++
    }
    fmt.Println()
}

これはCなど他の言語における while と同じです。

Goには while はなく、 for だけを使います。

無限ループを次のようにコンパクトに表現できます。

package main

func main() {
    for {
    }
}

If

If文もforと同様に、()が不要です。

package main

import "fmt"

func main() {
    i := 46
    if i == 46 {
        fmt.Println("hoge")
    }
}
$ go run main.go
hoge

forのように条件の前に、評価するための簡単な文を書くことができます。

ここで宣言された変数は、ifのスコープ内のみで有効です。

package main

import "fmt"

func main() {
    if i := 64; i == 46 {
        fmt.Println("hoge")
    } else {
        fmt.Println(i)
    }
    // fmt.Println(i) スコープ外なのでエラーになる
}
$ go run main.go
64

Switch

GoのswitchはCやJavaなど他の言語と似ていますが、Goでは選択されたcaseだけを実行してそれに続くすべてのcaseは実行されません。break文が自動的に提供されているからです。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
        // break いらない
    case "linux":
        fmt.Println("Linux.")
    default:
        fmt.Printf("%s.", os)
    }
}
$ go run main.go
// your OS

Defer

defer文は、deferへ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。

deferへ渡した関数の引数はすぐに評価されますが、その関数自体は呼び出し元の関数がreturnするまで実行されません。

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("World")

    fmt.Println("Hello")
}
$ go run main.go
Hello
World

deferへ渡した関数が複数ある場合、その呼び出しはスタックされ、LIFOで実行されます。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Countdown...")

    defer fmt.Println("GO!!")
    defer fmt.Println("1...")
    defer fmt.Println("2...")
    defer fmt.Println("3...")
}
$ go run main.go
Countdown...
3...
2...
1...
GO!!

Pointers

ポインタは値のメモリアドレスを指します。

&オペレータがオペランドへのポインタを引き出し、*オペレータがポインタの指す先の変数を示します。

package main

import (
    "fmt"
)

func main() {
    i := 46
    p := &i         // iへのポインタ
    fmt.Println(*p) // ポインタpを通してiの値を読む
    *p = 64         // ポインタpを通してiの値を変更
    fmt.Println(i)
}
$ go run main.go
46
64

Structs

struct(構造体)は、フィールドの集まりです。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    fmt.Println(Name{"hoge", "piyo"})
}
$ go run main.go
{hoge piyo}

structのフィールドは、ドット( . )を用いてアクセスします。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    n := Name{"hoge", "piyo"}
    n.A = "fuga"
    fmt.Println(n.A)
}
$ go run main.go
fuga

フィールド名:値と書くことで、一部のフィールドだけを列挙することができます。

&をつけると、新しく割り当てられたstructへのポインタを返します。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    n1 := Name{"hoge", "piyo"}
    n2 := Name{A: "hoge"}
    n3 := Name{}
    p := &Name{"hoge", "piyo"}
    fmt.Println(n1, n2, n3, p)
}
$ go run main.go
{hoge piyo} {hoge } { } &{hoge piyo}

Arrays

以下では、長さ2の配列を宣言しています。

配列の長さは型の一部のため、配列のサイズを変えることはできません。

package main

import "fmt"

func main() {
    var i [2]int
    i[0] = 1
    i[1] = 2
    fmt.Println(i)
}
$ go run main.go
[1 2]

なおこのようにvarを使わずに書くこともできます。

package main

import "fmt"

func main() {
    i := [2]int{1, 2}
    fmt.Println(i)
}

Slices

配列は固定長な一方で、スライスは可変長です。より柔軟な配列と言ってもいいでしょう。

i[low:high]のように境界を指定することによって、スライスが形成されます。

package main

import "fmt"

func main() {
    i := [5]int{1, 2, 3, 4, 5}

    var s []int = i[1:4]
    fmt.Println(s)
}
$ go run main.go
[2 3 4]

スライスは配列への参照のようなものです。

スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示しています。

スライスの要素を変更すると、その元となる配列の対応する要素が変更されます。

同じ元となる配列を共有している他のスライスは、それらの変更が反映されます。

package main
package main

import "fmt"

func main() {
    i := [5]int{1, 2, 3, 4, 5}

    var s1 []int = i[1:4]
    var s2 []int = i[0:3]
    fmt.Println(s1, s2)

    s1[0] = 46
    fmt.Println(s1, s2)
    fmt.Println(i)
}
$ go run main.go
[2 3 4] [1 2 3]
[46 3 4] [1 46 3]
[1 46 3 4 5]

配列を宣言するのと同じように、スライスを宣言できます。

package main

import "fmt"

func main() {
    // i := [5]int{1, 2, 3, 4, 5}
    i := []int{1, 2, 3, 4, 5} // 上と同じ配列を作成し、それを参照するスライスを作成する
    fmt.Println(i)

    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    fmt.Println(s)
}
$ go run main.go
[1 2 3 4 5]
[{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

スライスは、長さと容量を持っています。

長さは、そのスライスの要素数。容量は、そのスライスの最初の要素から数えて、元となる配列の要素数です。

それぞれlen()とcap()という式で得ることができます。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    printSlice(s)

    s = s[:0]
    printSlice(s)

    s = s[:4]
    printSlice(s)

    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
$ go run main.go
len=5 cap=5 [1 2 3 4 5]
len=0 cap=5 []
len=4 cap=5 [1 2 3 4]
len=2 cap=3 [3 4]

スライスのゼロ値は nil です。

nil スライスは 0 の長さと容量を持っており、何の元となる配列も持っていません。

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}
$ go run main.go
[] 0 0
nil!

スライスは、組み込みのmake関数を使用して作成することができます。

make([]int, len, cap)のように指定します。

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}
$ go run main.go
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [0 0 0]

組み込みのappend関数を使うことにより、スライスへ新しい要素を追加できます。

package main

import "fmt"

func main() {
    var s []int
    printSlice(s)

    s = append(s, 0)
    printSlice(s)

    s = append(s, 1, 2)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
$ go run main.go
len=0 cap=0 []
len=1 cap=1 [0]
len=3 cap=4 [0 1 2]

Range

for ループに利用する range は、スライスや、マップ( map )をひとつずつ反復処理するために使います。

スライスをrangeで繰り返す場合、rangeは反復毎に2つの変数を返します。 1つ目の変数はインデックス( index )で、2つ目はインデックスの場所の要素のコピーです。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    for i, v := range s {
        fmt.Println("index:", i, " value:", v)
    }
}
$ go run main.go
index: 0  value: 1
index: 1  value: 2
index: 2  value: 3
index: 3  value: 4
index: 4  value: 5

インデックスや値は、アンダーバー( _ )へ代入することで捨てることができます。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    for _, v := range s {
        fmt.Println("value:", v)
    }
}
$ go run main.go
value: 1
value: 2
value: 3
value: 4
value: 5

Maps

make関数でマップを作成できます。

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["Nogizaka"] = 46
    m["AKB"] = 48
    fmt.Println(m)
}
$ go run main.go
map[AKB:48 Nogizaka:46]

map m に対し、要素の挿入/更新/削除と、キーに対する要素が存在するかどうかの確認をしていきます。

要素の存在確認は、 elem, ok = m[key] のように書きます。

もし、 m に key があれば、変数 ok は true となり、存在しなければ、 ok は false となります。

なお、mapに key が存在しない場合、 elem はmapの要素の型のゼロ値となります。

package main

import "fmt"

func main() {
    m := make(map[string]int)

    // 挿入
    m["Nogizaka"] = 46
    fmt.Println("The value:", m["Nogizaka"])

    // 更新
    m["Nogizaka"] = 64
    fmt.Println("The value:", m["Nogizaka"])

    // 削除
    delete(m, "Nogizaka")
    fmt.Println("The value:", m["Nogizaka"])

    // 要素の存在確認
    v, ok := m["Nogizaka"]
    fmt.Println("The value:", v, "Present?", ok)
}
$ go run main.go
The value: 46
The value: 64
The value: 0
The value: 0 Present? false

Function values

関数も変数です。他の変数のように関数を渡すことができます。

package main

import "fmt"

func addHello(s string) string {
    return "Hello " + s
}

func main() {
    f := addHello
    fmt.Println(f("World"))
}
$ go run main.go
Hello World

無名関数を定義することもできます。

package main

import "fmt"

// func addHello(s string) string {
//  return "Hello " + s
// }

func main() {
    f := func(s string) string { return "Hello " + s }
    fmt.Println(f("World"))
}

Function closures

Goの関数は クロージャ( closure ) です。

関数と関数の処理に関連する関数の外の環境をセットにして閉じ込めた状態で取り扱うことができます。

この関数は、参照された変数へアクセスして変えることができ、その意味では、その関数は変数へ"バインド"( bind )されています。

package main

import "fmt"

func add() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    f1 := add()
    f2 := add()
    for i := 0; i < 10; i++ {
        fmt.Println(f1(i), f2(-i))
    }
}
$ go run main.go
0 0
1 -1
3 -3
6 -6
10 -10
15 -15
21 -21
28 -28
36 -36
45 -45

sum変数は見かけ上は関数内のローカル変数ですが、実際にはクロージャに属する変数として利用されます。

クロージャによって束縛された変数の領域は、何らかの形でクロージャが参照され続ける限りは破棄されることがありません。

そのため、この例では関数呼び出し毎の合計値をsum変数に保持することができています。

まとめ

こんなに長くなるとは思ってませんでしたが、めちゃくちゃ長くなってしまいました。。

(参考)
https://go-tour-jp.appspot.com/list
http://cuto.unirita.co.jp/gostudy/post/go-package/
https://blog.y-yuki.net/entry/2017/05/04/000000

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

【爆速】Go入門

この記事は2019新卒 エンジニア Advent Calendar 2019の15日目の記事です。

今回は、爆速でGoに入門していきます。

この記事を読み終えれば、Goの基礎は身についたと言っても過言ではないでしょう。

Hello World

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}
$ go run main.go
Hello World

Packages

Packageは名前空間を分けるための仕組みです。

同パッケージ内のメンバは無制限に参照することができますが、他のパッケージのメンバにアクセスするにはimport文が必要になります。

上のHelloWorldのコードでは、以下のようなfmtパッケージをimportしています。

package fmt

func Println(...) {
    ...
}

Exported names

最初の文字が大文字で始まる名前は、外部のパッケージから参照できるエクスポート(公開)された名前(exported name)です。

小文字から始まる名前は、外部から参照することができません。

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.Pi) //参照できる
    fmt.Println(math.pi) //参照できない
}
$ go run main.go
# command-line-arguments
./main.go:10:14: cannot refer to unexported name math.pi
./main.go:10:14: undefined: math.pi

Imports

複数のimport文をまとめて書くことができます。

package main

// import "fmt"
// import "math"

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.Pi)
}
$ go run main.go
3.141592653589793

Functions

足し算をするadd関数を定義します。

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(46, 9))
}
$ go run main.go
55

2つ以上の引数が同じ型の場合は、省略して記述できます。

package main

import "fmt"

// func add(x int, y int) int {
//  return x + y
// }

func add(x, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(46, 9))
}

複数の戻り値を返すことができます。

package main

import "fmt"

func swap(x, y int) (int, int) {
    return y, x
}

func main() {
    fmt.Println(swap(46, 9))
}
$ go run main.go
9 46

戻り値の変数に名前をつけることができます。戻り値に名前をつけると、関数の最初で定義した変数名として扱われます(var sum intしたのと同じことになる)。

名前をつけた戻り値の変数を使うと、return文に何も書かずに戻すことができます。これを"naked return"と呼びます。

package main

import "fmt"

func add(x, y int) (sum int) {
    // var sum int
    sum = x + y
    return
}

func main() {
    fmt.Println(add(46, 9))
}
$ go run main.go
55

Variables

var文で変数を宣言します。変数名の後ろに型名を書くことに注意してください。

package main

import "fmt"

var hoge, piyo bool

func main() {
    var i int
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
0 false false

var宣言では、変数ごとに初期化子(initializer)を与えることができます。

また、初期化子が与えられている場合は型宣言を省略でき、その変数は初期化子が持つ型になります。

package main

import "fmt"

var hoge, piyo bool = true, false // 初期化子を与える

func main() {
    var i = 46 // 初期化子を与え、型宣言を省略
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
46 true false

import文と同様に、まとめて書くこともできます。

package main

import "fmt"

var (
    i    = 46
    hoge = true
    piyo = false
)

func main() {
    fmt.Println(i, hoge, piyo)
}

関数の中では、var宣言の代わりに、 := と書くことで暗黙的な型宣言ができます。

ただし、関数の外では、キーワードではじまる宣言(var, func など)必要で、 := での宣言はできません。

package main

import "fmt"

// i := 46 関数の外ではムリ

func main() {
    i := 46 // 暗黙的な型宣言
    fmt.Println(i)
}

型変換の際には、明示的な変換が必要です。

package main

import "fmt"

func main() {
    var i int = 46
    var hoge float64 = float64(i)
    var piyo float64 = i // エラーになる
    fmt.Println(i, hoge, piyo)
}
$ go run main.go
# command-line-arguments
./main.go:8:6: cannot use i (type int) as type float64 in assignment

明示的な型宣言をしない場合(:= や var = のいづれか)、変数の型は右側の変数から型推論されます。

右側の変数が型宣言された変数の場合、左側の新しい変数は右と同じ型になります。

右側が型を指定しない数値である場合、左側は右側の定数の精度に基づいた型になります。

package main

import "fmt"

func main() {
    var i int
    j := i
    fmt.Printf("j is of type %T\n", j)
    hoge := 46
    piyo := 46.123
    fmt.Printf("hoge is of type %T\n", hoge)
    fmt.Printf("piyo is of type %T\n", piyo)
}
$ go run main.go
j is of type int
hoge is of type int
piyo is of type float64

Constants

定数は、constキーワードを使って変数(var)と同じように宣言します。型宣言は省略できます。

なお、 := を使っての宣言はできません。

package main

import "fmt"

func main() {
    // const Name string = "gunsoo"
    const Name = "gunsoo" // 型宣言は省略できる
    fmt.Println("Hello", Name)
    Name = "heichoo" // エラーになる
}
$ go run main.go
# command-line-arguments
./main.go:8:7: cannot assign to Name

For

C言語やJavaと違い、初期化; 条件式; 後処理の部分をくくる括弧()はありません。中括弧{}は必要です。

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()
}
$ go run main.go
0 1 2 3 4 5 6 7 8 9

なお、初期化と後処理の部分は省略可能です。

package main

import "fmt"

func main() {
    i := 0
    for i < 10 {
        fmt.Print(i, " ")
        i++
    }
    fmt.Println()
}

これはCなど他の言語における while と同じです。

Goには while はなく、 for だけを使います。

無限ループを次のようにコンパクトに表現できます。

package main

func main() {
    for {
    }
}

If

If文もforと同様に、()が不要です。

package main

import "fmt"

func main() {
    i := 46
    if i == 46 {
        fmt.Println("hoge")
    }
}
$ go run main.go
hoge

forのように条件の前に、評価するための簡単な文を書くことができます。

ここで宣言された変数は、ifのスコープ内のみで有効です。

package main

import "fmt"

func main() {
    if i := 64; i == 46 {
        fmt.Println("hoge")
    } else {
        fmt.Println(i)
    }
    // fmt.Println(i) スコープ外なのでエラーになる
}
$ go run main.go
64

Switch

GoのswitchはCやJavaなど他の言語と似ていますが、Goでは選択されたcaseだけを実行してそれに続くすべてのcaseは実行されません。break文が自動的に提供されているからです。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
        // break いらない
    case "linux":
        fmt.Println("Linux.")
    default:
        fmt.Printf("%s.", os)
    }
}
$ go run main.go
// your OS

Defer

defer文は、deferへ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。

deferへ渡した関数の引数はすぐに評価されますが、その関数自体は呼び出し元の関数がreturnするまで実行されません。

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("World")

    fmt.Println("Hello")
}
$ go run main.go
Hello
World

deferへ渡した関数が複数ある場合、その呼び出しはスタックされ、LIFOで実行されます。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Countdown...")

    defer fmt.Println("GO!!")
    defer fmt.Println("1...")
    defer fmt.Println("2...")
    defer fmt.Println("3...")
}
$ go run main.go
Countdown...
3...
2...
1...
GO!!

Pointers

ポインタは値のメモリアドレスを指します。

&オペレータがオペランドへのポインタを引き出し、*オペレータがポインタの指す先の変数を示します。

package main

import (
    "fmt"
)

func main() {
    i := 46
    p := &i         // iへのポインタ
    fmt.Println(*p) // ポインタpを通してiの値を読む
    *p = 64         // ポインタpを通してiの値を変更
    fmt.Println(i)
}
$ go run main.go
46
64

Structs

struct(構造体)は、フィールドの集まりです。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    fmt.Println(Name{"hoge", "piyo"})
}
$ go run main.go
{hoge piyo}

structのフィールドは、ドット( . )を用いてアクセスします。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    n := Name{"hoge", "piyo"}
    n.A = "fuga"
    fmt.Println(n.A)
}
$ go run main.go
fuga

フィールド名:値と書くことで、一部のフィールドだけを列挙することができます。

&をつけると、新しく割り当てられたstructへのポインタを返します。

package main

import "fmt"

type Name struct {
    A string
    B string
}

func main() {
    n1 := Name{"hoge", "piyo"}
    n2 := Name{A: "hoge"}
    n3 := Name{}
    p := &Name{"hoge", "piyo"}
    fmt.Println(n1, n2, n3, p)
}
$ go run main.go
{hoge piyo} {hoge } { } &{hoge piyo}

Arrays

以下では、長さ2の配列を宣言しています。

配列の長さは型の一部のため、配列のサイズを変えることはできません。

package main

import "fmt"

func main() {
    var i [2]int
    i[0] = 1
    i[1] = 2
    fmt.Println(i)
}
$ go run main.go
[1 2]

なおこのようにvarを使わずに書くこともできます。

package main

import "fmt"

func main() {
    i := [2]int{1, 2}
    fmt.Println(i)
}

Slices

配列は固定長な一方で、スライスは可変長です。より柔軟な配列と言ってもいいでしょう。

i[low:high]のように境界を指定することによって、スライスが形成されます。

package main

import "fmt"

func main() {
    i := [5]int{1, 2, 3, 4, 5}

    var s []int = i[1:4]
    fmt.Println(s)
}
$ go run main.go
[2 3 4]

スライスは配列への参照のようなものです。

スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示しています。

スライスの要素を変更すると、その元となる配列の対応する要素が変更されます。

同じ元となる配列を共有している他のスライスは、それらの変更が反映されます。

package main
package main

import "fmt"

func main() {
    i := [5]int{1, 2, 3, 4, 5}

    var s1 []int = i[1:4]
    var s2 []int = i[0:3]
    fmt.Println(s1, s2)

    s1[0] = 46
    fmt.Println(s1, s2)
    fmt.Println(i)
}
$ go run main.go
[2 3 4] [1 2 3]
[46 3 4] [1 46 3]
[1 46 3 4 5]

配列を宣言するのと同じように、スライスを宣言できます。

package main

import "fmt"

func main() {
    // i := [5]int{1, 2, 3, 4, 5}
    i := []int{1, 2, 3, 4, 5} // 上と同じ配列を作成し、それを参照するスライスを作成する
    fmt.Println(i)

    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    fmt.Println(s)
}
$ go run main.go
[1 2 3 4 5]
[{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

スライスは、長さと容量を持っています。

長さは、そのスライスの要素数。容量は、そのスライスの最初の要素から数えて、元となる配列の要素数です。

それぞれlen()とcap()という式で得ることができます。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    printSlice(s)

    s = s[:0]
    printSlice(s)

    s = s[:4]
    printSlice(s)

    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
$ go run main.go
len=5 cap=5 [1 2 3 4 5]
len=0 cap=5 []
len=4 cap=5 [1 2 3 4]
len=2 cap=3 [3 4]

スライスのゼロ値は nil です。

nil スライスは 0 の長さと容量を持っており、何の元となる配列も持っていません。

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}
$ go run main.go
[] 0 0
nil!

スライスは、組み込みのmake関数を使用して作成することができます。

make([]int, len, cap)のように指定します。

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}
$ go run main.go
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [0 0 0]

組み込みのappend関数を使うことにより、スライスへ新しい要素を追加できます。

package main

import "fmt"

func main() {
    var s []int
    printSlice(s)

    s = append(s, 0)
    printSlice(s)

    s = append(s, 1, 2)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
$ go run main.go
len=0 cap=0 []
len=1 cap=1 [0]
len=3 cap=4 [0 1 2]

Range

for ループに利用する range は、スライスや、マップ( map )をひとつずつ反復処理するために使います。

スライスをrangeで繰り返す場合、rangeは反復毎に2つの変数を返します。 1つ目の変数はインデックス( index )で、2つ目はインデックスの場所の要素のコピーです。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    for i, v := range s {
        fmt.Println("index:", i, " value:", v)
    }
}
$ go run main.go
index: 0  value: 1
index: 1  value: 2
index: 2  value: 3
index: 3  value: 4
index: 4  value: 5

インデックスや値は、アンダーバー( _ )へ代入することで捨てることができます。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    for _, v := range s {
        fmt.Println("value:", v)
    }
}
$ go run main.go
value: 1
value: 2
value: 3
value: 4
value: 5

Maps

make関数でマップを作成できます。

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["Nogizaka"] = 46
    m["AKB"] = 48
    fmt.Println(m)
}
$ go run main.go
map[AKB:48 Nogizaka:46]

map m に対し、要素の挿入/更新/削除と、キーに対する要素が存在するかどうかの確認をしていきます。

要素の存在確認は、 elem, ok = m[key] のように書きます。

もし、 m に key があれば、変数 ok は true となり、存在しなければ、 ok は false となります。

なお、mapに key が存在しない場合、 elem はmapの要素の型のゼロ値となります。

package main

import "fmt"

func main() {
    m := make(map[string]int)

    // 挿入
    m["Nogizaka"] = 46
    fmt.Println("The value:", m["Nogizaka"])

    // 更新
    m["Nogizaka"] = 64
    fmt.Println("The value:", m["Nogizaka"])

    // 削除
    delete(m, "Nogizaka")
    fmt.Println("The value:", m["Nogizaka"])

    // 要素の存在確認
    v, ok := m["Nogizaka"]
    fmt.Println("The value:", v, "Present?", ok)
}
$ go run main.go
The value: 46
The value: 64
The value: 0
The value: 0 Present? false

Function values

関数も変数です。他の変数のように関数を渡すことができます。

package main

import "fmt"

func addHello(s string) string {
    return "Hello " + s
}

func main() {
    f := addHello
    fmt.Println(f("World"))
}
$ go run main.go
Hello World

無名関数を定義することもできます。

package main

import "fmt"

// func addHello(s string) string {
//  return "Hello " + s
// }

func main() {
    f := func(s string) string { return "Hello " + s }
    fmt.Println(f("World"))
}

Function closures

Goの関数は クロージャ( closure ) です。

関数と関数の処理に関連する関数の外の環境をセットにして閉じ込めた状態で取り扱うことができます。

この関数は、参照された変数へアクセスして変えることができ、その意味では、その関数は変数へ"バインド"( bind )されています。

package main

import "fmt"

func add() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    f1 := add()
    f2 := add()
    for i := 0; i < 10; i++ {
        fmt.Println(f1(i), f2(-i))
    }
}
$ go run main.go
0 0
1 -1
3 -3
6 -6
10 -10
15 -15
21 -21
28 -28
36 -36
45 -45

sum変数は見かけ上は関数内のローカル変数ですが、実際にはクロージャに属する変数として利用されます。

クロージャによって束縛された変数の領域は、何らかの形でクロージャが参照され続ける限りは破棄されることがありません。

そのため、この例では関数呼び出し毎の合計値をsum変数に保持することができています。

まとめ

こんなに長くなるとは思ってませんでしたが、めちゃくちゃ長くなってしまいました。。

(参考)
https://go-tour-jp.appspot.com/list
http://cuto.unirita.co.jp/gostudy/post/go-package/
https://blog.y-yuki.net/entry/2017/05/04/000000

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

Go、Node.jsのプログラム間でRPC通信をする

概要

gRPCを使用して、Go、Node.jsのプログラム間でRPC通信をします。
クライアント側をGo、サーバ側をNode.jsが担当します。

環境
MacOS Catalina: 10.15.1
Go: 1.13.4
Node.js: 10.15.3

クライアント(Go)の作成

クライアント側のディレクトリを作成します。

$ mkdir grpc-test-go

クライアント側のディレクトリ構成は最終的に以下のようになります。

$ cd grpc-test-go
$ tree
.
├── bridge
│   ├── bridge.pb.go
│   ├── bridge.proto
│   └── go.mod
├── client.go
└── go.mod

.protoファイルの作成

protoファイルを作成して、仕様を定義します。
型にrepeatedをつけると配列になります。
公式ページを参照してください。

bridge.proto
syntax = "proto3";

package bridge;

service BridgeService {
    rpc PostData (Data) returns (Reply) {}
} 

message Data {
    string key = 1;
    repeated string data = 2;
}

message Reply {
    string response = 1;
}

.protoファイルからコードを生成

定義した.protoファイルからクライアント、サーバー共通で使用するコードを生成します。
まず、コードを生成するために必要なprotobufパッケージをインストールします。

$ brew install protobuf
$ protoc bridge/bridge.proto --go_out=plugins=grpc:.

これで、bridge.pb.goが作成されました

module周りを整理

ローカルでbridge.pb.goを参照したいので、色々します。

$ go mod init grpc-test-go
$ cd bridge 
$ go mod init bridge

grpc-test-goの方のgo.modファイルを編集

go.mod
module grpc-test-go

go 1.13

require (
    github.com/[username]/grpc-test2/bridge v0.0.0
    google.golang.org/grpc v1.25.1
)

replace github.com/[username]/grpc-test-go/bridge => ./bridge

クライアント側コードの作成

client.go
package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    pb "github.com/melonattacker/grpc-test-go/bridge"
)

func RpcPost(key string, Data []string) (string, error) {
    conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
    if err != nil {
        return "", err
    }
    defer conn.Close()
    client := pb.NewBridgeServiceClient(conn)
    message := &pb.Data{Key: key, Data: Data}
    res, err := client.PostData(context.TODO(), message)
    response := res.Response
    if err != nil {
        return "", err
    }
    return response, nil
}

func main() {
    data := []string{"apple", "orange", "lemon"}
    result, err := RpcPost("fruit", data);
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(result)
}
$ go build

コンパイルが通るはずです。

サーバ(Node.js)の作成

サーバ側のディレクトリを作成します。
クライアント側と依存しない形で作成しましょう。

$ mkdir grps-test-node
$ cd grps-test-node

.protoファイルの作成

上で作成したbridge.protoをコピーしてきます。

bridge.proto
syntax = "proto3";

package bridge;

service BridgeService {
    rpc PostData (Data) returns (Reply) {}
} 

message Data {
    string key = 1;
    repeated string data = 2;
}

message Reply {
    string response = 1;
}

必要なnpmパッケージのインストール

$ npm init -y
$ npm install grpc @grpc/proto-loader --save

サーバ側コードの作成

server.js
const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const PROTO_PATH = __dirname + '/bridge.proto'

const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    }
)

const BridgeProto = grpc.loadPackageDefinition(packageDefinition)

const server = new grpc.Server()

const PostData = (call, callback) => {
    console.log(call.request);
    callback(null, { response: "Data was sent to server with key: " + call.request.key })
}

server.addService(BridgeProto.bridge.BridgeService.service, {
    PostData: PostData,
})

server.bind('127.0.0.1:50051', grpc.ServerCredentials.createInsecure())
console.log('Listening on 127.0.0.1:50051...')
server.start()

実行

サーバ(Node.js)

$ cd grpc-test-node
$ node main.js
Listening on 127.0.0.1:50051...

クライアント(Go)

$ cd grpc-test-go
$ go run client.go

サーバ(Node.js)

{ data: [ 'apple', 'orange', 'lemon' ], key: 'fruit' }

クライアント(Go)

Data was sent to server with key: fruit

無事データが送られました!
以上です!

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

GAEのデプロイが成功したらGopherが飛ぶようにして開発モチベーションを維持しよう!

まとめ

  • 趣味開発のモチベーション維持のため、デプロイ時に通知してくれるプログラムを作った。
  • ただ通知するだけだと愛想ないので、Gopherを飛ばしてみました。
  • Gopher 可愛い! コミットするのが楽しみになった!気がする

はじめに

開発のモチベーションを維持するのは難しい。

はじめは発想と情熱に任せてガリガリコードを書いていても、ある程度進んでくると
次第にモチベーションが低下してしまうものです。
恒常性維持機能の為せる業か、理由はわかりませんが、開発者なら経験したことがあるでしょう。

現在、私は趣味で Google App Engine 上で動くバックエンドAPIサーバを作成していますが
モチベーションや集中力の低下を感じていました。


そこで以前何かの雑誌で読んだ、開発メンバーのモチベーションを高める方法を実践しました。
方法はいたって簡単です。

進捗や変化を大袈裟に見える化する

記事では、開発チームのモチベーション維持のため、以下のような方法が紹介されていました。

  • 不具合発見時にパトライトを光らせる
  • リリース時に音楽を流す

大袈裟に見える化することで、リフレッシュ効果や同じ作業の繰り返し感をなくせるとのことでした。

そこで、GAEへのデプロイが成功したら、Windowsの画面上にGopherを飛ばそうと思いつきました。
The Gopher
やることが決まったら実装です。

実装

環境
Windows 10

Cloud Buildの実行状況を取得する

GAEへのデプロイにはCloud Buildを利用しているので、Cloud Buildの実行状況を取得します。
実行状況はCloud Pub/Subcloud-buildsというトピックにメッセージが送られます。

詳細は公式ドキュメントに詳しく書いてあります。
ビルド通知の送信 | Cloud Build

公式ドキュメントでは cloud-builds は自動で作成されるとありましたが
私のプロジェクトでは作成されていませんでした。
同名で作成したところ、メッセージが配送されるようになりました。

Pub/Subからメッセージを取得する

Cloud Pub/Sub からメッセージを取得するプログラムを作成します。
言語はGolangで作成しました。
書いたプログラムが基本的にWindowsでもLinuxでも動くのがGolangの良さですね。
pubsub - GoDoc

ドキュメントを参考に書きました。 まごうことなき糞コードです

package main

import (
    "os/exec"
    "context"
    "google.golang.org/api/option"
    "cloud.google.com/go/pubsub"
    "encoding/json"
    "fmt"
)

const ProjectName string = ""        // GCP プロジェクト名
const KeyPath string = ""            // 鍵ファイルパス
const SubscriptionName string = ""   // Pub/Subのサブスクリプション名
const RepositoryName string = ""     // ビルドを実行するリポジトリ名

// 成功時に実行するコマンド
const SuccessCommand string = "../fly.exe"
// 失敗時に実行するコマンド
const ErrorCommand string = "../panic.exe"

type BuildMessage struct {
    Id        string `json:"id"`
    ProjectId string `json:"projectId"`
    Status    string `json:"status"`
    Source    Source `json:"source"`
}
type Source struct {
    RepoSource RepoSource `json:"repoSource"`
}
type RepoSource struct {
    ProjectId  string `json:"projectId"`
    RepoName   string `json:"repoName"`
    BranchName string `json:"branchName"`
}

func main() {
    // pubsub clientの呼び出し
    ctx := context.Background()
    client, _ := pubsub.NewClient(
        ctx,
        ProjectName,
        option.WithCredentialsFile(KeyPath),
    )
    sub := client.Subscription(SubscriptionName)
    // メッセージの受信待ち受け処理
    sub.Receive(context.Background(), MessageHandler)
}

// メッセージ受信時の処理
func MessageHandler(ctx context.Context, m *pubsub.Message) {
    var message BuildMessage
    json.Unmarshal(m.Data, &message)

    // 受信確認
    m.Ack()

    // 意図したリポジトリかどうか
    if message.Source.RepoSource.RepoName != RepositoryName {
        fmt.Println("飛ばない")
        return
    }

    if message.Status == "SUCCESS" {
        fmt.Println("飛べ!!")
        exec.Command(SuccessCommand).Run()
    }

    if message.Status == "FAILURE" {
        // 失敗処理
        fmt.Println("パニック")
        exec.Command(ErrorCommand).Run()
    }
}

Gopherを動かす

Windowsのデスクトップ上でGopherを動かすコマンドはこちらを参考にしました。
本物の golang を... 本物の Gopher を、お見せしますよ。

今回はほとんどこの記事から着想を得ているといっても過言ではありません。
作者に感謝します。
デスクトップ上で飛び回るGopherも可愛いですが、
GAEへのデプロイ感を演出するため、プログラムを一部書き換え、動きを変更しました。

絵を描く

さらにデプロイ感を出すために、Gopherも飛びそうな絵に変更します。
絵を描くのには FireAlpaca を利用しました。
フリーのペイントツールですが、レイヤー機能があり初心者でもそれっぽい絵が描けました。

Gopher下書き

ゴーファー とぶ!

ついに完成したのがこちら

GIF.gif

成し遂げたぜ!

大体一週間くらいあれこれして、ついに完成しました。
絵を書くのに大半の時間を使いました。マウスで絵を書くの難しいですね…

最後に

おわかりいただけただろうか。

大体一週間くらいあれこれして、ついに完成しました。

プログラム開発のモチベーションを高めるために一週間プログラム開発を止めているのだ。
かくも開発モチベーションを維持することは難しい……


しかし、モチベーションが上がらないときはいっそ振り切って別のことをしてしまうのも悪くないです。
その後は集中して開発に取り組めました。納期が迫っただけ

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

docker-compose upするとコンテナがexited with code 1

昨日まで動いていたコンテナが急に動かなくなった

api_1    | build github.com/codegangsta/gin: cannot load gopkg.in/urfave/cli.v1: cannot find module providing package gopkg.in/urfave/cli.v1

go mod downloadでこけてそうなのでgo.modファイルを修正しました。

対応方法

replace directive を追加してモジュール名を書き換えます。

go.mod
module github.com/test-golang

go.1.12

replace gopkg.in/urfave/cli.v1 => github.com/urfave/cli v1.21.0

無事に動くようになりました。

似たようなissue

https://github.com/golang/go/issues/34342
上記を元に修正したのですが直接的な原因は分からなかったです...

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

gorm のコネクションプールを検証してみた

テックタッチアドベントカレンダー20日目担当する@smith-30です。
19日目は@kosy による Vueで日本全国ダーツの旅的なものを作ってみた でした。遊んでみたら僕は福島に行けと言われました。
弊社はまだエンジニア/デザイナが少数なので1人2回記事を書くスケジュールでしたが、毎日誰かの投稿がみれて楽しかったです。

このページについて

gorm のコネクションプール周りの挙動を理解するためにパフォーマンス等色々実験したときのメモです(2019-12)
なんとなく設定して使っていましたが、ちゃんと検証はしたことがなかったので動かしてみました。
といっても gorm は、 database/sql のラッパーなので実質その挙動の調査です。

内容

環境

mysql

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 100   |
+-----------------+-------+
1 row in set (0.00 sec)

mac

$ system_profiler SPHardwareDataType
Hardware:

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: MacBookPro15,2
      Processor Name: Intel Core i5
      Processor Speed: 2.3 GHz
      Number of Processors: 1
      Total Number of Cores: 4
      L2 Cache (per Core): 256 KB
      L3 Cache: 6 MB
      Hyper-Threading Technology: Enabled
      Memory: 16 GB

接続上限の設定

gorm は DB() で *sql.DB を呼び出し、 SetMaxOpenConns(num) で *sql.DB に最大接続数を設定できます。この値を設定していないと上限なくdbへ接続しにいくため、上記の設定値の100以上クエリを同時に発行するとerrorが返されます。

エラーを出力するサンプル

main.go
package conn_pool

import (
    "fmt"

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

const slowQuery = "select sleep(5)"

func doQuery(db *gorm.DB) error {
    return db.Exec(slowQuery).Error
}

func genSetting(username, password, host, port, dbName string) string {
    address := host + ":" + port
    setting := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", username, password, address, dbName)
    return setting
}

func openDB(setting string) *gorm.DB {
    db, err := gorm.Open("mysql", setting)
    if err != nil {
        panic(err)
    }
    return db
}

下記がテストコードです。クエリによる接続数を維持するために goroutine でconnCount数分並列にselect sleep(5) で5秒間待ちのクエリを投げています。期待される挙動としてはconnCountが100のときは接続上限なのでエラーなく実行されるが、次の101は接続上限を超えるのでエラーが起こる想定です。テストケースごとに Close() を呼んでいるのは、queryのコネクションがSleepで残ってしまい、後続のテストに影響がでてしまうからです。Closeを呼ぶとその接続はCleanされます。下記のような形で残ってしまっていたため。

mysql> SELECT * FROM information_schema.PROCESSLIST;
+------+------+------------------+---------------+---------+------+-----------+----------------------------------------------+
| ID   | USER | HOST             | DB            | COMMAND | TIME | STATE     | INFO                                         |
+------+------+------------------+---------------+---------+------+-----------+----------------------------------------------+
| 8116 | root | 172.21.0.1:42058 | test_database | Sleep   |    7 |           | NULL                                         |
| 8095 | root | 172.21.0.1:42016 | test_database | Sleep   |    7 |           | NULL                                         |
|  380 | root | localhost        | test_database | Query   |    0 | executing | SELECT * FROM information_schema.PROCESSLIST |
+------+------+------------------+---------------+---------+------+-----------+----------------------------------------------+
main_test.go
package conn_pool

import (
    "sync"
    "testing"
    "time"

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

func Test_maxConn(t *testing.T) {
    setting := genSetting("root", "root", "localhost", "13306", "test_database")
    type args struct {
        connCount int
    }
    tests := []struct {
        name string
        args args
    }{
        {
            args: args{
                connCount: 100,
            },
        },
        {
            args: args{
                connCount: 101,
            },
        },
    }
    for _, tt := range tests {
        connPoolDB := openDB(setting)
        //if tt.args.connCount > 100 {
        //  connPoolDB.DB().SetMaxOpenConns(100)
        //}

        t.Run(tt.name, func(t *testing.T) {
            db := connPoolDB
            wg := &sync.WaitGroup{}
            for index := 0; index < tt.args.connCount; index++ {
                go func() {
                    wg.Add(1)
                    defer wg.Done()
                    if err := doQuery(db); err != nil {
                        t.Errorf("%v\n", err)
                    }
                }()
            }
            wg.Wait()
            connPoolDB.Close()
        })
    }
}

実行結果

=== RUN   Test_maxConn
=== RUN   Test_maxConn/接続数が上限のとき
=== RUN   Test_maxConn/接続数が上限を超えているとき
--- FAIL: Test_maxConn (10.13s)
    --- PASS: Test_maxConn/接続数が上限のとき (5.06s)
    --- FAIL: Test_maxConn/接続数が上限を超えているとき (5.07s)
        /go/src/github.com/smith-30/goparco/gorm/conn_pool/main_test.go:110: Error 1040: Too many connections
FAIL
FAIL    github.com/smith-30/goparco/gorm/conn_pool  10.154s
FAIL
Error: Tests failed.

想定どおりの結果が出ていますね。次はコメントアウトしていた下記の部分をテストの処理に加えてみます。

if tt.args.connCount > 100 {
    connPoolDB.DB().SetMaxOpenConns(100)
}

期待する動作としては 接続数が上限を超えているとき のケースのときにエラーが出ないかつ、実行時間が10秒であること。(一つのクエリが接続上限の設定により待たされるため。)

実行結果

=== RUN   Test_maxConn
=== RUN   Test_maxConn/接続数が上限のとき
=== RUN   Test_maxConn/接続数が上限を超えているとき
--- PASS: Test_maxConn (15.11s)
    --- PASS: Test_maxConn/接続数が上限のとき (5.06s)
    --- PASS: Test_maxConn/接続数が上限を超えているとき (10.04s)
PASS
ok      github.com/smith-30/goparco/gorm/conn_pool  15.125s

期待してた結果になりました。アプリケーションの想定同時接続数を意識しつつ設定していきたいですね。

コネクションプール

次は、コネクションプールの設定を試してみます。これにより、内部で接続状態を持つことができるのでDBへの接続回数が減ります(はず)。その分処理のコストも減ると考えられます。以下のようにテストコードを書いて実験してみました。クエリを打てるgoroutineを5つセマフォで管理し実行。終わったgoroutineからセマフォが解放され、順次処理が行われていきます。とりあえず、1msec ~ 30msecかかるクエリを10個作り、処理を行わせてみました。コネクションプールの設定は、SetMaxIdleConns メソッドです。今回は 0 にしているので、dbへの接続は10回増える想定です。(ちなみに現状、sql.DBのデフォルトのコネクションプールの数は2でした。)

main_test.go
func Test_useIdleConn(t *testing.T) {
    setting := genSetting("root", "root", "localhost", "13306", "test_database")
    connPoolDB := openDB(setting)
    connPoolDB.DB().SetMaxOpenConns(100)
    connPoolDB.DB().SetMaxIdleConns(0)
    connPoolDB.DB().SetConnMaxLifetime(time.Hour)

    sem := make(chan struct{}, 5)
    qs := getQueries(10)
    for _, item := range qs {
        sem <- struct{}{}
        go func(db *gorm.DB, item string) {
            defer func() {
                <-sem
            }()
            if err := fetch(db, item); err != nil {
                panic(err)
            }
        }(connPoolDB, item)
    }
}

func getQueries(num int) []string {
    qs := make([]string, 0, num)
    for index := 0; index < num; index++ {
        qs = append(qs, fmt.Sprintf("select sleep(%v)", random(0.001, 0.03)))
    }
    return qs
}

実行前

mysql> SHOW GLOBAL STATUS LIKE 'connections';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections   | 14177 |
+---------------+-------+
1 row in set (0.00 sec)

実行後

mysql> SHOW GLOBAL STATUS LIKE 'connections';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections   | 14187 |
+---------------+-------+

10回接続を試みたようですね。想定通り。
次は、SetMaxIdleConns(5) としてコネクションプールの数を明示的に設定してみます。期待する動作としては、goroutineの実行ごとに接続情報はプールに移り、それが使い回されるため、接続回数は5回以下になるはずですね。

実行前

mysql> SHOW GLOBAL STATUS LIKE 'connections';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections   | 14187 |
+---------------+-------+

実行後

mysql> SHOW GLOBAL STATUS LIKE 'connections';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections   | 14192 |
+---------------+-------+
1 row in set (0.00 sec)

接続回数は 5回増えただけでした。標準パッケージなので当たり前ですが、うまく機能しているようで安心です。

コネクションプールをつかったときのパフォーマンス比較

では、コネクションプールをつかったときと使わなかったときでどのくらいのパフォーマンスがでるのかということが気になったのでベンチマークを取りました。やっていることは上記のものと変わりません。SetMaxIdleConns(0)とSetMaxIdleConns(5)のケースで実行してみます。
ベンチマークの各指標についてはこちらの記事がわかりやすいのでご覧ください

main.go
func BenchmarkUseIdleConn(b *testing.B) {
    setting := genSetting("root", "root", "localhost", "13306", "test_database")
    connPoolDB := openDB(setting)
    connPoolDB.DB().SetMaxOpenConns(100)
    connPoolDB.DB().SetMaxIdleConns(0)
    connPoolDB.DB().SetConnMaxLifetime(time.Hour)

    b.Run("", func(b *testing.B) {
        sem := make(chan struct{}, 5)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            sem <- struct{}{}
            go func(db *gorm.DB) {
                defer func() {
                    <-sem
                }()
                if err := fetch(db, "select sleep(0.01)"); err != nil {
                    panic(err)
                }
            }(connPoolDB)
        }

    })
    connPoolDB.Close()
}

SetMaxIdleConns(0)のケース

goos: darwin
goarch: amd64
pkg: github.com/smith-30/goparco/gorm/conn_pool
BenchmarkUseIdleConn/#00-8               296       4292555 ns/op       10379 B/op         90 allocs/op
PASS
ok      github.com/smith-30/goparco/gorm/conn_pool  1.700s

接続回数増分: 400

SetMaxIdleConns(5)のケース

goos: darwin
goarch: amd64
pkg: github.com/smith-30/goparco/gorm/conn_pool
BenchmarkUseIdleConn/#00-8               500       2502588 ns/op        2173 B/op         23 allocs/op
PASS
ok      github.com/smith-30/goparco/gorm/conn_pool  1.515s

接続回数増分: 11

処理速度や、メモリ使用量、メモリ割り当て回数に優位な改善が見られました。設定しておいて損はなさそうですね。DBの接続回数においては圧倒的に減っているのでDBにも優しい処理になるのではないでしょうか。
ちなみに、都度gorm.DBを作ってしまっている場合はどうなんでしょうか。ついでに検証してみます。goroutineの中で都度DBへの接続を開くように変更しました。*gorm.DBの生成コストもかかってしまっています。

main_test.go
func BenchmarkUseIdleConn(b *testing.B) {
    setting := genSetting("root", "root", "localhost", "13306", "test_database")
    b.Run("", func(b *testing.B) {
        sem := make(chan struct{}, 5)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            sem <- struct{}{}
            go func() {
                defer func() {
                    <-sem
                }()
                connPoolDB := openDB(setting)
                connPoolDB.DB().SetMaxOpenConns(100)
                connPoolDB.DB().SetMaxIdleConns(0)
                connPoolDB.DB().SetConnMaxLifetime(time.Hour)
                defer connPoolDB.Close()
                if err := fetch(connPoolDB, "select sleep(0.01)"); err != nil {
                    panic(err)
                }
            }()
        }
    })
}

実行結果

goos: darwin
goarch: amd64
pkg: github.com/smith-30/goparco/gorm/conn_pool
BenchmarkUseIdleConn/#00-8               206       5247975 ns/op       20418 B/op        177 allocs/op
PASS
ok      github.com/smith-30/goparco/gorm/conn_pool  1.683s
Success: Benchmarks passed.

接続回数増分: 606

さらにパフォーマンスが落ちてしまいました。*gorm.DBでオブジェクトを手にしたあとは、使い回すのが適切なようですね。

おわりに

mysql の global status や process list は今まで打つことがなかったので新しく勉強になりました。ベンチマークはどうすればうまく計測できるかなと結構試行錯誤しましたが、結果的にはうまく動いてくれてよかったです。goはテストやベンチマークが簡単にとれるので色々検証しやすくて助かってます。小さなことでも計測すれば何かしら気づきがあるので今後も続けていきたいです。
21日目は @mochibuta による golandの設定紹介 です。

今回検証したコードはこちら

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

Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法

この記事は 株式会社 ACCESS Advent Calendar 2019 16 日目の記事です。

昨日は、@Momijinnさんが楽しげな記事を書いてくれました。

今日は、すこしマニアックな話になってしまいます。
go mod でインターネット上のレポジトリにはないローカルパッケージを扱いたいときに、go.mod の replace directive を使えばいいらしいことは分かったのですが、やってみると色々とハマったので、その時に学んだことや調べたことを記録したいと思います。

ここで記述する内容は、 go 1.13 のバージョンを前提としています。


結論

Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法

まずはじめに、結論だけ知りたい人向けに、どうすればいいのかを記述しておきます。

go.mod で以下のように、記述します。

go.mod
module example.com/me/hello

go 1.13

replace local.packages/goodbye => ./goodbye

module example.com/me/hello : ローカルパッケージを使う側のモジュール名。
go 1.13 : go のバージョン
local.packages/goodbye : ローカルパッケージのモジュール名(任意だがパスのルート部分(ドメイン名に該当する箇所)にドットが含まれている必要があります
replace local.packages/goodbye => ./goodbye : 左側のモジュールを、右に指定した(ローカル上の)相対パスに置き換えるように指定しています。

ソースファイルで import する時に、 go.mod で記述した同じモジュール名である必要があります(絶対パスや相対パスで記述しても import 先を見つけてくれません)。

hello.go
import "local.packages/goodbye"

また、ローカルパッケージのディレクトリ ./goodbye にも、 go.mod が配置されている必要があります(touchで作った空ファイルでも動く)。
モジュール名と、ローカルのディレクトリ名は、必ずしも一致していなくても動きます。

以上を行うことで、go modを使って、インターネット上のレポジトリにはないローカルパッケージを import することができます。

なお、上記の条件を満たすサンプルを、以下に公開しました。
GitHub - hnishi/zakurero_test_gomodules


解説

go mod は、import されるモジュールがインターネット上のレポジトリで管理されることが前提とされていると思われます。

そのため、go.mod の require directive では、github.com などのリポジトリホストのドメイン名が含まれている必要があります。

go modが登場する以前は、 import に直接、絶対パスや相対パスを書くことが許容されていました。
なお、 Module-aware mode (go mod による管理有効化) に対して、古い GOPATH に依存パッケージを探しにいく方法を、 GOPATH mode と呼ぶそうです。

If GO111MODULE=off, then the go command never uses module support. Instead it looks in vendor directories and GOPATH to find dependencies; we now refer to this as "GOPATH mode."

go - The Go Programming Language

パスのルート部分(ドメイン名該当する箇所)にドットが含まれている必要がある理由を、公式ドキュメント等で探してみましたが、見つからず。
おそらく、インターネット上のレポジトリで管理されることが前提(必須レベル)にされていると思われます。

パッケージのディレクトリにgo.modを置く必要がある理由としては、これもドキュメント等で記述を見つけられませんでした。
おそらく、go modで import されるモジュール(パッケージ)は、同じように go.mod で管理されるべきという方針があるのだと思います。

まとめ

Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法に関して記述しました。
明日は、@knagauchiさんが「とりあえず、今の作業が間に合えばKotlinの話、間に合わなければ今年プロジェクトで実践したタスク管理術の話です」をしてくださるそうです。
お楽しみに!

Appendix

Go Modules 概要

  • Go Modulesは、go1.11から導入されたGo標準のGoパッケージ管理ツールである
  • Go1.11時点では試験的に導入されており、feedbackを収集してgo1.14にて完成が予定されている

go mod 使用方法

  • Module-aware modeの有効化
    • GO111MODULE=on : 有効化(go.modがないと動かない)
    • GO111MODULE=off : 無効化(依存パッケージをGOPATHに探しに行く)
    • GO111MODULE=auto : go.modが カレントディレクトリ or 上流ディレクトリに存在している場合は有効化、ない場合は無効化
    • go1.13では、GO111MODULE=autoがデフォルト
    • https://golang.org/cmd/go/#hdr-Module_support
  • go.modの作成
    • go.modを作成したいディレクトリへ移動
    • go mod init <作成するモジュール名>
    • go.modが生成されると、go buildなどのgo commandsを実行した際に自動でgo.modに最新の依存パッケージが追記される(go.sumも生成される)
      • > Once the go.mod file exists, no additional steps are required: go commands like 'go build', 'go test', or even 'go list' will automatically add new dependencies as needed to satisfy imports.
      • https://golang.org/cmd/go/#hdr-Defining_a_module
  • go.modの整理
  • 既にgo.modが存在する場合
    • go build実行時に、go.modに記載のバージョンが、インストールされる
      • 事前に go get や go install する必要はない
  • go.sumの使い方
  • go.modの記述方法
    • 3つのdirectiveを使用する
    • require: to require a particular module at a given version or later
    • exclude: to exclude a particular module version from use
    • replace: to replace a module version with a different module version
    • ローカルモジュールを使用するために、replaceを使う
      • replace <モジュール名> => ローカルに置いたモジュールのソースまでの絶対パス
        • 相対パスを指定する場合は、./ or ../をPATHの先頭に記載する
    • https://golang.org/cmd/go/#hdr-The_go_mod_file
  • 依存パッケージのアップグレード

go mod ハマりポイント

  • モジュール名のルートには、ドット(.)が含まれていなければならない
  • replace directiveの右側には絶対パスを指定する
    • 相対パスを指定する場合は、./ or ../をPATHの先頭に記載する
  • ローカルに置いてあるパッケージを使う場合は、そのパッケージのディレクトリにgo.modを置く必要がある(touchで作った空ファイルでも動く)
    • モジュール名と、ローカルのディレクトリ名は一致していなくても良い
  • go buildするときに読み込まれるgo.modは、ワークディレクトリから1個ずつ上のディレクトリへ遡っていき、一番最初に見つかったgo.modが使われる
    • > When the go command is run, it looks in the current directory and then successive parent directories to find the go.mod marking the root of the main (current) module.
    • go buildするワークディレクトリを変えることで、go.modを使い分けることができる
  • ローカルモジュール等でバージョンがない場合は、go.modファイルのrequireで、v0.0.0を指定する
    • > Untagged revisions can be referred to using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef, where the time is the commit time in UTC and the final suffix is the prefix of the commit hash.
    • https://golang.org/cmd/go/#hdr-Pseudo_versions
  • > 現状の go mod は git tag と git branch の区別が付けれない

ドキュメントから要点を引用

  • Goにおいて、モジュールとはパッケージの集合のことを指す、新しいコンセプトらしい。

A module is a collection of related Go packages that are versioned together as a single unit.

レポジトリ、モジュール、パッケージの関係

  • 1つのレポジトリは、1つ以上のモジュールを含む
  • それぞれのモジュールは、1つ以上のパッケージを含む
  • それぞれのパッケージは、1つのディレクトリに、1つ以上のGoソースファイルを含む

Summarizing the relationship between repositories, modules, and packages:

  • A repository contains one or more Go modules.
  • Each module contains one or more Go packages.
  • Each package consists of one or more Go source files in a single directory.

Modules · golang/go Wiki · GitHub

  • モジュールという概念について

A module version is defined by a tree of source files, with a go.mod file in its root. When the go command is run, it looks in the current directory and then successive parent directories to find the go.mod marking the root of the main (current) module.

  • go mod initは、depなどのツールの設定を取り込んでくれる

In a project already using an existing dependency management tool like godep, glide, or dep, 'go mod init' will also add require statements matching the existing configuration.

  • GOPATHについて

When using modules, GOPATH is no longer used for resolving imports. However, it is still used to store downloaded source code (in GOPATH/pkg/mod) and compiled commands (in GOPATH/bin).

go - The Go Programming Language

実験

go mod の挙動を調べるために、実験を行った。

GitHub - hnishi/zakurero_test_gomodules

モジュール名にドットが含まれていない場合のエラー

import "localpackage/goodbye"
$ go run hello.go
build command-line-arguments: cannot load goodbye: malformed module path "goodbye": missing dot in first path element

goodbye/にgo.modがない場合のエラー

$ go run hello.go
go: local.packages/goodbye@v0.0.0: parsing ../goodbye/go.mod: open /path/to/zakurero_test_gomodules/goodbye/go.mod: no such file or directory

importを相対パスで指定した場合

goodbyeというローカルパッケージを使いたい。
Go Modulesを使わない場合には、相対パスでimportすれば良かったが、Go Modulesではそれが禁止されている。
Module-aware modeがonのときは、モジュール名で指定しなければならない。
試しに、相対パスで指定したときのエラーは以下。

hello2.go
    package main

    import (
     "fmt"
     "../goodbye"
    )

    func main() {
     fmt.Println(goodbye.Goodbye())
    }
$ GO111MODULE=on go run hello2.go
build _/path/to/zakurero_test_gomodules/goodbye: cannot find module for path _/path/to/zakurero_test_gomodules/goodbye

Module-aware modeがoffのときは、成功する。

$ GO111MODULE=off go run hello2.go
Goodbye

このレポジトリをgo getできるか試してみた

$ GO111MODULE=on go get github.com/hnishi/zakurero_test_gomodules

go: finding github.com/hnishi/zakurero_test_gomodules latest
go get: github.com/hnishi/zakurero_test_gomodules@v0.0.0-20191215082227-606721d8e919 requires
        local.packages/goodbye@v0.0.0: unrecognized import path "local.packages/goodbye" (https fetch: Get https://local.packages/goodbye?go-get=1: dial tcp: lookup local.packages on 192.168.65.1:53: no such host)

https://local.packages/goodbye が見つからないと、言っている。
go.mod の replace で記述した、相対パス ./goodbye は、 go get では、よしなにしてくれないようである。
ダウンロードは、してくれている。

$ ls ${GOPATH}/pkg/mod/github.com/hnishi/zakurero_test_gomodules\@v0.0.0-20191215082227-606721d8e919
LICENSE  README.md  go.mod  go.sum  hello  zakurero_test_gomodules.go

公式ドキュメントリンク

https://github.com/golang/go/wiki/Modules
https://golang.org/cmd/go/#hdr-The_go_mod_file

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

結局goのmigrationツールは何を使えばよいのか

Goのmigrationツールのデファクトってなくないですか?

2019年9月からGopherの仲間入りを果たしたのですが、golangのmigrationツールって色々あって、どれ使えば良いのか判断しにくいなと思いました。
9月末にmigrationの調査をしてみたので、存在するmigrationツールや、それらのメリットデメリットをまとめてみたいと思います。

ちなみに、筆者はgolangの前はRailsを使っていたので、migrationは最初から付いているものだと思い込んでいました。(そんなわけない)

migrationツール群(star数100以上)

「go migration」などで検索するといくつかのossが出現しました
それらのメリット・デメリットまとめておきます

名称 Star数  バージョン管理可能 CLI   dryrun 設定方法  
gh-ost 6859 online ok ok online sql
golang-migrate/migrate 2984 up/down ok × sql
sql-migrate 1457 up/down ok ok sql
goose 988 up/down ok × sql
rambler 431 up/down × ok sql
go-gormigrate/gormigrate 355 ? × × go code
DavidHuie/gomigrate 161 up/down × × sql

※ 2019年9月末時点, star数100以下は未調査

※ CLIがないツールは説明しない(CLIなしとか考えられませんでした)

名称 Star数
darwin 87
BurntSushi/migration 64
Boostport/migration 57
migu 50
kamimai 45
carpenter 40
transporter 35
pravasan 24
wallester/migrate 17

どれ使えば良いの?

star数が100以上の中でもgoose、sql-migrate、golang-migrate/migrate、gh-ostならば正直、どれでも良いのでは?ぐらいには考えています。

ただし、gh-ostはmysqlに対してのサポートで、CloudSQLへの対応はできません。
お気をつけください。

有名どころのmigrationツールのサンプルを作ってみた

以下の3つのossについて

  • golang-migrate/migrate
  • sql-migrate
  • goose

dockerを利用して、各種migrationのコマンドを試せるようなサンプルを作ったので、ぜひ参考にしてみてください。
https://github.com/mergitto/go-migrate-sample

golang-migrate/migrate

  • migration file作成(今回はusersテーブル作成)
mkdir migrations
touch migrations/1_create_users_table.up.sql
touch migrations/1_create_users_table.down.sql

upとdownを別々のファイルに記述する点がこのあと説明する、sql-migrategooseとの違いの1つです

  • statusの確認
migrate -database 'mysql://root:mysql@tcp(127.0.0.1:3306)/golang_migrate' -path ./migrations version

1つupしている状態であれば、次のような数字だけが表示されます(少し不親切な気もします)

1
  • upファイルの作成
CREATE TABLE IF NOT EXISTS users (id int);
  • downファイルの作成
DROP TABLE users;
  • migrationを実行
migrate -database 'mysql://root:mysql@tcp(127.0.0.1:3306)/golang_migrate' -path ./migrations up
  • migrationをrollback
migrate -database 'mysql://root:mysql@tcp(127.0.0.1:3306)/golang_migrate' -path ./migrations down

sql-migrateのコマンド例

sql-migrateはconfigの情報はyamlに記述するので、golang-migrate/migrateよりもシンプルなコマンドになります

  • 現在のversionの確認
sql-migrate status

statusの表示内容はgolang-migrate/migrateと違いますね。私はこちらの方が好きです

出力結果
+---------------------------------+---------+
|            MIGRATION            | APPLIED |
+---------------------------------+---------+
| 20190930153619-create_users.sql | no      |
+---------------------------------+---------+
  • migration file作成
sql-migrate new create_users
  • 作成されたmigrationファイルの中身の修正
-- +migrate Up
CREATE TABLE IF NOT EXISTS users (id int);

-- +migrate Down
DROP TABLE IF EXISTS users;
  • migrationを実行
sql-migrate up
  • migrationをrollback
sql-migrate down

gooseのコマンド例

  • statusの確認
goose status

gooseのstatusもgolang-migrate/migrateと比較するとわかりやすいですね
どの環境に対してかも出力してくれます

goose: status for environment 'development'
    Applied At                  Migration
    =======================================
    Pending                  -- 20190930160542_create_users_table.sql
  • migration file作成(今回はusersテーブル作成)
goose create create_users_table sql
  • 作成されたmigrationファイルの中身の修正
-- +migrate Up
CREATE TABLE IF NOT EXISTS users (id int);

-- +migrate Down
DROP TABLE IF EXISTS users;

sql-migrateと同じでup/downを同一ファイルに記述する方式みたいです

  • migrationを実行
goose up
  • migrationをrollback
goose down

結局何使えばよいのか

今回は以下の3つを主に比較しました

  • golang-migrate/migrate
  • sql-migrate
  • goose

これらのツールの違いはほとんど感じられませんでした。sql-migrateがdry-runをすることができる点において少し優秀という印象でしょうか

golang-migrate/migrateはstar数が多かったですが、なぜstar数が多くつくのか理解できませんでした。
私であれば、sql-migrategooseを採用すると思います
以下の点が、その2つの共通点であり、migrationで満たしてほしい条件だと思っています

  • CLIで使える
  • up/downでmigrationができる
  • 環境ごとに設定することができる

というところで、migrationについてのまとめになります。皆さんはいかがでしょうか??

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

OpenTelemetryパッケージに見るpluginパッケージの利用方法

Go6アドベントカレンダーの穴埋め投稿です。

Go Conference 2019 Autumnでどこかの発表で「pluginパッケージはOpenTelemetryで使われていますよ」司会をしたセッションで紹介したのですが、実は最近のコードではその部分は削除されいました。とはいえ、その利用方法というのはpluginパッケージに対応していないWindowsでも使えるような感じで、知っていて損はない感じだったので過去のコミットを掘り出しつつ紹介します。

このエントリーで紹介するテクニックのコードは、以前はルート直下のexporterパッケージの中にありました。そのおかげで目についたのですが、7/12のコードでexperimental/streaming/exporterパッケージに移動になり、最終的に9/24のコミットで削除されました。R.I.P。

プラグインのパッケージ構成

最初のexporter直下にあったときのコードを引っ張り出してきます。OpenTelemetryの情報を標準出力に出すプラグインstdoutです。

- stdout/
  + install/
  | + package.go
  + plugin/
  | + Makefile
  | + package.go
  + stdout.go

このうち、stdout.goはプラグインで利用可能になるロジックを含むパッケージです。pluginパッケージなどの外装には影響されない純粋なロジックパッケージです。

pluginパッケージ

pluginパッケージはpluginのエントリポイントです。

stdout/plugin/package.go
package main

import (
    "github.com/open-telemetry/opentelemetry-go/exporter/observer"
    "github.com/open-telemetry/opentelemetry-go/exporter/stdout"
)

var (
    stdoutObs = stdout.New()
)

func Observer() observer.Observer {
    return stdoutObs
}

func main() {
    _ = Observer()
}
Makefile
.PHONY: module

module:
    go build -buildmode=plugin -o stdout.so package.go 

installパッケージ

これが面白いパッケージで、コメントにあるとおりに import _ "github.com/open-telemetry/opentelemetry-go/exporter/stdout/install"と利用側のパッケージに書いておくと、静的リンクになります。Windowsのようにpluginパッケージに非対応の環境で使うと良いでしょう。

exporter/stdout/install/pakcage.go
package install

import (
    "github.com/open-telemetry/opentelemetry-go/exporter/observer"
    "github.com/open-telemetry/opentelemetry-go/exporter/stdout"
)

// Use this import:
//
//   import _ "github.com/open-telemetry/opentelemetry-go/exporter/stdout/install"
//
// to include the stderr exporter by default.

func init() {
    observer.RegisterObserver(stdout.New())
}

プラグインの読み込み

loaderパッケージが同じリポジトリにあります。環境変数で読み込むモジュールを指定していますね。

exporter/loader/loader.go
package loader

import (
    "fmt"
    "os"
    "plugin"
    "time"

    "github.com/open-telemetry/opentelemetry-go/exporter/observer"
)

// TODO add buffer support directly, eliminate stdout

func init() {
    pluginName := os.Getenv("OPENTELEMETRY_LIB")
    if pluginName == "" {
        return
    }
    sharedObj, err := plugin.Open(pluginName)
    if err != nil {
        fmt.Println("Open failed", pluginName, err)
        return
    }

    obsPlugin, err := sharedObj.Lookup("Observer")
    if err != nil {
        fmt.Println("Observer not found", pluginName, err)
        return
    }

    f, ok := obsPlugin.(func() observer.Observer)
    //obs, ok := obsPlugin.(*observer.Observer)
    if !ok {
        fmt.Printf("Observer not valid\n")
        return
    }
    //observer.RegisterObserver(*obs)
    observer.RegisterObserver(f())
}

func Flush() {
    // TODO implement for exporter/{stdout,stderr,buffer}
    time.Sleep(1 * time.Second)
}

まとめ

pluginパッケージの使い方の例としてOpenTelemetryのコードを紹介しました。フォールバック手法の用意の仕方が面白いですよね。plugin対応の環境であれば、特定のフォルダをスキャンしてそこの実行形式をプラグインとしてロード、そうでない環境は使いたいpluginを静的ロードしたカスタム版の実行ファイルを作成、みたいな感じの使い分けです。plugin対応の環境であっても、シングルバイナリの方がデバッガーでテストするのも楽でしょうしね。スタックトレースが呼び出し側とplugin内で一括で出てくるようになると思いますし。

とはいえ、バイナリサイズをそこまで気にする必要もないし、pluginも、読み込む側と読み込まれる側で利用するパッケージのバージョンをそろえる必要があったりすることを考えると、用途があまり見当たらないというのが正直なところ。Go Cloudもinstallパッケージによる静的な追加のパターンだけだし、そこまで積極的に使われていないというのは事実かと思います。

OpenTelemetryは、エージェントを置いて、各プログラムはエージェントに送信、エージェントはプラグインで機能拡張、という方向性に見えたんですが、どうも違うみたいですね。実装が落ち着いたらまたじっくりコードを追いかけてみようと思います。

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

分散自律知性ネットワークを作ってみた

これはSecHack365 修了生 Advent Calendar 2019の15日目の記事です。

TL;DR

:o:分散
:o:自律
:question:知性
:o:ネットワーク

動機

人間、脳が死んだら死ぬのやばくないですか?

公正的戦闘規範の第二内戦の「ライブラ」とか、ネタバレになっちゃうけど[これ]とか、スタンド・アローン・コンプレックスみたいな、各ノードが分散して自律的に思考した結果全体として知性を持つように見えるやつ、かっこよくないですか?

植物みたいに各細胞が各々の役割を果たせば生きられる、コアのないネットワークこそ至高。
つまり、分散知性ネットワークの代表例である植物が知性を持っているように、コアが無くても各ノードがそれぞれ演算をすればそのネットワークは知性を持つはずで、それをコンピュータ上に再現出来たらかっこよくないですか?

人間

最初に人間をdisってしまったけど、人間(動物)も脳の中は分散ネットワークになっていて(ref: 意識はいつ生まれるのか)、その結果複雑性が生まれて人間には知性(意識?)があるともいえるらしい。
でも人間の体は脳だけじゃ働かないので、その点マクロに見ると分散していないコア有システムだと思ふ。

実装

分散を再現するには並列で思考するノードがあれば良さそう[要出典]。並列と言えば…Go言語かな[要出典]。
ソースはこちら→https://github.com/sudnonk/collective_intelligence

ノードの持つ情報と、思考体系は下図の通りで、だいぶシンプルかつ各ノードで完結しています。

ノードとパスが持つ情報

cell_info.png

ノードの思考フロー

cell_flow.png

パスの思考フロー

  1. 道幅を超えた容量の資源が入ってきたら、広げて欲しさを増やす
  2. 広げてもらえたら、広げて欲しさをリセットする

あとは

  • すでにノードがある場所にノードは置けないWorld.canPut()
  • ノード同士はある程度離れていけないといけないconfig.MinDist
  • 新しいノードの場所はパスの間隔が開いているところPaths.GetWidest()
  • 新しいノードは生成元ノードの性格から±10%の範囲で変動した性格になるPersona.calcMutation()

とかの処理を書きましたが、端折ります。
場所はXY座標で保存し、ノードの場所の管理は世界のサイズ分の行列をgonum/matで用意してそこにフラグを立てることで実現しました。

あとは、nステップの間各ノードの思考関数をgoroutineで並列に実行すればやりたかったことができます。

可視化

時間ごとに円グラフが動く図を作れたらエモいなと思ったのでそうしました。
最初はgithub.com/ajstarks/svgoを使ってGo言語側で可視化処理をしていたのですが、マウスオーバーで各ノードの状態が見れた方がエモくない?って思ったのでJSONに吐き出してブラウザでJavaScriptで見られるようにすることにしました。

結果

というわけで、ブラウザ上でインタラクティブにブラウズできるようにしたので皆さんも見てください。
https://sudnonk.net/ci/

GIFアニメにしてみたのがこれです。(![]()を使うとnot foundになる…?のでimgurへのリンクです。)
ちゃんと自律的にノードを増殖させていることがわかります。
gif @ imgur

結果2

でもなんか成長しきっちゃうと動きがなくてつまらないですね。適当にランダムでダメージを与えてみることにしました。
具体的には、ランダムに選んだ点から±5ぐらいの範囲にあるノードに最大体力の半分のダメージを与えます。

こちらもブラウザで見られます
https://sudnonk.net/ci2/

GIFアニメにしたのがこれです。黄色い範囲のノードにダメージが入ります。(GIFアニメの容量が大きいのでWifi環境推奨)
gif @ imgur

ちゃんとダメージが入ったノードに資源が移動したり、新しいノードが作られたりしていて見ていて面白くなりました。
爆撃で空いた空間に周りのノードが一気に新しいノードを作っているので、一部密度がヤバいことになってますね。あるステップで生成されたノードの情報は次のステップにならないと共有されないので、ノード間の距離制約が機能してないっぽい。

今後

  • 近くにノードがあればそことの間に道を作る関数を書いたのですが、動いてないっぽいので修正したい。
  • 道を作るコストを定数じゃなくて長さと比例させたい。
  • 爆撃の代わりに陣営を2つ作って、近くの対立陣営を攻撃する、みたいな動きも格好良さそうなので実装したい。
  • 意識と知性は何が違うんだろう…

結論

自分たちで勝手に動いてネットワーク形成してるの、かっこいい~~

でも知性ではないような気がする…どうやったら知性を持たせられるんだろう(神の領域)
遺伝的アルゴリズムとか強化学習とかでノードの思考フローも勝手に成長するようになれば知性があるのでしょうか。そこまでしなくても、最初にも紹介した意識はいつ生まれるのか によると、意識があると複雑性が十分に大きいそうです(逆が成り立つとは言っていない)。もし逆が成り立って、とても複雑なネットワークを作ったとして、もしそこに意識が生じたら、それは外部から観測できるのでしょうか。
あなたは私に知性があると思いますか?私はあなたの知性を確認できますか?

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

Unityのmeta漏れを探すCLI

GO言語の練習がてら、Unityのmetaファイルの整合性を雑に検証するCLIを書いてみた。

どう考えても必要な情報は Library/assetDatabase3 に入ってるが、形式は内緒っぽいので、指定ディレクトリ以下のファイルを見繕って guid 一覧を作ることにした。

UnityのYAMLはYAMLとしては非正規なのでまともにParseできないしやらない。正規表現で雑に引っこ抜いてる。

もうちょっと頑張ると、バージョン管理にファイル追加するときに meta を補完したり、guidを全部手繰って足りないファイルを勝手に add するとかも可能だと思われ。多分重たいけど。

package main

import (
    "path"
    "path/filepath"
    "os"
    "flag"
    "fmt"
    "bufio"
    //"strings"
    "regexp"
)

func readguid4meta(fpath string) string {
    var line []byte
    //var err error

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    line,_,_ = rdr.ReadLine()
    line,_,_ = rdr.ReadLine()
    defer fp.Close()
    //var s := string(line)
    return string(line)[6:]
}

func pick_guids_file(fpath string ) []string {
    var err error

    re,_ := regexp.Compile("guid: [0-9a-f]{32}")
    res := make([]string,3)

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    for {
        var buf []byte
        buf, _, err = rdr.ReadLine()
        if err != nil {
            break
        }
        ///fmt.Printf(string(buf))
        foundx := re.FindAllString(string(buf),9)

        for _, x := range foundx {
            if( x == "00000000000000000000000000000000" || x == "0000000000000000f000000000000000" || x == "0000000000000000e000000000000000" ){
                continue
            }
            res = append(res, x[6:])
        }
        ///fmt.Printf("%s\n",found)
        //fmt.Printf(found[0])
    }

    return res
}

var guid_map = make(map[string]string,3)

func register_guid(oname string, guid string ) {
    var ok bool
    if _,ok = guid_map[guid] ; ! ok { // unknown guid
        //fmt.Printf("# %s|.meta: %s\n",oname,guid)
        guid_map[guid] = oname
    } else {
        if guid_map[guid] == oname {

        } else if guid_map[guid][0] != '?' && oname[0] == '?' {

        } else if guid_map[guid][0] == '?' && oname[0] != '?' {
            ////fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        } else if guid_map[guid][0] != '?' {
            fmt.Printf("# ? %s!|.meta: %s != %s\n",guid_map[guid],guid,oname )
        } else {
            //fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        }
    }
}

func visit(fpath string, fi os.FileInfo, err error ) error {
    extn := path.Ext(fpath)
    oname := fpath[:len(fpath)-len(extn)]
    switch extn {
    case ".meta":
        guid := readguid4meta(fpath)
        register_guid(oname,guid)
    case ".unity",".mat","prefab","asset":
        ///fmt.Printf("? %s\n",fpath )
        ll := pick_guids_file(fpath)
        for _, gs := range ll {
            register_guid("?" + fpath, gs )
        }
        //fmt.Printf("%s\n",ll)
    }
    return nil
}

func main () {
    flag.Parse()
    root := flag.Arg(0)
    filepath.Walk(root,visit)

    for k,v := range guid_map {
        if v[0] == '?' {
            fmt.Printf("%s %s\n",v[1:],k )
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityのmetaのcommit漏れを探すCLI

GO言語の練習がてら、Unityのmetaファイルの整合性を雑に検証するCLIを書いてみた。

guidから実ファイルへの検索が必要で、どう考えても必要な情報は Library/assetDatabase3 に入ってるが、形式は内緒っぽいので、指定ディレクトリ以下のファイルを見繕って guid 一覧を作ることにした。

UnityのYAMLはYAMLとしては非正規なのでまともにParseできないしやらない。正規表現で雑に引っこ抜いてる。

もうちょっと頑張ると、バージョン管理にファイル追加するときに meta を補完したり、guidを全部手繰って足りないファイルを勝手に add するとかも可能だと思われ。多分重たいけど。

package main

import (
    "path"
    "path/filepath"
    "os"
    "flag"
    "fmt"
    "bufio"
    //"strings"
    "regexp"
)

func readguid4meta(fpath string) string {
    var line []byte
    //var err error

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    line,_,_ = rdr.ReadLine()
    line,_,_ = rdr.ReadLine()
    defer fp.Close()
    //var s := string(line)
    return string(line)[6:]
}

func pick_guids_file(fpath string ) []string {
    var err error

    re,_ := regexp.Compile("guid: [0-9a-f]{32}")
    res := make([]string,3)

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    for {
        var buf []byte
        buf, _, err = rdr.ReadLine()
        if err != nil {
            break
        }
        ///fmt.Printf(string(buf))
        foundx := re.FindAllString(string(buf),9)

        for _, x := range foundx {
            if( x == "00000000000000000000000000000000" || x == "0000000000000000f000000000000000" || x == "0000000000000000e000000000000000" ){
                continue
            }
            res = append(res, x[6:])
        }
        ///fmt.Printf("%s\n",found)
        //fmt.Printf(found[0])
    }

    return res
}

var guid_map = make(map[string]string,3)

func register_guid(oname string, guid string ) {
    var ok bool
    if _,ok = guid_map[guid] ; ! ok { // unknown guid
        //fmt.Printf("# %s|.meta: %s\n",oname,guid)
        guid_map[guid] = oname
    } else {
        if guid_map[guid] == oname {

        } else if guid_map[guid][0] != '?' && oname[0] == '?' {

        } else if guid_map[guid][0] == '?' && oname[0] != '?' {
            ////fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        } else if guid_map[guid][0] != '?' {
            fmt.Printf("# ? %s!|.meta: %s != %s\n",guid_map[guid],guid,oname )
        } else {
            //fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        }
    }
}

func visit(fpath string, fi os.FileInfo, err error ) error {
    extn := path.Ext(fpath)
    oname := fpath[:len(fpath)-len(extn)]
    switch extn {
    case ".meta":
        guid := readguid4meta(fpath)
        register_guid(oname,guid)
    case ".unity",".mat","prefab","asset":
        ///fmt.Printf("? %s\n",fpath )
        ll := pick_guids_file(fpath)
        for _, gs := range ll {
            register_guid("?" + fpath, gs )
        }
        //fmt.Printf("%s\n",ll)
    }
    return nil
}

func main () {
    flag.Parse()
    root := flag.Arg(0)
    filepath.Walk(root,visit)

    for k,v := range guid_map {
        if v[0] == '?' {
            fmt.Printf("%s %s\n",v[1:],k )
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go で IoT 開発する理由

私は今ウミトロン株式会社で、水産養殖に使うIoTデバイス、およびそれを用いたサービスの開発をしています。

IoT デバイスのプログラミングに Go を採用しているのですが、そういった例はあまり多くないということで、理由について書いてみます。自分は IoT の開発は今の会社が初めてなので比較が色々と難しいのですが、Raspberry Pi の使用例の記事によくある Python や Node を使っているようなケースとの比較と、あとは C / C++ 等のコンパイル言語に比べた時の Go の特徴が生きてくる理由の紹介になります。

サービスについて

デプロイがいつでもできるわけではない

水産養殖に使うIoTデバイスということで、デバイスを海で動かしています。ネットワークは無線ですし、電力はソーラー。デバイスを使うのも充電も昼間のみなので日が沈んだらスリープ状態にしますし、そもそも主電源をユーザーが切る可能性もあるので、我々サービス提供側が必ず操作できるとは限りません。

シングルバイナリにしたい

プログラムをシングルバイナリにできると、VM や、ネイティブコンパイルが必要なライブラリなどの実行環境をデバイス側にもたなくていいので、実行環境のメンテナンスコストが小さくなりデプロイ時に考えることが減ります

信頼性が上がる選択をしたい

IoT デバイスは、台数が規模に対して多くなりがちですし、デプロイが失敗したときのロールバックの対応コストが高いので、デプロイのコストがウェブサーバへと比べて高くなりがちです。

そこで、コンパイル時にエラーが発見できる、コンパイル言語を使う方が有利になります。また、Go であれば C++ と比べてメモリ管理のような複雑な問題を抱える心配も減ります。

I/O 速度が異なるデータへのアクセスが多い

プロダクトとして提供しているデバイスでは複数のセンサーが繋がっていたりログを収集しているので、複数のデータにアクセスしており、HTTP のほか、GPS、BLE、I2C などのそれぞれ I/O 速度が異なります。

Go は goroutine を使った並行処理が標準の方法として使えるので、I/O 待ちが発生する処理がたくさんあっても比較的簡単に扱うことができます。

開発について

クロスコンパイルを簡単に使いたい

Raspberry Pi を使うときは CPU が Arm なので、Macで開発するにはクロスコンパイルが必要です。Go は Arm のクロスコンパイルを標準でサポートしてるので、 go build コマンド実行時に環境変数で指定するだけです。

習得難易度が低い言語にしたい

まだ会社の人数が少ない中で、Ruby や Python などのサーバアプリケーションの開発もするエンジニアがデバイスプログラムの開発もしています。
個人的な意見ですが、一応やったことのある C も C++ に比べ Go が一番 LL に近い感じがあって圧倒的に簡単でした。すぐ動くし。

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

海で使う IoT デバイスの開発に Go を採用する理由

私は今ウミトロン株式会社で、水産養殖に使うIoTデバイス、およびそれを用いたサービスの開発をしています。

IoT デバイスのプログラミングに Go を採用しているのですが、そういった例はあまり多くないということで、理由について書いてみます。自分は IoT の開発は今の会社が初めてなので比較が色々と難しいのですが、Raspberry Pi の使用例の記事によくある Python や Node を使っているようなケースとの比較と、あとは C / C++ 等のコンパイル言語に比べた時の Go の特徴が生きてくる理由の紹介になります。

サービスについて

デプロイがいつでもできるわけではない

水産養殖に使うIoTデバイスということで、デバイスを海で動かしています。ネットワークは無線ですし、電力はソーラー。デバイスを使うのも充電も昼間のみなので日が沈んだらスリープ状態にしますし、そもそも主電源をユーザーが切る可能性もあるので、我々サービス提供側が必ず操作できるとは限りません。

シングルバイナリにしたい

プログラムをシングルバイナリにできると、VM や、ネイティブコンパイルが必要なライブラリなどの実行環境をデバイス側にもたなくていいので、実行環境のメンテナンスコストが小さくなりデプロイ時に考えることが減ります

信頼性が上がる選択をしたい

IoT デバイスは、台数が規模に対して多くなりがちですし、デプロイが失敗したときのロールバックの対応コストが高いので、デプロイのコストがウェブサーバへと比べて高くなりがちです。

そこで、コンパイル時にエラーが発見できる、コンパイル言語を使う方が有利になります。また、Go であれば C++ と比べてメモリ管理のような複雑な問題を抱える心配も減ります。

I/O 速度が異なるデータへのアクセスが多い

プロダクトとして提供しているデバイスでは複数のセンサーが繋がっていたりログを収集しているので、複数のデータにアクセスしており、HTTP のほか、GPS、BLE、I2C などのそれぞれ I/O 速度が異なります。

Go は goroutine を使った並行処理が標準の方法として使えるので、I/O 待ちが発生する処理がたくさんあっても比較的簡単に扱うことができます。

開発について

クロスコンパイルを簡単に使いたい

Raspberry Pi を使うときは CPU が Arm なので、Macで開発するにはクロスコンパイルが必要です。Go は Arm のクロスコンパイルを標準でサポートしてるので、 go build コマンド実行時に環境変数で指定するだけです。

習得難易度が低い言語にしたい

まだ会社の人数が少ない中で、Ruby や Python などのサーバアプリケーションの開発もするエンジニアがデバイスプログラムの開発もしています。
個人的な意見ですが、一応やったことのある C も C++ に比べ Go が一番 LL に近い感じがあって圧倒的に簡単でした。すぐ動くし。

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

GoでmongoDBから出力したJSONファイルを処理するスクリプトを書いてみた

はじめに

会社で作っているチャットボットのログをmongoDBに格納しているのですが、その対話のログデータから、前後数分以内に同じ発言をしているログを抽出して調査したいといったことがありました。そのためにスクリプトを書くことにしたのですが、どうせなら最近勉強を始めたGolangで書いてみよう!!と思い、勉強をかねてスクリプトを実装してみました。

流れとしては、mongoDBからデータをjsonで出力するコマンドmongoexportを使い、出力されたJSONファイルを読み込んでGoの構造体にした後、時間と発言内容を比較する。といった感じです。
mongoexportコマンドで出力されるのは、一行ずつJSONデータとなっているファイルなので、スクリプトで一行ずつ読み込んで処理する必要があります。

本記事では、mongoDBからexportしたJSONファイルを読み込んでGoの構造体へ変換する方法について記載していきます。
もっとこうしたほうが良いよ!これは間違っている!などございましたらご指摘お願いします:bow:

JSONの取り扱い方法

はじめに、簡単にですがJSONデータをGoの構造体に変換する方法を紹介します。
Goの標準パッケージであるencoding/jsonを使用します。このパッケージには、下記メソッドが実装されています。

https://golang.org/pkg/encoding/json/

json.Marshal

構造体->JSON

定義
func Marshal(v interface{}) ([]byte, error)

構造体をJSONへ変換するメソッドです。引数にはJSONに変換したい構造体を渡します。
構造体定義時に、JSONへの出力時のKeyの名前を指定することが出来ます。Keyの名前を指定しない場合は、構造体で定義されているKey名使用されます。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name    string
    Age     int      `json:"age"`
    Email   string   `json:"mail_address"`
    Hobbies []string `json:"hobbies"`
}

func main() {
    user := User{
        Name:    "taro",
        Age:     3,
        Email:   "taro@example.jp",
        Hobbies: []string{"fishing", "tennis"},
    }
    json, err := json.Marshal(&user)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", user)
    fmt.Println(string(json))
}
出力結果
{Name:taro Age:3 Email:taro@example.jp Hobbies:[fishing tennis]}
{"Name":"taro","age":3,"mail_address":"taro@example.jp","hobbies":["fishing","tennis"]}

json.Unmarshal

JSON->構造体

定義
func Unmarshal(data []byte, v interface{}) error

JSONから構造体へ変換するメソッドです。引数には、変換したいbyte配列と変換後の構造体を格納する変数を渡します。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name    string
    Age     int    `json:"nenrei"`
    email   string // 頭文字が小文字のため"email"のJSONは読み込まれない
    Hobbies []string
}

func main() {
    j := `{
        "name": "taro",
        "nenrei": 3,
        "email": "taro@sample.jp",
        "hobbies": ["fishing", "tennis"]}`
    bytes := []byte(j)
    var user User
    // 変換に失敗したらerrorが返ってくるのでハンドリングする
    if err := json.Unmarshal(bytes, &user); err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", user) // mapのキーとバリュー両方表示するためにPrintf+vを使用
}
出力結果
{Name:taro Age:3 email: Hobbies:[fishing tennis]}

ここ2つ注目してほしい部分があります。
まずひとつ目はstruct定義のemailで、構造体定義の頭文字がemailと小文字になっている場合は、JSONから構造体に変換されず、代わりにstring型の初期値である空の文字列""になってしまっています。
2つ目は、JSONではnenreiとなっているキーをGoの構造体ではAgeとして受け取っている部分です。このようにJSONで与えられるキーとは別の名前としたい場合には、jsonで指定することが出来ます。

データの準備

今回サンプルで使用するデータをmongoDBに格納していきます。
db名はsmaple。collection名はlogsとしています。

> use sample;
switched to db sample
> db.createCollection('logs');
{ "ok" : 1 }
> show dbs;
admin   0.000GB
config  0.000GB
local   0.000GB
sample  0.000GB
> show collections;
logs

logsコレクションにデータを追加

> db.logs.insert({name: "leg", utterance: '{"message": "明日の天気は?"}', extra: { type: "hoge" }, created_at: ISODate("2019-12-01T00:02:00+09:00")})
...

mongoexportコマンド

次にmongoexportコマンドを使って、コレクションに格納されているデータのjsonファイルを取得します。

option

-d: db名
-c: コレクション名
-out: 出力ファイル名
--query: 検索条件クエリ

mongoexport -d=sample -c=logs -out=sample.json --query '{ "created_at": {"$gte" : ISODate("2019-11-01T00:00:00+00:00"), "$lte" : ISODate("2019-12-03T00:00:00+00:00")} }'

上記コマンドにより、下記データが出力されました。

sample.json
{"_id":{"$oid":"5df5102e6909535236a55d2b"},"name":"Charlotte","utterance":"{\"message\": \"こんにちは\"}","extra":{},"created_at":{"$date":"2019-11-30T15:01:00.000Z"}}
{"_id":{"$oid":"5df510326909535236a55d2c"},"name":"leg","utterance":"{\"message\": \"明日の天気は?\"}","extra":{"type":"hoge"},"created_at":{"$date":"2019-11-30T15:02:00.000Z"}}
{"_id":{"$oid":"5df510376909535236a55d2d"},"name":"Chloe","utterance":"{\"message\": \"今日の運勢\"}","created_at":{"$date":"2019-11-30T15:03:00.000Z"}}
{"_id":{"$oid":"5df5103b6909535236a55d2e"},"name":"jummy","utterance":"{\"message\": \"こんにちは\"}","created_at":{"$date":"2019-11-30T15:04:00.000Z"}}
{"_id":{"$oid":"5df513d76909535236a55d31"},"name":"taro","utterance":"{\"message\": \"明日の天気は?\"}","created_at":{"$date":"2019-11-30T15:05:00.000Z"}}
{"_id":{"$oid":"5df513ea6909535236a55d32"},"name":"taro","utterance":"{\"message\": \"メニュー\"}","extra":{"piyo":"piyopiyo"},"created_at":{"$date":"2019-11-30T15:06:00.000Z"}}

*データの補足ですが、utteranceにはJSONの形で格納されていて、extraのデータ存在しなかったり、存在したとしても不規則な形で格納されているデータです。

スクリプトの実装

今回やりたいことは、前後5分以内の同じMessage(重複したログ)があれば取得することです。
上で用意したデータの場合、Messageがこんにちは明日の天気は?になっている2つずつのログが取得出来れば成功となります。

構造体の定義

まずは、JSONを読み込むための構造体を定義します。

type Id struct {
    Oid string `json:"$oid"`
}

type Utterance struct {
    Message string `json:"message"`
}

type CreatedAt struct {
    Date time.Time `json:"$date"`
}

type Log struct {
    Id         Id                     `json:"_id"`
    Name       string                 `json:"name"`
    Utterance  Utterance              `json:"utterance"`
    CreatedAt CreatedAt              `json:"created_at"`
    Extra      map[string]interface{} `json:"extra"`
}

ここでのポイントはUnmarshalの説明部分でも紹介した、JSONのデータでのキーとは別名で受け取る方法を利用しています。理由としては、_id, $oid, $dateのように頭文字がアルファベットまたは数値意外の場合にはGoの構造体のキーとして使用できないためです。
また、Extraの部分はどんなデータ型がバリューとして入っているのかわからないため、interface{}としています。

このまま上記の構造体へjson.Unmarshalを使って変換しようとするとエラーが起きます。
それは、Utteranceを構造体のtype Utterance struct { Message string }として受け取ろうとしていますが、実際に読み込む値はstringとなっているためです。

type Utterance struct {
    Message string `json:"message"`
}

type Log struct {
    Utterance  Utterance `json:"utterance"` // 構造体として受け取ろうとしている
}

// 読み込むデータ: {"utterance": "{\"message\": \"こんにちは\"}"} stringとなっている

UnmarshalJSONメソッドの実装

再度Unmarshal(JSON->構造体)してあげる必要があります。
このように、Unmarshal(JSON->構造体)するときにデータを加工したり、追加したいデータが場合などにはUnmarshalJSONというメソッドを定義することが出来ます。
イメージとしては、最終的に吐き出したいデータのために途中で処理を挟むという感じです。

https://golang.org/pkg/encoding/json/#RawMessage.UnmarshalJSON

type Log struct {
    Id         Id                `json:"_id"`
    Name       string            `json:"name"`
    Utterance  Utterance         `json:"utterance"` // ここが構造体になっている
    CreatedAt CreatedAt         `json:"created_at"`
    Extra      map[string]string `json:"extra"`
}

func (log *Log) UnmarshalJSON(b []byte) error {
    // まずは、途中で使用する構造体を定義する
    // 上のstructのLogとの違いは、Utteranceだけ
    type Log2 struct {
        Id        Id                `json:"_id"`
        Name      string            `json:"name"`
        Utterance string            `json:"utterance"` // ここをstringで定義
        CreatedAt CreatedAt         `json:"created_at"`
        Extra     map[string]string `json:"extra"`
    }

    var log2 Log2                   // 新しく定義した構造体の変数を宣言
    err := json.Unmarshal(b, &log2) // json.Unmarshalを実行するときに渡されるbyteをUnmarshal(JSON->構造体)する
    if err != nil {
        return err
    }
    var utterance Utterance                                  // Utteranceの変数を宣言
    err = json.Unmarshal([]byte(log2.Utterance), &utterance) // ここがポイント!stringを構造体にする

    // 加工し終わったデータたちを、logの構造体変数へ格納していく
    log.Id = log2.Id
    log.Name = log2.Name
    log.Utterance = utterance
    log.CreatedAt = log2.CreatedAt
    log.Extra = log2.Extra

    return err
}

JSONファイルの読み込み

続いて、JSONファイルを読み込んで実際にjson.Unmarshalを使用して構造体に変換するコードを書いていきます。
mongoexportで出力したファイルは、1行に1つのJSONが保存されているので、1行ずつ読み込んで構造体にしスライスに格納すれば良さそうです。

ファイルの読み込みにはos.Openbufio.NewScannerを使います。

os.Open

https://golang.org/pkg/os/#Open

func Open(name string) (*File, error)

bufio.NewScanner

https://golang.org/pkg/bufio/#NewScanner

リターンされるscannerからメソッドを呼び出すことで、一行ずつ読み込む処理をすることが出来ます。

func NewScanner(r io.Reader) *Scanner

下記のようにReadFileという関数を定義します。
引数としてファイルの名前と、結果を格納するスライスのポインタを受け取り、ファイルを一行ずつ読み込んで構造体に変換してから、スライスへ追加していく処理を実装しています。

func ReadFile(filename string, resultLogs *[]Log) {
    fp, err := os.Open(filename)
    if err != nil {
        fmt.Println(err)
    }
    defer fp.Close()                // 先にCloseを定義しておく
    scanner := bufio.NewScanner(fp) // 読み込んだファイルのスキャナーが返ってくる
    for scanner.Scan() {            // ループを処理でScanメソッドを実行することで、一行ずつ処理できる
        txt := scanner.Text()       // Text()メソッドで一行のテキストを受け取る
        bytes := []byte(txt)
        var log Log
        if err := json.Unmarshal(bytes, &log); err != nil {
            fmt.Println(err)
        }
        *resultLogs = append(*resultLogs, log) // 構造体にした結果をスライスに格納する
    }
    if err = scanner.Err(); err != nil {
        fmt.Println(err)
    }
}

JSONを構造体にする

あとは関数を呼び出す側を書けば、各行の構造体が格納されたスライスが作成されます。

const FILE_NAME = "sample.json"

func main() {
    logs := make([]Log, 0)
    ReadFile(FILE_NAME, &logs)
    for _, v := range logs {
        fmt.Printf("%+v\n", v)
    }
}
出力結果
$ go run main.go
{Id:{Oid:5df5102e6909535236a55d2b} Name:Charlotte Utterance:{Message:こんにちは} CreatedAt:{Date:2019-11-30 15:01:00 +0000 UTC} Extra:map[]}
{Id:{Oid:5df510326909535236a55d2c} Name:leg Utterance:{Message:明日の天気は?} CreatedAt:{Date:2019-11-30 15:02:00 +0000 UTC} Extra:map[type:hoge]}
{Id:{Oid:5df510376909535236a55d2d} Name:Chloe Utterance:{Message:今日の運勢} CreatedAt:{Date:2019-11-30 15:03:00 +0000 UTC} Extra:map[]}
{Id:{Oid:5df5103b6909535236a55d2e} Name:jummy Utterance:{Message:こんにちは} CreatedAt:{Date:2019-11-30 15:04:00 +0000 UTC} Extra:map[]}
{Id:{Oid:5df513d76909535236a55d31} Name:taro Utterance:{Message:明日の天気は?} CreatedAt:{Date:2019-11-30 15:05:00 +0000 UTC} Extra:map[]}
{Id:{Oid:5df513ea6909535236a55d32} Name:taro Utterance:{Message:メニュー} CreatedAt:{Date:2019-11-30 15:06:00 +0000 UTC} Extra:map[piyo:piyopiyo]}

構造体にすることが出来ました!

比較処理の実装

最後に、前後5分以内で同じMessageがあれば取得する処理の実装です。
下記のように時間を比較する関数を返すFactory関数を作ります。比較もとの時間を渡すことで、比較先の時間を渡して時間以内であればtrueを返す関数を作成してくれます。このFactory関数はクロージャになっており、比較もと時間の5分前と5分先の時間を保持するようになっています。

const TIME_RANGE = 5

func compareTimeFactory(baseTime time.Time) func(target time.Time) bool {
    beforeTime := baseTime.Add(-TIME_RANGE * time.Minute)
    afterTime := baseTime.Add(TIME_RANGE * time.Minute)

    return func(targetTime time.Time) bool {
        return beforeTime.Unix() <= targetTime.Unix() &&
            targetTime.Unix() <= afterTime.Unix()
    }
}
results := make([][]Log, 0)
    for i := 0; i < len(logs); i++ {
        baseLog := logs[i]
        compareTime := compareTimeFactory(baseLog.CreatedAt.Date)
        duplicateLogs := make([]Log, 0)
        duplicateLogs = append(duplicateLogs, baseLog)

        for ii := 0; ii < len(logs); ii++ {
            targetLog := logs[ii]
            // 自分自身は比較しない
            if baseLog.Id == targetLog.Id {
                continue
            }
            if compareTime(targetLog.CreatedAt.Date) && baseLog.Utterance.Message == targetLog.Utterance.Message {
                duplicateLogs = append(duplicateLogs, targetLog)
                // 重複を発見したら、そのlogを空のstructにして2重で結果に吐かれないようにする
                logs[ii] = Log{}
            }
        }

        // 重複したログがある場合に結果の箱にいれる
        if len(duplicateLogs) > 1 {
            results = append(results, duplicateLogs)
        }
    }

    for _, v := range results {
        fmt.Printf("%+v\n", v)
    }
出力結果
$ go run main.go
[{Id:{Oid:5df5102e6909535236a55d2b} Name:Charlotte Utterance:{Message:こんにちは} CreatedAt:{Date:2019-11-30 15:01:00 +0000 UTC} Extra:map[]} {Id:{Oid:5df5103b6909535236a55d2e} Name:jummy Utterance:{Message:こんにちは} CreatedAt:{Date:2019-11-30 15:04:00 +0000 UTC} Extra:map[]}]
[{Id:{Oid:5df510326909535236a55d2c} Name:leg Utterance:{Message:明日の天気は?} CreatedAt:{Date:2019-11-30 15:02:00 +0000 UTC} Extra:map[type:hoge]} {Id:{Oid:5df513d76909535236a55d31} Name:taro Utterance:{Message:明日の天気は?} CreatedAt:{Date:2019-11-30 15:05:00 +0000 UTC} Extra:map[]}]

無事に想定した結果を取得することが出来ました:tada:

完成したコードの全体はイカのようになりました。

コード
package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
    "time"
)

const FILE_NAME = "sample.json"
const TIME_RANGE = 5

type Id struct {
    Oid string `json:"$oid"`
}

type Utterance struct {
    Message string `json:"message"`
}

type CreatedAt struct {
    Date time.Time `json:"$date"`
}

type Log struct {
    Id        Id                `json:"_id"`
    Name      string            `json:"name"`
    Utterance Utterance         `json:"utterance"`
    CreatedAt CreatedAt         `json:"created_at"`
    Extra     map[string]string `json:"extra"`
}

func (log *Log) UnmarshalJSON(b []byte) error {
    type Log2 struct {
        Id        Id                `json:"_id"`
        Name      string            `json:"name"`
        Utterance string            `json:"utterance"`
        CreatedAt CreatedAt         `json:"created_at"`
        Extra     map[string]string `json:"extra"`
    }

    var log2 Log2
    err := json.Unmarshal(b, &log2)
    if err != nil {
        return err
    }
    var utterance Utterance
    err = json.Unmarshal([]byte(log2.Utterance), &utterance)

    log.Id = log2.Id
    log.Name = log2.Name
    log.Utterance = utterance
    log.CreatedAt = log2.CreatedAt
    log.Extra = log2.Extra

    return err
}

func readFile(filename string, resultLogs *[]Log) {
    fp, err := os.Open(filename)
    if err != nil {
        fmt.Println(err)
    }
    defer fp.Close()
    scanner := bufio.NewScanner(fp)
    for scanner.Scan() {
        txt := scanner.Text()
        bytes := []byte(txt)
        var log Log
        if err := json.Unmarshal(bytes, &log); err != nil {
            fmt.Println(err)
        }
        *resultLogs = append(*resultLogs, log)
    }
    if err = scanner.Err(); err != nil {
        fmt.Println(err)
    }
}

func compareTimeFactory(baseTime time.Time) func(target time.Time) bool {
    beforeTime := baseTime.Add(-TIME_RANGE * time.Minute)
    afterTime := baseTime.Add(TIME_RANGE * time.Minute)

    return func(targetTime time.Time) bool {
        return beforeTime.Unix() <= targetTime.Unix() &&
            targetTime.Unix() <= afterTime.Unix()
    }
}

func main() {
    logs := make([]Log, 0)
    readFile(FILE_NAME, &logs)

    results := make([][]Log, 0)
    for i := 0; i < len(logs); i++ {
        baseLog := logs[i]
        compareTime := compareTimeFactory(baseLog.CreatedAt.Date)
        duplicateLogs := make([]Log, 0)
        duplicateLogs = append(duplicateLogs, baseLog)

        for ii := 0; ii < len(logs); ii++ {
            targetLog := logs[ii]
            if baseLog.Id == targetLog.Id {
                continue
            }
            if compareTime(targetLog.CreatedAt.Date) && baseLog.Utterance.Message == targetLog.Utterance.Message {
                duplicateLogs = append(duplicateLogs, targetLog)
                logs[ii] = Log{}
            }
        }

        if len(duplicateLogs) > 1 {
            results = append(results, duplicateLogs)
        }
    }

    for _, v := range results {
        fmt.Printf("%+v\n", v)
    }
}

参考にした記事

https://www.takedajs.com/entry/2016/04/30/105738
https://qiita.com/kitoko552/items/d7178915a4792d1e3e85

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

chan chan は意外と美味しい

すっかり寒くなってきてチャンチャン焼きが美味しい今日この頃ですね(^_^)

ところで、Go言語でchannelをchannelで受け渡し出来ること、ご存知でしょうか。
自分の周囲では使っている人少なそうですが、意外と便利なので使用例をいくつか紹介したいと思います。

使用例 1: Request/Response

channelは通常片方向の受け渡しですが、channelを二重にすることでレスポンスを受け取ることができます。

例えば処理結果のerrorを受け取りたい場合は chan chan error を使用します。

reqc := make(chan chan error)

リクエストを送る側は chan error をmakeして、chan chanに送信します。
このchannelに結果が返ってきます(結果が返るまでブロックされます)。

ch := make(chan error)
reqc <- ch // リクエスト送信!!

err := <-ch  // 終了結果を待機

リクエストを受ける側は受け取ったchannelに処理結果(ここではerror)を返します。

// 例えばfor-selectループ
for {
    select {
    case ch := <-reqc:
        err := doSomething()
        ch <- err // errorを返す
    }
}

サンプルコード

package main

import (
    "fmt"
    "log"
    "time"
)

type Daemon struct {
    reqc chan chan error
}

func (d *Daemon) StartLoop() {
    go func() {
        for {
            select {
            case ch := <-d.reqc:
                err := doSomething()

                ch <- err // errorを返す
            }
        }
    }()
}

func (d *Daemon) DoSomething() error {
    ch := make(chan error)
    d.reqc <- ch                 // リクエスト送信!!
    if err := <-ch; err != nil { // 終了結果を待機
        return err
    }
    return nil
}

func main() {

    d := &Daemon{
        reqc: make(chan chan error),
    }

    d.StartLoop()

    if err := d.DoSomething(); err != nil {
        log.Fatal(err)
    }

    fmt.Println("done")
}

参考

Advanced Go Concurrency 3 つのパターン 1
Understanding Chan Chan's in Go
https://github.com/micro/go-micro/blob/dd7677e6cca1f48e1a41bd7ce3f5edf3f4a8d9dc/api/server/http/http.go#L30

使用例 2: 処理の待機

Request/Responseと類似ですが、単純に処理完了を待機するのに使えます。
githubのOSSを検索した限りだとこの用例が比較的多かったです。

結果を返す必要はないのでchan chanは空構造体型で用意します。

reqc := make(chan chan struct{})

リクエストを送る側は chan struct{} をmakeして、chan chanに送信します。
処理完了はcloseによって通知されるのでchannel受信でそれを待ちます(完了までブロックされます)。

ch := make(chan error)
reqc <- ch // リクエスト送信!!

<-ch  // 終了結果を待機(closeされたら再開)

リクエストを受ける側は処理が完了したら受け取ったchannelをcloseします。

for {
    select {
    case ch := <-reqc:
        doSomething()
        close(ch) // 処理完了を通知する為のclose
    }
}

サンプルコード

package main

import (
    "fmt"
    "time"
)

type Daemon struct {
    reqc chan chan struct{}
}

func (d *Daemon) StartLoop() {
    go func() {
        for {
            select {
            case ch := <-d.reqc:
                doSomething()

                close(ch) // 処理完了を通知する為のclose
            }
        }
    }()
}

func (d *Daemon) DoSomething() {
    ch := make(chan struct{})
    d.reqc <- ch
    <-ch // 終了を待機(closeされたら再開)
}

func main() {

    d := &Daemon{
        reqc: make(chan chan struct{}),
    }

    d.StartLoop()

    d.DoSomething()

    fmt.Println("done")
}

参考

https://github.com/peco/peco/blob/dc15605aee1581634602cef22af6212e24a8fbe6/screen.go#L24
https://github.com/go-kit/kit/blob/e2d71a06a40aa95cb82ccd72e854893612c02db7/sd/eureka/instancer.go#L20

使用例 3: Subscriber登録

ある意味素直な用途ですが、Publisher/Subscriberで、Subscriber(channel)を登録するのに使用します。

サンプルコード

package main

import (
    "fmt"
    "time"
)

type PubSub struct {
    subscribe   chan chan string
    unsubscribe chan chan string
    publish     chan string
}

func (ps *PubSub) Subscribe(sub chan string) {
    ps.subscribe <- sub
}

func (ps *PubSub) Unsubscribe(sub chan string) {
    ps.unsubscribe <- sub
}

func (ps *PubSub) Publish(msg string) {
    ps.publish <- msg
}

func (ps *PubSub) Start() {
    go func() {
        subscribers := make(map[chan string]struct{})

        for {
            select {
            case ch := <-ps.subscribe:
                subscribers[ch] = struct{}{}
            case ch := <-ps.unsubscribe:
                delete(subscribers, ch)
            case msg := <-ps.publish:
                for sub := range subscribers {
                    select {
                    case sub <- msg:
                    default:
                    }
                }
            }
        }
    }()
}

参考

https://github.com/google/gocw/blob/c04bd445135da75ae4ab557a7e98f34fe7e78083/util/broker.go#L22
https://github.com/ethereum/go-ethereum/blob/736b45a87606e6cdfd5aecf38d259517b10e7f7e/eth/downloader/api.go#L33

使用例 4: 順序の保証

実は個人的には一番よく使う用例です。

goroutineで処理を高速化しつつ、順序を保証したいときにchan chanを利用します。

例えば大きなファイルを読み込みながら、1行ごとになんらかのフェッチ(DBアクセス, RPC, etc)をして加工するパイプライン処理を考えます。

↓はchan chan適用前のコードです。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

// ファイルから1行ずつ読み込む
func readSomething() <-chan string {
    outCh := make(chan string)

    go func() {
        defer close(outCh)

        for i := 0; i < 1000; i++ {
            // ファイルを読みこむ代わりにSleep
            time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)
            outCh <- fmt.Sprintf("line:%d", i)
        }
    }()

    return outCh
}

// フェッチ&加工
func fetchSomething(inCh <-chan string) <-chan string {
    outCh := make(chan string)

    go func() {
        defer close(outCh)

        for line := range inCh {
            // フェッチする代わりにSleep
            time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
            outCh <- fmt.Sprintf("%s ... fetched!", line)
        }
    }()

    return outCh
}

func main() {
    start := time.Now()

    for line := range fetchSomething(readSomething()) {
        fmt.Println(line)
    }

    fmt.Println("done", time.Now().Sub(start))
}

実行結果

line:0 ... fetched!
line:1 ... fetched!
line:2 ... fetched!
line:3 ... fetched!
(中略)
line:996 ... fetched!
line:997 ... fetched!
line:998 ... fetched!
line:999 ... fetched!
done 49.956134795s

goroutine使ってますがほぼ直列処理なのですごく時間がかかっています。
そこでフェッチ処理をgoroutineで並行処理して高速化をはかります。

↓chan chanはまだ出てきません。

// フェッチ&加工
func fetchSomething(inCh <-chan string) <-chan string {
    outCh := make(chan string)

    c := context.Background()

    go func() {
        defer close(outCh)

        var wg sync.WaitGroup

        sem := semaphore.NewWeighted(10)

        for line := range inCh {
            wg.Add(1)
            sem.Acquire(c, 1)

            go func(line string) {
                defer wg.Done()
                defer sem.Release(1)

                // フェッチする代わりにSleep
                time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
                outCh <- fmt.Sprintf("%s ... fetched!", line)
            }(line)
        }

        wg.Wait()
    }()

    return outCh
}

無条件に全部goroutineでぶん回せば劇速になりますが、入力ファイルが巨大になるとその分メモリなどリソースを消費するためセマフォ(golang.org/x/sync/semaphore) を使って同時実行数を10に制御しています。

実行結果

line:3 ... fetched!
line:8 ... fetched!
line:7 ... fetched!
line:2 ... fetched!
line:6 ... fetched!
line:0 ... fetched!
(中略)
line:991 ... fetched!
line:989 ... fetched!
line:995 ... fetched!
line:993 ... fetched!
line:996 ... fetched!
done 5.059474157s

かなり速くなりました!

しかしよく見ると結果が変わっています。
行は全て出力されていそうですが、順序が入れ替わってしまっています。
これはフェッチの為の複数goroutineが順不同で結果を出力channelに書き込んでいるからです。

出力が順不同でよければこれでOKです。
しかし、ときには出力順を入力と揃えたい、というケースもあるでしょう。

そんなとき chan chan の出番です!

// フェッチ&加工
func fetchSomething(inCh <-chan string) <-chan string {
    outChCh := make(chan chan string, 10)

    go func() {
        defer close(outChCh)

        for line := range inCh {
            outCh := make(chan string)
            outChCh <- outCh

            go func(line string) {
                // フェッチする代わりにSleep
                time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
                outCh <- fmt.Sprintf("%s ... fetched!", line)
            }(line)
        }
    }()

    // chan chanをそのまま後続処理に渡してもよいが、ここではchanに変換しておく
    outCh := make(chan string)
    go func() {
        defer close(outCh)

        for ch := range outChCh {
            outCh <- <-ch
        }
    }()

    return outCh
}

出力用バッファ付きchan chanを作成し、上流channelからデータを受け取ったらまずフェッチ処理出力用channelを作成してそれを出力用chan chanへ送信していまいます。
そしてgoroutineでフェッチ処理を行い、結果をフェッチ処理出力用channelへ送信します。

コードのちょっと面白い点として、前バージョンで使用していたセマフォがなくなっています。
chan chanのバッファサイズがセマフォの役割も担っています。
WaitGroupも不要になったのでコードが気持ちスッキリしました。(^^)v

実行結果

line:0 ... fetched!
line:1 ... fetched!
line:2 ... fetched!
line:3 ... fetched!
line:4 ... fetched!
line:5 ... fetched!
(中略)
line:995 ... fetched!
line:996 ... fetched!
line:997 ... fetched!
line:998 ... fetched!
line:999 ... fetched!
done 7.275727182s

整いました!\(^o^)/

順不同バージョンよりも若干スループットが落ちてますがこれは仕組み上仕方ないところです。
chan chanのバッファサイズでスループットとメモリリソースのトレードオフを調整できます。

おしまいちゃんちゃん

OSSから利用例をたくさん探して紹介する予定でしたが存外見つからず、少しか紹介できませんでした(´・ω・`)
他にも面白い使用例ご存知でしたらコメントで教えていただけると嬉しいです。


  1. 昔参加していた勉強会の記事がひっかかった。懐かしい・・・(^ω^) 

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

Goで画像を扱う話(webp編)

2回目の登場になります @sys_cat です。気づいたらVtuberが好きになってました、推しが良すぎて胸が苦しい…ハッ、これが恋…。

はい。15日目になります。
昨日は @kyam_ さんの [Swift] UITextViewに画像を添付する方法 でした。
普段、アプリ開発者の方々とお話する機会が無いのですが社内LTとかで凄く面白い話していただくのでとっても悔しい思いをしてます。そういうの、もっと頂戴っていう。


今回は前回書きました Goで画像を扱う話 でも予告したとおりWebp変換のお話になります。

Webpとは?

Googleさんが丁寧な 解説 を書いておりますので、詳しい話はそちらで。
簡単に書くならば

  • Googleが開発している画像形式
  • PNGやJPEGよりちょっと(25%〜30%位)軽くなるよ(非可逆圧縮モードの時の話)
  • 軽くて画像劣化が少ない(無いわけではない)のでレスポンスが向上しやすい
  • Web開発ではWebp化対応はたまーに聞く(定石かは知らないです…)

はい、「なんかいい感じの画像」って思ってくれたら良さそうな感じですね。

GoはWebp対応してんの?

  • 読むことは出来る
  • 作成する事は出来ない

というところです。

公式パッケージとしては webp単独のものは無く、Decode用の パッケージ が用意されているのみです。

有志の外部パッケージ

公式は無くともそれはGo言語、つよつよなエンジニアさんが界隈に沢山いらっしゃいますので調べてみますと、外部パッケージで作成されている作者さんが以下の様にいらっしゃいます。

以前Webp対応した時は chai2010さんのパッケージを使いましたが既存のimageパッケージの枠にはまらない使い方でとても驚いたのを覚えてます
(しっかりbytesを使ってたしあの頃は1日ずっとDocument片手にソースコードを読む毎日でとても為になりました!)

今回は

個人的に尊敬している harukasan さんの go-libwebp を使って変換をしていきます。

Webp変換

前提

個人的な趣味として以下の前提の上に実装をしてみました

  • Webサーバとして提供したい
    • GETで受けてファイルをResponseする
  • Docker上に諸々置く
    • 最近 docker-compose.yml 書くのが癖みたいになってるんですよ
    • 今回、インフラ面に少し記述があるので一応 Dockerfile も晒します
  • FwはEcho
    • 最高にEchoすこ、時点でGinとかGoa

そーすこーど

main.go
package main

import (
    "bufio"
    "github.com/labstack/echo"
    "github.com/harukasan/go-libwebp/webp"
    "golang.org/x/image/draw"
    "image"
    "image/jpeg"
    "net/http"
    "os"
)

type (
    Error struct {
        Message string
    }
)

func initServe() *echo.Echo {
    e := echo.New()
    e.Static("tmp", "tmp")
    e.GET("/", handler)
    return e
}

func handler(c echo.Context) error {
    f, err := os.Open("./tmp/Go-Logo_LightBlue.jpg")
    if err != nil {
        return c.JSON(http.StatusNotFound, &Error{
            Message:"file not found",
        })
    }
    defer f.Close()

    img, err := jpeg.Decode(f)
    if err != nil {
        return c.JSON(http.StatusGone, &Error{Message:err.Error()})
    }
    bou := img.Bounds()
    dst := image.NewRGBA(image.Rect(0, 0, bou.Dx(), bou.Dy()))
    draw.CatmullRom.Scale(dst, dst.Bounds(), img, bou, draw.Over, nil)

    o, err := os.Create("tmp/New.webp")
    if err != nil {
        return c.JSON(http.StatusInternalServerError, &Error{Message:"create new image is failed"})
    }
    w := bufio.NewWriter(o)
    defer func() {
        w.Flush()
        o.Close()
    }()

    con, _ := webp.ConfigPreset(webp.PresetDefault, 80)
    err = webp.EncodeRGBA(w, dst, con)

    if err != nil {
        return c.JSON(http.StatusInternalServerError, &Error{Message:"encode error"})
    }

    return c.File("tmp/New.webp")
}

func main() {
    serve := initServe()

    serve.Logger.Fatal(serve.Start(":8080"))
}
FROM golang:1.13-alpine3.10

RUN apk update && apk add --no-cache make\
        gcc\
        g++\
        git\
        libwebp\
        binutils-gold \
        curl \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python\
        libwebp-tools\
        libwebp-dev\
        tiff-dev\
        vips-dev\
        libzip libzip-dev\
docker-compose.yml
version: '3'
services:
  go:
    build:
      context: ./
      dockerfile: ./Dockerfile
    volumes:
      - ./:/go/src/github.com/sys-cat/imageapi_for_advent2019
    working_dir: /go/src/github.com/sys-cat/imageapi_for_advent2019
    environment:
      - GO111MODULE=on
    ports:
      - "8080:8080"
    command: "go run main.go"

ちょっと長いですね。1つずつ説明していきます。

せつめい!( Go )

画像をDecodeする

    f, err := os.Open("./tmp/Go-Logo_LightBlue.jpg")
    if err != nil {
        return c.JSON(http.StatusNotFound, &Error{
            Message:"file not found",
        })
    }
    defer f.Close()

    img, err := jpeg.Decode(f)
    if err != nil {
        return c.JSON(http.StatusGone, &Error{Message:err.Error()})
    }
    bou := img.Bounds()
    dst := image.NewRGBA(image.Rect(0, 0, bou.Dx(), bou.Dy()))
    draw.CatmullRom.Scale(dst, dst.Bounds(), img, bou, draw.Over, nil)

前回まででやったところですね。
go-libwebp のドキュメントを見た場合分かるのですが画像が RGBA 形式であれば dst あたりの記述は不要になります。今回は対象の画像が image.YCbCr 形式なので新しくRGBA形式の画像を作成しています。

Webp設置用の場所を確保する

    o, err := os.Create("tmp/New.webp")
    if err != nil {
        return c.JSON(http.StatusInternalServerError, &Error{Message:"create new image is failed"})
    }
    w := bufio.NewWriter(o)
    defer func() {
        w.Flush()
        o.Close()
    }()

tmp に適当な画像を作っています。 go-libwebp では 出力時io.Writerが必要なので bufio.NewWriter(os.File) でWriterを作っておきます。 Writerって Flush() でClose相当の処理やってくれるんですねぇ。知らなかった…(1敗)

Webp変換

    con, _ := webp.ConfigPreset(webp.PresetDefault, 80)
    err = webp.EncodeRGBA(w, dst, con)

    if err != nil {
        return c.JSON(http.StatusInternalServerError, &Error{Message:"encode error"})
    }

    return c.File("tmp/New.webp")

README.md を読んでると webp.EncodeRGBA(w, dst, webp.ConfigPreset(webp.PresetDefault, 80)) とかしたくなるけど実は webp.ConfigPreset*webp.Config, error が返却値になるので注意です(1敗)。
Configは非可逆モードだったり多岐に設定出来るので読めばもっと詰める事が出来るかも( https://godoc.org/github.com/harukasan/go-libwebp/webp#Config )
c.File でファイルを返せるの、いいよね(申し訳程度のEcho要素)。

せつめい!( Docker )

Webp を扱う為に…

FROM golang:1.13-alpine3.10

RUN apk update && apk add --no-cache make\
        gcc\
        g++\
        git\
        libwebp\
        binutils-gold \
        curl \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python\
        libwebp-tools\
        libwebp-dev\
        tiff-dev\
        vips-dev\
        libzip libzip-dev\

もうこれに尽きるというか、Webpってjpegなどと違って簡単に扱える様になってないので alpine に色々と食わせてやる必要があります。ここに上げてるのは今回対応していて必要そうだったファイル群です。
これで大体100パッケージ位インストールされます。(g++とかはビルドに必要になる感じ)
Go内部で扱えれば良いのですが今回利用している go-libwebp はCGOを使っているのでどうしても webp.h がないといけなかった。という訳ですね。

出来たものを比べてみよう!

ファイルサイズ

Screenshot from 2019-12-15 04-29-08.png

????

いや、ちょっとこれは差が多すぎる… image.YCbCrimage.RGBA 化したりQuarityが80だったりしているので劣化込みでこのサイズ減かなと思います。

ファイル劣化

変換画像をそのまま上げるのはちょっと憚れるのでそれぞれの環境で試していただきたいのですが個人環境で確認してみると以下の劣化を確認出来ました。

  • 色境界のエッジがちょっときついかも
  • 色劣化?(色Profileが異なるので判別が付きづらいかも)

設定詰めたり色Profileを社内で規定すれば劣化がほとんど無い変換が可能だと思います。

まとめ

  • Webp変換はあんまり手間でも無いので画像サイズで困ってる人は使ってみると良さそう
  • 脱ImageMagic!が現実的になる
    • 昔のImageMagicを使い続ける必要がなくなる(OSバージョンが固定化されなくなる)
    • PHPのバージンアップがしやすくなる
    • しあわせなせかい
  • 実はPNGからの変換もいい感じに出来るのでいい感じに変換出来る
  • Gif to Webp はちょっと難易度高めです
  • みんなもGoで画像操作しよう!!

次回予告

21日も書きます。
ちゃんと弊社にも自キー好きがいるよって話をします。

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

Golang+Bazelで依存ライブラリをいい感じに管理する

この記事はOpenSaaS Studio Advent Calendar 2019、11日目の記事です。3日目はPython+Bazelでしたが、今回はGolang+Bazel。

はじめに

GolangのプロジェクトでBazelを使う場合、依存ライブラリの二重管理が必要になることがあります。
二重管理は変更の反映漏れにつながったり管理方法が理解しづらくなるため基本的には避けるべきですが、上手くハンドリングできればビルド速度などBazelのメリットを享受しつつ問題なく運用できます。
この記事では、私がジョインしているプロジェクトでこの課題にどう対応したのか紹介します。

依存ライブラリの二重管理

BazelではWORKSPACEというファイルで依存ライブラリを定義します(マクロに定義してWORKSPACEでそのマクロを呼ぶ方法もあります)が、Golangのアプリを開発する場合にはIDEの対応状況やワークフローなどを考慮してgo.modに依存ライブラリを定義することがほとんどだと思います。
その場合、以下のように複数のファイルに同じ依存ライブラリの定義が存在するため二重管理が必要になります。

WORKSPACE
go_repository(
    name = "com_google_cloud_go",
    importpath = "cloud.google.com/go",
    sum = "h1:0sMegbmn/8uTwpNkB0q9cLEpZ2W5a6kl+wtBQgPWBJQ=",
    version = "v0.44.3",
)
go.mod
require (
    cloud.google.com/go v0.44.3
)

二重管理の運用

go.modの変更をWORKSPACEに反映する

一般的な開発フローではIDEなどによりまずgo.modが変更され、その変更をWORKSPACEに反映するためにGazelleが使われます。
READMEの記載を参考にセットアップすると、以下のコマンドでgo.modからWORKSPACEに変更が反映されます。

bazel run //:gazelle -- update-repos -from_file=go.mod

ただし、このコマンドでは以下の問題があります。

  • WORKSPACEの定義がオリジナルな依存ライブラリとGazelleで生成された依存ライブラリが同じファイルで管理される
  • go.modから削除されたライブラリがWORKSPACEから削除されない

Gazelleで生成する依存ライブラリ定義を隔離する

Gazelleは生成対象の依存ライブラリをマクロとして出力する機能があり、この機能を利用することでWORKSPACEから分離することができます。

bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories

to_macroフラグで出力先を指定しており、この例だとrepositories.bzlgo_repositoriesという名前で依存ライブラリを定義するためのマクロが生成されます。
生成されたマクロは以下のようにWORKSPACEで呼び出せば依存ライブラリの定義がロードされます。

WORKSPACE
### Below is the auto-generated dependency macro from go.mod.
# gazelle:repository_macro repositories.bzl%go_repositories

load("//:repositories.bzl", "go_repositories")

go_repositories()

不要になったライブラリの削除

これもフラグがあるのでそれを指定するだけです。

bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories -prune=true

pruneフラグです。
ただし、このフラグを設定するとgo.modで解決できないライブラリのgo_repositoryが全て削除されてしまいます。
そのままだとPGVなどが削除されてしまうため、もし同様のライブラリ定義があればkeepコメントで削除されないようにしてください。

WORKSPACE
### Protoc-gen-validate
# keep
# It is used in order to generate validation code from proto, not defined in go.mod.
go_repository(
    name = "com_github_envoyproxy_protoc_gen_validate",
    importpath = "github.com/envoyproxy/protoc-gen-validate",
    tag = "v0.1.0",
)

まとめ

  • GolangとBazelで依存ライブラリを管理するためのファイルが別になっており、二重管理が必要
  • アプリを開発する時はgo.modを変更
  • Gazelleを使ってgo.modの変更をBazel側に反映し、不要になったライブラリも削除

この記事では以上の内容を説明してきましたが、他の言語でBazelを使う場合やデファクト以外のビルドツールを使う場合にもこの問題は出てくると思うので、その際の検討の参考になればと思います。

これからやりたいこと

ライブラリのバージョンを上げるのって手間ですよね。
自動でバージョンを上げてくれるサービスがあったりするので、これを利用して自動で更新できるようにしたい。
この記事で書いたように自動生成された定義とそうでない定義は分離できているので、どのファイルをサービスにinputすればいいかは整理できている。
あとはリグレッションテストを完備すればデグレには気付ける、はず!

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

マップの中に同じ値のキーが2つ存在する!?

TL;DR

キー値が同じ値だと思っていたら実は違っていたというオチです。

  • キー項目として、time.Time型を含んだ構造体を使用した
  • time.Time型は、==で比較してはいけない
    • 構造体を比較する場合、構造体の中にポインタ型が含まれている場合は、ポインタ値を比較する
    • time.Time型は、*time.Location型の項目を含んでいる
  • マップのキーは、==trueの場合、同値として判定する

結論は上記の通りですが、備忘録として、発生した経緯と調査の過程を下記に残したいと思います。

発覚するまで

例えば、データベースに以下のようなテーブルがあったとします。

ID 名前 登録日時
1 七尾 2017-06-29 12:00:00
2 高坂 2017-06-29 13:00:00
3 北沢 2017-06-29 14:00:00
4 矢吹 2017-06-29 15:00:00

このテーブルから、指定された回数の分だけランダムでレコードを取得し、レコードごとの抽出回数を集計する処理を実装しようとしていました。
最終的に以下のように出力する想定でした。

ID 名前 抽出回数
1 七尾 27
2 高坂 26
3 北沢 25
4 矢吹 22

その場合、以下のような処理を書けば目的は達成されるはずです。

const count = 100

type User struct {
    ID           int
    Name         string
    RegisteredAt time.Time
}

func main() {
    m := make(map[User]int)

    for i := 0; i < count; i++ {
        u := getUserRandom()
        m[u]++
    }

    for u, count := range m {
        fmt.Printf("%v %v %v\n", u.ID, u.Name, count)
    }
}

func getUserRandom() model.User {
    // データベースからランダムに1レコード取得する処理
}

上記の処理ですが、countを増やすとおかしくなることがあります。以下のような結果が出てきたりします。

ID 名前 抽出回数
1 七尾 1500
1 七尾 1200
2 高坂 1200
2 高坂 1400
3 北沢 1500
3 北沢 1000
4 矢吹 1100
4 矢吹 1100

同一IDのデータが2行出力されていますね。明らかにおかしいです。

調査

マップ上で別項目として扱われている以上、必ず何かしらの値が違うはずです。とりあえずデバッグ出力してみます。

...
    for u, count := range m {
        // fmt.Printf("%v %v %v\n", u.ID, u.Name, count)
        fmt.Printf("%#v\n", u)
    }
...
出力結果
User{ID:1, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634302000, loc:(*time.Location)(0xc00033d020)}}
User{ID:1, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634302000, loc:(*time.Location)(0xc00033dda0)}}
User{ID:2, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634305600, loc:(*time.Location)(0xc00033d020)}}
User{ID:2, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634305600, loc:(*time.Location)(0xc00033dda0)}}
User{ID:3, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634309200, loc:(*time.Location)(0xc00033d020)}}
User{ID:3, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634309200, loc:(*time.Location)(0xc00033dda0)}}
User{ID:4, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634312800, loc:(*time.Location)(0xc00033d020)}}
User{ID:4, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634312800, loc:(*time.Location)(0xc00033dda0)}}

1行目と2行目を比較すると、RegisteredAtのtime.Timeの中にあるlocのポインタ値が異なっています。

マップのキーとして同一かどうかは ==演算子で比較した結果trueかどうか で判定していると考えて良いです。
一方で、構造体の中にポインタ項目があった場合、ポインタのアドレスが同一かどうかで比較されます(参照先は比較しません)
その観点で考えると、1行目のUser != 2行目のUserであるため、異なるキー項目として扱われるのは正しい。という結論になります。

上記を踏まえた回避策としては以下の2つが考えられます

  • time.TimeのタイムゾーンをUTCに変更する
    • locnullになる
    • 1行目のUser == 2行目のUserとなり、同一キーとして扱われるようになる
  • そもそもUserをキーとして使わない
    • User.IDなどのプリミティブな値を使用する

今回についてはタイムゾーンを設定する必要があったため、後者を選択し、無事解決しました。

余談

データベースからデータを取得した場合、このような現象が発生したかについては詳細を調べていません。
データベースから取得してtime.Time構造体を生成する際、ロケーションは使いまわしているが、一定間隔で再生成するのでしょうか?

参考資料

Try Golang! time.Timeの等値判定で注意すること

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

Apple の TLS 証明書無効の影響を受けた話

認証認可技術 Advent Calendar 2019 15日目は TLS に関する記事です。TLS は 通信相手の認証、通信内容の暗号化、改ざんの検出を可能とするという通信プロトコルであり、HTTPS 通信で使用され、また OAuth 2.0 は TLS を前提とした仕様になっていたりととても重要な認証技術要素の一つです。その TLS について、今年行われた Apple の TLS 証明書無効化 による影響を受けたというお話を軽く紹介したいと思います。

はじまり

ちょうど一ヶ月ほど前に、最新の macOS Catalina を搭載した新型 MacBoo Pro が発表されました。タイミングよく開発機を新調しようとしていたところに発表され、人柱になるかもしれませんが(なりました)購入したわけです。で、ちょうど先週納品されてウキウキしながら:relaxed:セットアップし始めたのですが・・・

しかし、即この TLS 証明書無効化による影響をうけてドハマリしたわけです:scream_cat:

Apple の TLS 証明書無効化

Apple は2019年6月5日、以前より危険性が指摘されてきたハッシュアルゴリズムSHA-1を使用した TLS 証明書 を、iOS 13 および macOS 10.15(つまり Catalina)から無効とする方針を表明したことは皆さんご存知でしょうか? 当時 ITmedia エンタープライズ などのニュースサイトでも取り上げられていました(自分は記憶にありませんでしたが。。。)

Apple からの公式発表は以下になります。

SHA-1 を使った TLS 証明書はすでに Chrome、Firefox、MS Edge、Internet Explorer 11 でもサポートされていないので、特に気にする人はいなかったかもしれません。

受けた影響

自分はソフトウェア開発用途で Mac を使うため、開封の儀を済ませてとりあえずパッケージマネージャーの Homebrew をインストールしようとしたわけです。 Homebrew はターミナルより以下のコマンドを実行するとインストールすることができます。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

このコマンドは https://raw.githubusercontent.com/Homebrew/install/master/install の Ruby で記述されたプログラムを取得し実行するのですが、 スクリプト中では某設計図共有サイトより Git リポジトリをフェッチしてインストールを行っています。ここでサーバーの TLS 証明書が信頼できずにアクセスできずエラーとなってしまいました。

企業内ネットワークだとよくある話だと思いますが、社内からのインターネットアクセスにはプロキシサーバを経由する必要があり、かつ特定サイトにアクセスするには別途自己署名のルート証明書を手元のマシンにインポートし、信頼するように設定が必要でした。今回すでにその設定は行っていましたが、なぜかアクセスできずでした。 切り分けのため curl コマンド で Git リポジトリに直接アクセスしても以下のようにエラーになってしまいます。Windows マシンからだと問題なくアクセスできるのに、です。

curl: (60) SSL certificate problem: self signed certificate in certificate chain
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

これは Catalina があやしいなぁと思いつつ、とりあえずエラーメッセージ + mac + Catalina とかでググってみると、以下の Symfony(PHP のフレームワーク) の CLI ツールの Issue が見つかりました。

https://github.com/symfony/cli/issues/146

コメントを読んでいくと、ここで先の Apple の公式発表ページ を発見したわけです。

何が問題だったか

実は問題は、ニュースサイトの見出しにあったSHA-1ではありませんでした。前述の Apple の公式発表や英語のニュースサイトをちゃんと読むと、SHA-1 以外に無効となる条件が書かれているのです。以下、原文の引用です。

All TLS server certificates must comply with these new security requirements in iOS 13 and macOS 10.15:

  • TLS server certificates and issuing CAs using RSA keys must use key sizes greater than or equal to 2048 bits. Certificates using RSA key sizes smaller than 2048 bits are no longer trusted for TLS.
  • TLS server certificates and issuing CAs must use a hash algorithm from the SHA-2 family in the signature algorithm. SHA-1 signed certificates are no longer trusted for TLS.
  • TLS server certificates must present the DNS name of the server in the Subject Alternative Name extension of the certificate. DNS names in the CommonName of a certificate are no longer trusted.

Additionally, all TLS server certificates issued after July 1, 2019 (as indicated in the NotBefore field of the certificate) must follow these guidelines:

  • TLS server certificates must contain an ExtendedKeyUsage (EKU) extension containing the id-kp-serverAuth OID.
  • TLS server certificates must have a validity period of 825 days or fewer (as expressed in the NotBefore and NotAfter fields of the certificate).

SHA-1 以外にもいくつかNGとなる条件が書かれています。

  • RSA使う場合はキーサイズは 2048bit 未満だとNG
  • ハッシュアルゴリズム SHA-1 だとNG(これが記事の見出しにかかれている点)
  • 証明書の中に SAN(Subject Alternative Name) で DNS 名が入っていないとNG(CommonName ではNG)
  • 加えて 2019年7月1日 以降に発行された証明書の場合は、
    • EKU(ExtendedKeyUsage) にサーバ認証(1.3.6.1.5.5.7.3.1) が入っていないとNG
    • 有効期間が 825日(27ヶ月) より長いとNG

というのが無効となる条件だったわけです。

実際、自分がエラーとなってしまった証明書の中を見てみると、上記条件の一つに引っかかっていました。。。

解決方法

原因が分かったものの、自己署名のルート証明書で署名されるサーバー証明書側(つまり、社内のプロキシサーバー側)を対応してもらう必要があります。そのうち対応してくれるとは思いますが、とても待ってられません:persevere: というわけで別の対応を考えました。

やっては駄目な方法

たとえば curl だと -k--insecure オプション指定し、アクセス先サーバー証明書の検証を行わないという方法があります。が、これだとニセサイトにすり替えられても検知できず超危険です。絶対にやめましょう

MITM

MITM(Man-in-the-middle attack:中間者攻撃)という TLS のようなセキュアチャンネル上の通信の盗聴方式があります。これは、クライアントとサーバー間の通信にて、間で一旦セキュアチャンネルの通信を終了して平文に復号化しつつ、再度暗号化してクライアントに流すという方式です。この方式を TLS で利用し、先の Apple の信頼済み証明書の要件 に合うように自己署名のサーバー署名書を生成すればうまくいきそうです。ただし、 Apple が駄目よと言っているリスクのあるサーバー証明書を受け入れてしまうのでその点は注意。あくまで妥協案であり、プロキシサーバー側が正式対応されるまでのワークアラウンドです。

というわけで実装してみた

Go 言語は自前で TLS を実装していることで知られています([go-nuts] Why did Go implement its own SSL library?)ので、Apple のこの変更の影響を受けずに TLS を処理することができます。というわけで Go で自前のプロキシを実装してみました。幸いにも MITM のサンプルは、作って学ぶ 「Https Man in The Middle Proxy」 in Go などがありますので参考にして実装することができます。今回、下記のようなシーケンスで実装しています。ざっくり処理概要を言うと、

  • 初回アクセス時に自前プロキシにて別途バックエンドに TLS 接続を行いサーバー証明書が Apple 要件を満たすかチェックし、問題なければ MITM せずにそのまま通信
  • Apple 要件に引っかかってしまう場合は、要件を満たすサーバー証明書を生成した上で MITM した通信を行う

image.png

なお、今回作ったものをもうちょいブラッシュアップ&pacファイル対応など追加して作ったものをこちらに置いています。同じようにこの TLS 問題に遭遇した方には役立つかもしれません:thinking:

まとめ

今回は Apple の TLS 証明書無効にまつわるお話の紹介でした。私が遭遇した社内プロキシ起因の問題だけでなく、開発環境やテスト環境などでオレオレなサーバー証明書を使って構築されている方は、同じように影響を受ける可能性があります(前述のSymfony CLI の Issue なんかはまさにツールで開発用途に TLS 証明書を生成しており、有効期限が長すぎて Catalina では使えなくなってしまった、という問題のようです)。

というわけで無事にセットアップできましたー!

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

Cloud FunctionsでGETリクエストを受けてHello Worldを返す関数をデプロイするメモ

なにこれ

タイトルの通り。
GCP初めて。
クイックスタート: gcloud コマンドライン ツールの使用を参考しながら自分用に削除したり、付け足ししたりしてます。

環境準備

  1. GCPのプロジェクトを新規で立てる
  2. Cloud Shellを起動する
  3. Google Cloud SDKを最新に更新する sudo gcloud components update
  4. Google Cloud SDKのベータ版機能をインストールする sudo gcloud components install beta

Hello World!を返す関数をデプロイする

プログラムはGo言語でサクッと書く。

package functions

import (
    "fmt"
    "net/http"
)

func HelloWorldHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        fmt.Fprintln(w, "Hello World!")
    default:
        http.Error(w, "Not Allowed", http.StatusMethodNotAllowed)
    }
}

コマンドでデプロイする

gcloud functions deploy HelloWorldHTTP --runtime go111 --trigger-http

関数を新規登録する時には確認有り

Allow unauthenticated invocations of new function [HelloWorldHTTP]?
(y/N)?

デプロイ完了したら以下が表示される。

Deploying.function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: HelloWorldHTTP
httpsTrigger:
  url: hogehoge
labels:
  deployment-tool: cli-gcloud
name: projects/hogehoge/locations/us-central1/functions/HelloWorldHTTP
runtime: go111
serviceAccountEmail: hogehoge@appspot.gserviceaccount.com
sourceUploadUrl: hogehoge
status: ACTIVE
timeout: 60s
updateTime: '2019-12-14T15:49:56Z'
versionId: '1'

動作確認

curl -X GET [url]
Hello World!

curl -X POST -d "hoge" [url]
Not Allowed

できてるー。

所感とか雑多なメモ

  • 簡単なAPI作成するのにめっちゃ便利。安いし。料金のドキュメント
  • 2019/12/14時点ではGoのランタイムは1.11が最新っぽい。1.12, 1.13は動かず。実行環境のドキュメント
  • Cloud Shell便利。だけどこいつ何者?
  • asia-northeast1リージョンにデプロイしたい
  • 日本語ドキュメントが全体的にAWSよりわかりやすい気がする。Cloud Functionsのドキュメント
  • コード変更変更してからデプロイまでするフローどうやって管理するんだ?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golangでmapの中の構造体を書き換えようとしたらcannot assign to struct field in mapになる

エラー

こういう構造体があって

type User struct {
  Id int
  Connected bool
}

User{0, true}という構造体をmapに追加してその要素を書き換えようとすると

users := map[int]User{0: User{0, true}}
users[0].Connected = false

cannot assign to struct field users[0].Connected in mapとエラーが出る。

解決策

構造体ごと書き換える

users := map[int]User{0: User{0, true}}
u, ok := users[0]
if ok {
  u.Connected = false
}
users[0] = u

値をポインタにする

users := map[int]*User{0: &User{0, true}}
users[0].Connected = false

コメント

他にいいやり方ないですかね。

参考

https://www.pospome.work/entry/2017/02/01/015856

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