20190209のGoに関する記事は6件です。

[go-astilectron]D&DでGoモジュール側にファイルパスを受け渡す

GoでGUIアプリを作ってみたいと思い、go-astilectronでDrag and DropでファイルパスをGoのモジュール側に受け渡してみたのでメモ

まだastilectron自体にはD&Dをハンドリングするような機能は実装されていない(2019/02時点)ようだったので、少々泥臭い方法で実装してみた

所感

go-astilectronで作ってgo buildすれば単一のバイナリに固められるので、すごく便利
しかし、以下の点だけは少々気になった…

  • JavaScript側でastilectronオブジェクトを扱う必要があったが、その仕様が調べても出てこない
    • Devtoolを有効にすれば持っている関数名は調べられるが、当該関数をどのように呼び出すべきかがわからない…

処理の流れ

HTML, JavaScriptでdropイベントを検出
-> astilectron.sendMessage() でD&Dされたファイルのパスを文字列として受け渡す
-> Goモジュール側で文字列をパースする

ディレクトリ構成

% tree . -L 2
drag_n_drop
├── Gopkg.lock
├── Gopkg.toml
├── drag_n_drop
├── main.go
├── static
│   ├── index.html
│   └── main.js
└── vendor
    ├── astilectron
    ├── astilectron-v0.30.0.zip
    ├── electron-linux-amd64
    ├── electron-linux-amd64-v4.0.1.zip
    ├── github.com
    ├── golang.org
    └── status.json

ソースコード

Golang

package main

import (
    "fmt"
    "strings"

    ast "github.com/asticode/go-astilectron"
)

func main() {
    var a, _ = ast.New(ast.Options{})
    defer a.Close()

    a.Start()

    var w, _ = a.NewWindow("static/index.html", &ast.WindowOptions{
        Center: ast.PtrBool(true),
        Height: ast.PtrInt(600),
        Width:  ast.PtrInt(600),
    })
    w.Create()
    w.Show()

    w.OnMessage(func(m *ast.EventMessage) interface{} {
        // Unmarshal
        var s string
        m.Unmarshal(&s)
        pathes := strings.Split(s, ";")
        fmt.Println(pathes)
        return nil
    })

    a.Wait()
}

HTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <div id="dropzone">Drop files here!</div>
    <script src="./main.js"></script>
</body>
</html>

JavaScript

const elemDrop = document.getElementById('dropzone');

elemDrop.addEventListener('dragover', function(event) {
  event.preventDefault();
  event.dataTransfer.dropEffect = 'copy';
});
elemDrop.addEventListener('drop', function(ev) {
  event.preventDefault();

  files = event.dataTransfer.files;
  pathes = []
  for (let i=0; i<files.length; i++) {
    // ディレクトリやその他MIMEタイプが取れないものは弾く
    if (files[i].type === "") {
      continue
    }
    pathes.push(files[i].path);
  }
  // ファイルパスを";"で連結してastilectron側に渡す
  astilectron.sendMessage(pathes.join(';'))
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goの静的解析系ツールをGitHub Actionsで動かしてみた

Golangの静的解析ツールをGitHub Actionsで動かして、PRに通知してみた。GitHubTokenとかあまり考えなくていいからその辺の管理が楽そうですごくいい印象を受けた。

ただ、まだGithubActionsが理解できていない部分が多く、不備は多々ありそう・・・

TerraformのGithubActionsをかなり参考にした。(ほぼgolangに書き換えただけ)
hashicorp/terraform-github-actions
ほんと、ありがとうございます:bow:

TL;DL

結論から言うと、こんな感じファイルを .github/main.workflow に置くだけ。

.github/main.workflow
workflow "Golang Test Workflow" {
  on = "pull_request"
  resolves = [
    "go imports",
    "go vet",
    "staticcheck",
  ]
}

action "filter to pr open synced" {
  uses = "actions/bin/filter@master"
  args = "action 'opened|synchronize'"
}

action "go imports" {
  uses = "grandcolline/golang-github-actions/imports@v0.1.2"
  needs = "filter to pr open synced"
  secrets = ["GITHUB_TOKEN"]
}

action "go vet" {
  uses = "grandcolline/golang-github-actions/vet@v0.1.2"
  needs = "filter to pr open synced"
  secrets = ["GITHUB_TOKEN"]
  env = {
    FLAGS = "-shadow"
  }
}

action "staticcheck" {
  uses = "grandcolline/golang-github-actions/staticcheck@v0.1.2"
  needs = "filter to pr open synced"
  secrets = ["GITHUB_TOKEN"]
}

これで、こんな感じでPRに通知してくれる。

imports.png
staticcheck.png

少し詳細

上記のワークフローの中で、下記のレポジトリを呼び出して、そのレポジトリ内にあるDockerfileから、Dockerコンテナを起動し、テストを実行している。

grandcolline/golang-github-actions

なので実際は、上記のレポジトリの部分を実装していった。
(ただ、実装といっても、静的解析ツールを入れたDockerfileと、テスト実行のシェルスクリプトのentrypoint.shを用意するだけ・・・)

例えば、goimportsを動かすジョブは、

goimports/Dockerfile
FROM golang:latest

LABEL "com.github.actions.name"="go imports"
LABEL "com.github.actions.description"="Run goimports"
LABEL "com.github.actions.icon"="terminal"
LABEL "com.github.actions.color"="purple"

LABEL "repository"="https://github.com/grandcolline/golang-github-actions"
LABEL "homepage"="https://github.com/grandcolline/golang-github-actions"
LABEL "maintainer"="grandcolline <grandcolline@gmail.com>"

RUN apt-get update && \
    apt-get -y install jq && \
    go get -u golang.org/x/tools/cmd/goimports

COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

jqgoimportsを取得するDockerfileを用意する。
LABELなどの仕様は、GitHubActionsの仕様に従う。詳しくは、ここ

実行するシェルは、こんな感じ。

goimports/entrypoint.sh
#!/bin/sh
set -e
cd "${GO_ACTION_WORKING_DIR:-.}"

set +e
UNFMT_FILES=$(sh -c "goimports -l . $*" 2>&1)
test -z "${UNFMT_FILES}"
SUCCESS=$?
echo "${UNFMT_FILES}"
set -e

if [ ${SUCCESS} -eq 0 ]; then
    exit 0
fi

if [ "${GO_ACTION_COMMENT}" = "1" ] || [ "${GO_ACTION_COMMENT}" = "false" ]; then
    exit ${SUCCESS}
fi

FMT_OUTPUT=""
for file in ${UNFMT_FILES}; do
FILE_DIFF=$(goimports -d -e "${file}" | sed -n '/@@.*/,//{/@@.*/d;p}')
FMT_OUTPUT="${FMT_OUTPUT}
<details><summary><code>${file}</code></summary>

\`\`\`diff
${FILE_DIFF}
\`\`\`
</details>

"
done

COMMENT="## goimports Failed
${FMT_OUTPUT}
"
PAYLOAD=$(echo '{}' | jq --arg body "${COMMENT}" '.body = $body')
COMMENTS_URL=$(cat /github/workflow/event.json | jq -r .pull_request.comments_url)
curl -s -S -H "Authorization: token ${GITHUB_TOKEN}" --header "Content-Type: application/json" --data "${PAYLOAD}" "${COMMENTS_URL}" > /dev/null

exit ${SUCCESS}

自動でカレントディレクトリ(/github/workspace)にソースコードがcheckoutさてれいるので、goimports -l . でフォーマットがおかしいファイル一覧を取得し、ファイル毎にdiffを${FMT_OUTPUT}に整形しながら入れてあげて、最後にcurlでPRに通知する。
${GITHUB_TOKEN}などは、workflow側から入れてあげれば、自然な形で環境変数として使える。

環境変数などはここをみながら。

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

GitHub Link Card Creatorがカッコいいのでnpm scriptsに組み込む

はじめに

みんなにOSSを見てもらいたい人の為に、GitHubリポジトリのOGP的画像を自動生成してくれるサービスを作った
こちらの記事で紹介されているGitHub Link Card Creatorが素晴らしくカッコいいので、node.jsのnpm scriptsに組み込む方法を模索してみました。

対象とするユーザー

  • Go?なにそれ?
  • 普段はnode.jsを使っている。
  • ターミナルを触ったことがある。
  • 自作のGitHubリポジトリにリンクカードをつけたい。
  • macユーザーである。

この記事の環境

この記事は以下の環境を想定しています。各ソフトのバージョンが異なると、記事の内容は適用できない場合があります。ご注意ください。

  • macOS 10.14.3
  • node 8.11.4
  • go 1.11.5 darwin/amd64
  • Homebrew 2.0.0

この記事で解消したい問題

GitHub Link Card CreatorにはオフィシャルのWebアプリケーションがあります。
GitHub Link Card Creator

こちらのWebアプリケーションを利用すれば、画像の生成からリンクコードの出力までが一気にできます。
しかし、出力された画像URLがQiitaでは直接利用できないという問題があります。(参考 :issue#4)

現状では、生成された画像をQiitaの記事内にアップロードし、生成されたリンクURLを書き換えることで対応が可能です。
しかしQiitaに画像をアップロードしてしまうと、カード情報の更新のたびに再アップロードが必要になります。

この問題を解消するため

  • GitHub Link Card Creatorをローカル環境で動かす。
  • npm scriptsのタスクに組み込む。
  • 生成された画像をGitHub Pagesにプッシュする。
  • Qiitaの記事から画像を読み込む。

という組み込み作業を行ってみます。

Goとは

Go公式ページ

GoはGoogleが主導して開発しているプログラム言語およびその環境です。
正式な名称はGoですが、golangと呼ばれることもあります。
開発環境はオープンソースで、パッケージをインストールすればマルチプラットフォームで動作します。

設定

macOS環境で、Homebrewを経由してGoパッケージをインストールします。

Homebrewのインストール

Homebrew
HomebrewはmacOS用のパッケージマネージャーです。Homebrewからさまざまなパッケージをインストールできます。
すでにHomebrewを導入している人は、この項目をスキップしてください。

このスクリプトをターミナルに入力すると、Homebrewがインストールされます。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールが成功したか確認するためにバージョンを表示してみます。

brew -v
Homebrew 2.0.0

バージョン番号が表示されたら無事インストール成功です。

Go環境の構築

インストール

先ほどインストールしたHomebrewを利用して、Goのパッケージをインストールします。

brew install go

パッケージのサイズが100MB以上ありますので、少し処理に時間がかかります。ゆっくりお待ちください。

go version
go version go1.11.5 darwin/amd64

こちらもバージョン情報が表示できればインストール成功です。

GitHub Link Card Creatorのインストール

GitHub Link Card Creatorパッケージは、以下のコマンドでインストールできます。

go get github.com/po3rin/github_link_creator/cmd/repoimg

ホームディレクトリ直下のgoフォルダーの中にファイルが保存されていれば、インストールは成功です。

パスの設定

GitHub Link Card Creatorはターミナルからコマンドrepoimgで呼び出すことができます。
このコマンドが通るように、ターミナルにパスを通す必要があります。

このパスはホームディレクトリ直下の.bash_profileというファイルに保存されています。
このファイルに以下の2行のパスを追加します。

export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

ターミナルから.bash_profileを編集する方法はこちらの記事をご参照ください。
MacでPATHを通す

また、Finderとお好きなテキストエディターを使って編集することもできます。

Finderから「移動」→「フォルダへ移動…」を選択し

~/.bash_profileへ移動します。
ここで表示されたファイルをお好きなテキストエディターで編集してください。

ターミナルの再起動

.bash_profileの変更はそのままでは反映されません。再読み込みのコマンドを実行するか、ターミナルを再起動する必要があります。

再読み込みのコマンドは以下の通りです

source ~/.bash_profile

.bashrcや.bash_profileなどの変更設定をすぐに反映させたい

WebStormやVS Codeなどのターミナルを内包しているソフトも、再起動をする必要があります。

以下のようなエラーが出る場合は、.bash_profileの反映ができていません。ソフトやmacの再起動を試してみてください。

bash: repoimg: command not found

ここまでの作業でGitHub Link Card Creatorがターミナルから呼び出せるようになりました。

repoimg -n <GitHubのユーザー名>/<リポジトリ名>

で画像が生成されれば成功です。

GitHub Pagesの設定

次に、GitHub Pagesの公開設定を行います。


まずは作成済みのリポジトリのWebページにアクセスし、Settingsを開きます。


次に、GitHub Pagesの設定項目に移動し、Sourceをmaster Branch /docs folderに変更します。

これでリポジトリの./docs以下がhttps://<ユーザー名>.github.io/<リポジトリ名>/でアクセスできます。

npm scriptsに統合

最後に、npm scriptsにrepoimgコマンドを組み込みます。

package.json
  "scripts": {
    "doc:card": "repoimg -n <GitHubのユーザー名>/<リポジトリ名> -o ./docs/card.png"
  }

このスクリプトで./docs/card.pngが生成されます。
このファイルをプッシュすると以下のURLでアクセスができます。

https://<ユーザー名>.github.io/<リポジトリ名>/card.png

このURLをGitHub Link Card Creatorで生成される埋め込みコードに組み込むと

無事Qiitaの記事からGitHub Pagesの画像ファイルが読み込めました!

以上、ありがとうございました。

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

a[:0] と append の秘密

http://zetamatta.hatenablog.com/entry/2019/02/05/161003 より転載)

io.Reader のプリプロセッサな io.Reader を作るの中で、"tidwall/transform"で使われていると紹介したテク:a = a[:0] は領域のサイズをリセットするが、a = make([]T,0,cap(a)) と違って、使っていたメモリブロックを再利用するため、allocation 回数を削減できるというものだった。

だが、旧a の領域が他で使われていないかを気にせず、無頓着に使うと append でおかしいことになる。たとえば

a := []string{ "a","b","c" }
b := a[:0]
b = append(b,"1")

fmt.Printf("%+v\n",a)

とすると a の内容が [1 b c] となる。これは append が領域を上書きで使用するためだ。

append 側で他所で使われているかチェックしてくれたらよさそうな話ではあるが、おそらく物理的に無理だろう。というのも参照カウンタ方式ではなく、マーク&スイープ方式の Garbage Collector を使っている場合、参照されている場所が自分を含めて2個以上あるか、調べようがないのだ。(GCの中身のコードならばできなくもないだろうが…)

となると、常に他で使われているという前提で append を実装しないと、上のような状況は避けられないわけだ。そういう append ちょっと作ってみよう。

package main

import (
    "fmt"
)

func appendStr(a []string, b string) []string {
    r := make([]string, len(a)+1)
    copy(r,a)
    r[len(a)] = b
    return r
}

func main() {
    a := []string{}
    for i := 0; i < 10; i++ {
        a = appendStr(a, fmt.Sprintf("%d", i))
    }
    fmt.Printf("%+v\n", a)
}

こんな実行効率わるそうなの、みんな使いたいと思うだろうか?結局、みんなオリジナル append を使うことになるだろう?(でも、スクリプト言語では効率が悪くても平気でやってそうではある)。


こういった append の挙動は一見奇異なものに見え「Go言語は糞」と罵られる一因ではある。だが、C言語を知る者は append は realloc と対応する関数だから、領域拡張前の a が正常に使える保証はなく、使うべきではないと直感的に分かっている人が多い。

人によって、Go言語の評価が「サイコー」or「糞」と正反対になるのは、そういうあまり言及されない前提知識や慣れ(訓練されたC言語プログラマはエラーチェックコードが多くてもまったく気にしない等)の有無が大きいのではないだろうか。

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

素人が低層から理解するGo Web開発(2)【TCPsocketのソースを覗く】

このシリーズについて

  • ド素人が
  • GoによるWeb開発を
  • どのコードもブラックボックスにならず
  • プロトコル, ネットワークの流れもふわっと分かり
  • 実際にWebアプリをデプロイする

過程を記録していきます。これで第2回目です。

前回までと今回

前回(第1回)socket を使って低レイヤを意識しながらHTTPリクエストを送ってみようということで

  • インターネット層
  • トランスポート層
  • socket

の概念を流し見して socket を送るコードが何をしているのかぼや~っと把握しました。

今回(第2回)はコードに用いられているインターフェース、構造体の中身を見ていこうと思います。

ResolveTCPAddr

前回の最後では、リクエストを送るプログラムの1行目

  tcpAddr, err := net.ResolveTCPAddr("tcp4", "almi.tokyo:80")
  checkError(err)

ResolveTCPAdrr ってなんだ?」って話でしたね。中身を覗いていきましょう。場所は src\net\tcpsock.goです。

func ResolveTCPAddr(network, address string) (*TCPAddr, error) {
    switch network {
    case "tcp", "tcp4", "tcp6":
    case "": // a hint wildcard for Go 1.0 undocumented behavior
        network = "tcp"
    default:
        return nil, UnknownNetworkError(network)
    }
    addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address)
    if err != nil {
        return nil, err
    }
    return addrs.forResolve(network, address).(*TCPAddr), nil
}

return : *TCPAddr

まずこの関数が何を返すのか見てみましょう。返り値の型は*TCPAddr, つまりTCPAddrのポインタです。多分TCPアドレスのことでしょう。

TCPAddrの定義

同じくsrc\net\tcpsockの中で定義されています。楽でタスカル。

// TCPAddr represents the address of a TCP end point.
type TCPAddr struct {
    IP   IP
    Port int
    Zone string // IPv6 scoped addressing zone
}

コメントを見ると TCP エンドポイントを表現する構造体だ、とされていますね。
つまりトランスポート層が責任を持つ範囲の一番端の住所を表現します。前回見たとおり、IP address(ここでは IP で定義されてるやつかな) にポート番号を付け加えたものとなっていますね。

bird-eye.jpg

この画像で言えば IP によってインターネット層が各端末(サーバ、スマホ、デスクトップ)の位置を識別します(オレンジの枠)。

そしてポート番号によって、端末上のどのアプリケーションのどのプロセスか(HTML, Amazon, Twitter, Chrome, Evernote...)を識別します(青の枠)。

結果的にIP + port番号 は TCPAddress とみなせることになります。

IPv6対応のために(?)Zoneが追加されていますが、stringとしてなのであとでどこかでParse(Marshal?)するんでしょう。今は置いておきましょう。

特にひねったこともなくフツーに定義されていましたね。一応 IP型 の中身もみてみましょう。

IP

src\net\ip.goにありました。

// An IP is a single IP address, a slice of bytes.
// Functions in this package accept either 4-byte (IPv4)
// or 16-byte (IPv6) slices as input.
//
// Note that in this documentation, referring to an
// IP address as an IPv4 address or an IPv6 address
// is a semantic property of the address, not just the
// length of the byte slice: a 16-byte slice can still
// be an IPv4 address.
type IP []byte

はぁ。
どうやら IP adrress は単純な構造体としてではなく、このパッケージ全体でセマンティックに定義されるもののようです。

......とりあえずIPの役割が分かっているので良しとしましょう(´;ω;`).
(後で帰ってくるかも)

次に引数を見てみます。

arguments : network, address

 network, address string

ですね。ただの文字列なんですが、入力例のとおり network ではプロトコルの種類(tcp4, tcp6, tcpのどれか), address ではURL + port番号を渡していました。

引数はただの string なので、振舞を知るには内部処理を見る必要がありますね。

内部処理

  switch network {
    case "tcp", "tcp4", "tcp6":
    case "": // a hint wildcard for Go 1.0 undocumented behavior
        network = "tcp"
    default:
        return nil, UnknownNetworkError(network)
  }
  addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address)
  if err != nil {
    return nil, err
  }
  return addrs.forResolve(network, address).(*TCPAddr), nil

switchブロック

まずswitchブロックで "tcp" のプロトコルの種類の指定以外をはじいてます。特別な処理はしていなく、networkに変な文字列が入っていたらエラーを返すだけです。

Goにおけるリゾルバ : Resolver

  addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address)

DefaultResolverのメソッドに引数を渡してaddrsを取得しています。
デフォルトの「リゾルバ」。実はこのリゾルバ前回(第1回)インターネット層を勉強した際に記述をはしょったモノの1つでした。

トランスポート層からデータを渡される際、インターネット層は DNS サーバ群による名前解決システムで URL から IP address を取得します。

例えばスマホから自宅のWifiでqiita.comにアクセスする時、自宅のルーターは ONU にリクエストのデータと、qiita.comのヘッダーを付けて渡します。(トランスポート層からインターネット層へ)

localdns.jpg

ONUは(ローカルな) DNS へ問い合わせを行います。このDNSは一定の範囲のIP addressとURLを紐づけて知っていて、自分が知っている範囲のURL, IP であればリクエストに従った処理を行います。

もしDNSにとって未知のURLに問い合わせがあった場合はどうなるのでしょう。

globaldns.jpg

その場合よりグローバルなDNSへと問い合わせを行います。グローバルDNSはローカルDNS群の住所を知っていて、リクエストされていたIP address を管理しているDNSへと中継を行います。

後はこの繰り返しです。(各DNSはキャッシュを持っていたりするんですが大体こんな感じです)

そしてこの一連の流れを名前解決といい、DNSへ問い合わせて実アドレスを取得する機能を resolver と言います。Go はこれをサポートしています。
定義を見てみましょう。src\net\loolup.go より

var DefaultResolver = &Resolver{}

type Resolver struct {
    PreferGo bool
    StrictErrors bool
    Dial func(ctx context.Context, network, address string) (Conn, error)
    lookupGroup singleflight.Group
}

contextに関しては......保留させてください。なんかGoプログラム自体の処理にかかわるインターフェースのようなんですが、ソース読んでもサッパリです。

LookUpIPAddr

DefaultResolverinternetAddrListインターフェースを持っていて、この中でメインの役割を果たすのがLookUpIPAddrです。
コードを見てみましょう。場所はsrc\net\lookup.goです。

// LookupIPAddr looks up host using the local resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]IPAddr, error) {
  ...
}

掘れば掘るほど出てきますね。これ以上やると終わらなくなるのでとりあえずここでストップします。(素人ではさらっと読めるコードじゃなかったです)
ローカルリゾルバを用いてホストを探す関数のようです。戻り値は([]IPAddr, error)です。

ResolveTCPAddrのまとめ

network, address を引数にとって、TCPAddrのポインタを返す関数だということが分かりました。

TCPAddrIPport番号の組み合わせで表現されていて, IPはbyte型で, パッケージ全体でセマンティックに定義されていました。いつか全部さらいたいですね。

関数の中ではLookUpIPAddrが呼び出されローカルリゾルバを再帰的にコールして(途中でエラーが無ければ)最後まで名前解決を行います(フルリゾルバ)。
これもLookUpAPAddrの関数の中まではまだ追っていません。

と、言うことで、ResolveTCPAddrは実際にネットワークに問い合わせを行いTCPアドレスを取得する関数だということが分かりましたね。
まあ関数名で大体予想がつくんですが。

力尽きました

次回はDialTCPを見ていきます。

ResolveTCPAddrが名前の通りリゾルバだったので、十中八九これはTCPコネクションを確立してソケットインターフェースを返す関数なんですが......一応中身を見ていきます。

果たして第何回目でアプリのデプロイまでたどり着くのでしょうか・

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

久しぶりにGoでAPIサーバーを書いてみる

概要

  • Go1.11からの新しい依存解決システムvgoはよい
  • 昔はGoでMongoDBを使うときはmgoを使っていたが、Go1.10+ならMongoDBの提供するmongo-go-driverが使える
  • freshを使えばGo製サーバーもライブリローディングしながら開発できる
  • 自分と同じようにGoを敬遠していた人もそろそろ始めどきかも

はじめに

久しぶりにGoを使ってJSON APIサーバーを書く機会がありました。

Goは1.6とかの時代にちょっと触っていたのですが1、Python, Ruby, Nodeといったスクリプト系言語を主な使用言語としている自分にはだるいな…と思うことが多々ありましたし、ディレクトリの場所が規定されてしまうというのも嫌だなぁと感じていました。
というわけでしばらくGoとはサヨウナラをしていました。

しかし最近バックエンドでGoを本格利用している会社の方が、最近Goもいろいろ変わっていてそろそろ再チャレンジするにはいい機会ですよ!と教えてもらいました。
特にGo1.11ではvgoという新しいバージョン管理システムが試験的に導入されており、これを使えばGOPATHはもういらないとのこと。

スキルセットとして低レイヤーなことができてWASM時代にも備えられる言語に触れておきたい、でも言語仕様が難しいやつはちょっと…という自分にとってGoはまさしく求めているものだという自覚はありました。
ちょうどプロトタイプを一個作ってみるといった案件があったため、これは何かの機会だと思い再びGoにチャレンジしてみました。

Docker

Goのバージョンを持っているPC全部でそろえるのは面倒なのでDockerを使うことに。
これだと別にvgo関係なくGOPATHどうこう気にする必要はなかったですね、、。

Dockerfile

FROM golang:1.11
WORKDIR /go/src/app

RUN go get github.com/pilu/fresh
ENV GO111MODULE=on
CMD ["fresh"]

ENV GO111MODULE=onによってvgoが使えるようになりますが、その後 go get がうまく動かなくなるので先に go get しています。
vgoが有効だと、相対パス記法でのインポートなど古い仕様を切り捨てるようになっているようでその影響かなぁと。

docker-compose.yaml

version: '3'
services:
  app:
    build:
      context: ./app/docker
    ports:
      - 8081:8081
    volumes:
      - ./app/src:/go/src/app
    command: fresh

開発ディレクトリ(./app/src)をDockerfileで指定した作業ディレクトリにマウントします。これでfreshを実行すると、自動的にmain.goが立ち上がるようです。freshについては後述。

まだビルドしてバイナリを出力するところまでは必要としていないので、まだその対応はしていません。

vgo

新しい依存管理システムです。importを書いておけば明示的にinstallとかしなくても必要があれば勝手にダウンロードしてくれる、Rubyのbundlerやnodeのnpmのようにバージョンロックできるというのが特徴です。

これを有効にすると相対パスを使ったインポートが使えなくなります。

:no_good:

main.go
import (
    "./lib",
)
...

main.goと同じディレクトリにgo.modを作成し、そこでプロジェクト名を定義し、<プロジェクト名>/<パス>というようにインポートします。

:ok_woman:

go.mod
module app
main.go
import (
    "app/lib"
)
...

参考
go 1.11のmodules(vgo)が有効な環境で相対importが cannot find module for path でエラーになった話。

mongo-go-driver

GoでMongoDBを使う際は、mgoというライブラリを使うのが定番だったようです。しかしこれはメンテナンス停止が宣言されており、フォークされたものを使っているケースが多いみたいですね。

しかし、MongoDBからmongo-go-driverというのがリリースされています。昨年12月にようやくアルファからベータになったばかりです。
Go1.10以上ということなので、これからGo+MongoDBという構成を導入するプロジェクトはこれを使ってみてもよさそうです。

fresh

freshというツールを使えば、Railsで開発しているのと同じようにライブリローディングが使えます。同じようなことを実現するツールにはginというものもあります。(Webフレームワークのginとは別物)

どちらもあまり更新されていないようです…。が、あるのとないのとだと開発中の気持ちよさが全然違います。
Goをだるいと感じていた最大の原因がコードを変えたらプロセスを立ち上げ直す必要がある、だったのでそれがなくなると気分的にはRailsで開発しているのとそう変わりません。

ちなみに、freshを使った環境構築はこちらを参考にしました。
Go v1.11 + Docker + fresh でホットリロード開発環境を作って愉快なGo言語生活

私はginではなく昔触ったことのあるechoを使っていますが、全く同じ方法でOKです。


GOPATH云々はDockerを使えばもともと気にする必要がなかったわけですが :sweat_smile:
vgoはGoの依存解決の決定版になるのでしょうか。まだ軽く触っている段階ですが、だいぶ体験としてはよいですね。bundle installとかnpm installとか、よくよく考えたら明示的に呼び出す必然性ってないですね。
(昔depというのがあったように思うのですが、同じようにまた新しいのが出てきたりしないでほしいなー…と思います。)

自分と同じように、昔触ったことがあるけどちょっとめんどくさくなって…という方は、そろそろ再チャレンジしてみてもよいかもしれません。


  1. そのころGoで「世界で闘うプログラミング力を鍛える150問」を解いていたときの残骸です。https://github.com/k5trismegistus/cracking-the-coding-interview-150 

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