20201223のGoに関する記事は10件です。

Goのnetパッケージを理解する~1日目~

はじめに

Goのパッケージの中でも、苦手意識しかないパッケージの一つであるnetパッケージの勉強をしていきます。

netパッケージとは

netパッケージは、TCP/IP、UDP、ドメイン名解決、UNIXドメインソケットを含むUnixネットワークソケットへのポータブルインタフェースを提供します。

引用:http://golang.jp/pkg/net

とりあえずサーバの起動

main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
}

これで、8080番ポートにアクセスすると、Hello Worldと出力されるはずです

一旦、ここまでで、

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

配属直後に go get したのに command not found で躓いた話

忙しい人向け

解決策①: パスを通す

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

単純にパスが通っていないだけであればこれで解決するだろう。

解決策②: go get で $GOPATH/bin に落としたバイナリを $GOROOT/bin にコピーする

しかし、こっちが本記事の結論。

Goのバージョンを切り替えやすくするために $GOROOT にシンボリックリンクを貼っていて、実行環境にバイナリ(実行ファイル)を go get できていなかった。

解決策はこちら

経緯

当時の私は新たな MacBook PRO を手に入れ(貸与され)、配属先でGo製のAPIを開発することになっていたため、ワクワクしながらGoの開発環境を構築していた。

(後々躓くことも知らずに)言われるがままに環境構築を行い、ビルド、手元でユニットテストを回した。
通った!
「よし、これで開発者としてチームの一員になれるぞ...」
その日はそのまま退勤。「明日から頑張るぞ!」という決意と共に会社を後にした。

さらに、参画することになっているプロジェクトでは sqlboiler というORMを使っているという情報を手に入れていた。生のSQLならまだしも、ActiveRecord以外のORMをまともに扱ったことがないこともあり、早速 sqlboiler を触ってみようと考えていた。

やってみる

以上を参考に、必要なパッケージを go get していく。

$ go get -u -t github.com/volatiletech/sqlboiler
$ go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql

あらかた必要になりそうなもののインストールは済んだので、 sqlboiler コマンドの実行!

$ sqlboiler psql

しかしここでなぜか sqlboiler がないと怒られる

$ sqlboiler psql
-bash: sqlboiler: command not found

command not found ... ?
んん ... ? go get したんだけどなと思いつつ確認する。

$ ls $GOPATH/bin
sqlboiler-psql*    sqlboiler*    その他省略

あれ?あるぞ?なぜ?
その後もあーだこーだしてるうちに時間が溶けてゆく。

原因を探る

まずはGOPATHの確認

$ echo $GOPATH
/Users/y-kanai/go

ふむ。
GOROOTは?

$ echo $GOROOT
/Users/y-kanai/goroot

goroot...? ってのはなんだ?
そんなもの知らんぞ?ってことで、

drwxr-xr-x   5 y-kanai  931577470     160  7 19 23:21 go/
drwxr-xr-x@ 21 y-kanai  931577470     672 12 23 16:51 go1.13.14/
lrwxr-xr-x   1 y-kanai  931577470      25  8  4 12:11 goroot@ -> /Users/y-kanai/go1.xx.xx/

あ!シンボリックリンクを貼ってるやんけ!完全理解!!!

ってことは、$GOROOT/bin に sqlboiler がないんだろうな..と

$ ls $GOROOT/bin
シーン ...

つまり、今回の原因はGoのバージョンを切り替えやすくするために $GOROOT にシンボリックリンクを貼っていて、実行環境にバイナリを go get できていなかったことにありました。

用意されたドキュメントをコピーしてペロって貼ってると中々気づかないものですね。

解決策

go get$GOPATH/bin に落としたバイナリ(実行ファイル)を、$GOROOT/bin にコピーする!

$ cp $GOPATH/bin/sqlboiler $GOROOT/bin/
$ cp $GOPATH/bin/sqlboiler-psql $GOROOT/bin/

$ ls $GOROOT/bin/
go*             gofmt*          sqlboiler*      sqlboiler-psql*

sqlboiler コマンドを実行!

$ sqlboiler psql
.
.
.

よし、ひとまず解決!
ここまで辿り着くのに1時間近くかかってしまいました。

振り返り

脳死コピーはダメですね。反省。以上!

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

go get したのに command not found で配属直後に躓いた話

忙しい人向け

解決策①: パスを通す

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

単純にパスが通っていないだけであればこれで解決するだろう。

解決策②: go get で $GOPATH/bin に落としたバイナリを $GOROOT/bin にコピーする

しかし、こっちが本記事の結論。

今回は、Goのバージョンを切り替えやすくするために $GOROOT にシンボリックリンクを貼っていて、実行環境にバイナリ(実行ファイル)を go get できていなかった。

解決策はこちら

経緯

当時の私は新たな MacBook PRO を手に入れ(貸与され)、配属先でGo製のAPIを開発することになっていたため、ワクワクしながらGoの開発環境を構築していた。

(後々躓くことも知らずに)言われるがままに環境構築を行い、ビルド、手元でユニットテストを回した。
通った!
「よし、これで開発者としてチームの一員になれるぞ...」
その日はそのまま退勤。「明日から頑張るぞ!」という決意と共に会社を後にした。

さらに、参画することになっているプロジェクトでは sqlboiler というORMを使っているという情報を手に入れていた。生のSQLならまだしも、ActiveRecord以外のORMをまともに扱ったことがないこともあり、早速 sqlboiler を触ってみようと考えていた。

やってみる

以上を参考に、必要なパッケージを go get していく。

$ go get -u -t github.com/volatiletech/sqlboiler
$ go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql

あらかた必要になりそうなもののインストールは済んだので、 sqlboiler コマンドの実行!

$ sqlboiler psql

しかしここでなぜか sqlboiler がないと怒られる

$ sqlboiler psql
-bash: sqlboiler: command not found

command not found ... ?
んん ... ? go get したんだけどなと思いつつ確認する。

$ ls $GOPATH/bin
sqlboiler-psql*    sqlboiler*    その他省略

あれ?あるぞ?なぜ?
その後もあーだこーだしてるうちに時間が溶けてゆく。

原因を探る

まずはGOPATHの確認

$ echo $GOPATH
/Users/y-kanai/go

ふむ。
GOROOTは?

$ echo $GOROOT
/Users/y-kanai/goroot

goroot...? ってのはなんだ?
そんなもの知らんぞ?ってことで、

$ ls -la
drwxr-xr-x   5 y-kanai  931577470     160  7 19 23:21 go/
drwxr-xr-x@ 21 y-kanai  931577470     672 12 23 16:51 go1.13.14/
lrwxr-xr-x   1 y-kanai  931577470      25  8  4 12:11 goroot@ -> /Users/y-kanai/go1.xx.xx/

あ!シンボリックリンクを貼ってるやんけ!完全理解!!!

ってことは、$GOROOT/bin に sqlboiler がないんだろうな..と

$ ls $GOROOT/bin
シーン ...

つまり、今回の原因はGoのバージョンを切り替えやすくするために $GOROOT にシンボリックリンクを貼っていて、実行環境にバイナリを go get できていなかったことにありました。

用意されたドキュメントをコピーしてペロって貼ってると中々気づかないものですね。

解決策

go get$GOPATH/bin に落としたバイナリ(実行ファイル)を、$GOROOT/bin にコピーする!

$ cp $GOPATH/bin/sqlboiler $GOROOT/bin/
$ cp $GOPATH/bin/sqlboiler-psql $GOROOT/bin/

$ ls $GOROOT/bin/
go*             gofmt*          sqlboiler*      sqlboiler-psql*

sqlboiler コマンドを実行!

$ sqlboiler psql
.
.
.

よし、ひとまず解決!
ここまで辿り着くのに1時間近くかかってしまいました。

振り返り

脳死コピーはダメですね。反省。

Goの複数バージョンを切り替えながら使っている方、
他に良い方法があれば、ぜひご教授願います。以上!

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

Go言語 学習記録

Go言語とは

 2009年にGoogleが開発したプログラミング言語です。
 静的型付け言語であり手続き型言語に分類されます。

私がGo言語を学ぶ理由

 大きく分けて2つあります。

理由1. 違うタイプの言語に触れてみたいと思ったため

 私は今まで、いわゆる「オブジェクト指向」かつ「動的型付け言語」であるRubyとJavaScriptを主に勉強してきました。

○「動的型付け言語」と「静的型付け言語」の違いとは?
 ざっくり言うと、「変数を定義するとき、文字列型とか数値型などの型を指定するかどうかの違い」があります。
 動的型付け言語の場合、指定しませんが、静的型付け言語の場合だと指定する必要があります。

理由2.Goの将来性

 現在、Go言語の需要は伸びています。求人数などもそうですが、技術的に今後のトレンドになるみたいです。
 Goは、マイクロサービス(複数の独立したWebアプリケーションをまとめたWebサービス)開発向けの言語です。今、マイクロサービスが注目されており、今後Goが開発環境で使われていくことは間違いないでしょう。

んで、実際に学んだことについて

いや、本当に初歩の初歩ですが、アウトプットさせてください。。

Println(プリントライン)

Goが用意している「コンソールに出力せよ」という命令の1つ。

Println("こんにちは")
[コンソール]

こんにちは

変数定義

「var 変数名 データ型」で定義する。
変数を定義する時点で型を決めることは、自分にとって新鮮!

var name str = "佐藤"
var age int = 24

最後に

 こんな感じでアウトプットさせていただきました。なんか初心者の人が頑張っているなぁと思ってモチベーションを上がってくれたら幸いです。

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

UbuntuにGoをインストールする方法

最新版を調べる

https://golang.org/dl/

インストール

cd ~
wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.15.6.linux-amd64.tar.gz
rm -f go1.15.6.linux-amd64.tar.gz
echo 'PATH="/usr/local/go/bin:$PATH"' >> .profile
source ~/.profile

確認

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

go:embed 詳解 - 使用編 -

この記事は Go Advent Calendar 2020 23日目 と CyberAgent Developers Advent Calendar 2020 24日目 の記事です。

はじめに

来年の2月にリリース予定の Go1.16 にはいつものリリースと同じように興味深い新機能が多数追加される。その中でも特に注目されている機能として、Go のビルド済みバイナリに読み込み専用の静的ファイルを埋め込む go:embed ディレクティブがある。これまで静的ファイルをバイナリに埋め込むアプローチは種々提案されてきたが1、ツールやOSごとに埋め込み方がバラバラで、それぞれ使い方を覚えたり、特定ツールへの依存がどうしても避けられなかった。今回このディレクティブの導入により Go 公式として統一される形となる。

このディレクティブ導入が注目される理由として、上記のような歴史的な背景ももちろんあるが、次のような疑問もあると思う。

  • どのような機能があるか?
  • どう使うか?(逆にどう使えないか?)
  • ファイルはどこに埋め込まれているか?
  • ファイルはどのような形式で埋め込まれているか?
  • ファイルの埋め込み元から呼び出し元までどのような過程を辿るか?

そこで go:embed の使い方を説明する 使用編 と、どのような仕組みで実装されているか深ぼる 仕様編 の前編・後編構成に分け、本記事では前編の使い方に的を絞って説明していく。後編はお楽しみにということで後日公開することにする。

この記事で使用する Go のバージョンは go1.16beta1 darwin/amd64 で、執筆時点ではまだ Go1.16 が正式リリースされていないため、この記事の内容から更新される場合があることに注意されたい。

階層構造ファイルシステムの統一的インターフェース

go:embed の説明に入る前に、これまた Go1.16 で導入予定の io.fs パッケージについて述べておく。

階層構造のファイルシステムは、Dennis Ritchie や Ken Thompson がベル研究所で行った UNIX 研究の主だった成果 であり、今日ではオペレーティングシステムはもちろんのこと、Web の URL や ZIP などの書庫ファイル(Vim で一度くらいは開いたことがあるでしょう;))にまで、いたるところで使われている。それにしたがって、Go にも階層構造のファイルシステムに配置されたファイルを読み取る標準パッケージがそれぞれ実装されている。オペレーティングシステムのファイルを操作するのが os パッケージであり、ZIP は archive/zip パッケージ、静的ファイルのテンプレートから HTML を動的に生成するのが html/template パッケージであり、URL に対する静的アセットを直接返すのが http パッケージ(の File/FileSystem 構造体)だ。これらは階層構造として同様に扱えるにも関わらず、それぞれ実装が統一されていなかったため、なんらかの橋渡しが必要であった。次世代 UNIX として同じくベル研究所で開発された Plan 9 で階層構造のファイルシステムとして表現されるリソースをプロトコルやアーキテクチャに依存せずに統一的に扱おうとしたにも関わらずである。

そんな中、io/fs パッケージが流星の如く現れた。Plan 9 の意思を受け継ぎ()ようやく Go でもファイルシステムを統一的なインターフェースで扱えるようになったのだ。

io/fs の説明や使い方については @spiegel_2007 さんの『次期 Go 言語で導入される(かもしれない) io/fs パッケージについて予習する』 の内容がほぼそのままリリース予定なので、詳細はそちらを参照していただこう。

使い方

基本的な使い方

さて、ここから本題に入る。

まずはじめに、go:embed ディレクティブを有効にするために embed パッケージをインポートする。embed パッケージを直接利用しない場合はブランクインポートしておく。

import _ "embed"

次にファイルの埋め込み方について、go:embed ディレクティブは var で宣言した初期化されていない変数に対して埋め込むことができる。他のディレクティブと同様に //go:embed の間に半角スペースなどを挟んでしまうと通常のコメントとして扱われてしまうため、注意が必要だ。指定できるファイルはカレントディレクトリ配下のファイルで、相対パスで指定する。Windows のような半角円記号 ¥ で階層構造を表すプラットフォーム向けにバイナリをビルドする場合でも、*nix 系のように半角スラッシュ / で表す。

//go:embed hello.txt
var hello string

//go:embed hello/world.txt
var world []byte

go:embed ディレクティブで埋め込める変数の型は、string[]byteembed.FS の3種類であるが、前者2つと最後の1つでは埋め込み方に違いがある。

string[]byte は単一の go:embed ディレクティブによってファイルを読み込み、通常通り初期化を行った変数として扱うことが可能となる。直感に即す通り、複数の go:embed を定義するとコンパイルエラーになる。

一方で embed.FSgo:embed で埋め込むファイルを階層型ファイルシステムとして埋め込むことができる、すなわち io/fs.FS インターフェースを実装している構造体2で、単一または複数のファイルやディレクトリを埋め込むことが可能である。複数のファイルやディレクトリを指定するには、ワイルドカード * を使うか、複数行に分けて指定する。

//go:embed image/* template/*
//go:embed style/*.css
//go:embed html/index.html
var assets embed.FS

最後に go:embed ディレクティブを埋め込むスコープについてだが、グローバルで埋め込むことができるのはもちろん、ローカルすなわち関数内でも埋め込むことは可能である。ローカルで埋め込むのは若干違和感を覚えるものの、通常の変数の初期化と同じように考えるとごく自然である3

//go:embed global.txt
var global string

func f() {
    //go:embed local.txt
    var local string
    ...
}

go:embed ディレクティブの簡単な使い方の説明は以上となる。

さてここからは細かい注意点、もっと言うと「意外とこういった使い方をしてもコンパイルエラーにならない」例や逆に「コンパイルエラーにならなそうで実はコンパイルエラーになる」といった例を挙げていく。

コンパイルエラーにならない例

重複してファイルやディレクトリを参照する

重複して読み込まれるファイルやディレクトリは無視される。それ故、次のように string 型に複数の go:embed ディレクティブを指定してもコンパイルエラーにならない。

//go:embed hello.txt
//go:embed hello.txt
var hello string

test で埋め込む

文字通り test 内でも埋め込むことは可能である。

embed.FS の前に go:embed ディレクティブがない

単に空のディレクトリとして認識されるだけである。

var fs embed.FS

fmt.Println(fs)
// {<nil>}

go:embed ディレクティブとファイルを埋め込む変数の間にスペースがある

(ディレクティブではないが)cgo では、コメントによる C 言語コードの記述と import "C" との間にスペースがあると C 言語コードが認識されないのに対し、go:embed ではスペースがあってもきちんと認識される。もっとも、スペースが複数行に渡る場合は gofmt によって1行にフォーマットされるのだが。

//go:embed hello.txt

var hello.txt

go:embed ディレクティブとファイルを埋め込む変数の間にコメントがある

上記のスペースの例と同様、コメントがあっても問題はない。

//go:embed hello.txt
// --- comment ---
var hello.txt

コンパイルエラーになる例

空のディレクトリを参照する

go:embed ディレクティブを指定せずに embed.FS の変数を宣言した時のように、空のディレクトリも埋め込むことが可能であると思いきやコンパイルエラーになる。上記で散々 ディレクトリを埋め込む と述べて来たが、実は go:embed ディレクティブで埋め込む単位は ファイル であり、ディレクトリ自体ではない。go:embed ディレクティブを指定せずに宣言した embed.FS の変数は、元の構造体を見ていただければ分かるが、file 型のスライスをメンバに持ち、単にこのスライスに対して明示的に初期化しないが故に0個のファイルを持つ状態になるのに対し、空のディレクトリを埋め込む場合はディレクトリの先に1個以上ファイルがあるのを期待するためコンパイルエラーとなる。Git で空のディレクトリをコミットできないことを想像していただければ分かりやすいであろう。後編で詳しく触れるが、埋め込むファイルは package と同じような扱いであり、空のディレクトリである package をインポートしたときにコンパイルエラーになる(この場合もはや package と呼べるか怪しいが)のと同様に、空ディレクトリを埋め込んだときもコンパイルエラーになる。

ディレクトリのパスを / で終わる

これも埋め込むのはファイルであってディレクトリではないため、コンパイルエラーになる。ディレクトリ内のファイルを全て埋め込みたいのであれば、ワイルドカードで指定する。

embed をインポートせずに go:embed ディレクティブを使う

github.com/go-sql-driver/mysqlgithub.com/mattn/go-sqlite3 のような SQL Driver を使う際にしばしばブランクインポートをするのとは別の理由で、コンパイル時にそう定めているためである。

存在しないファイルを参照しようとする

これは特段説明はいらないだろう。

親ディレクトリを参照する

io/fs パッケージでは、依存するファイルが完全にそれ配下の階層構造ファイルシステムにあることを期待するため、親ディレクトリを参照可能にする .. をパスに含めることは禁止されている4embed パッケージも io/fs パッケージが提供するインターフェースに則った階層構造ファイルシステムの構造体を実装しているため、go:embed ディレクティブでも .. を使うことが禁止される。

カレントディレクトリを参照する

意外に思われるかもしれないが、カレントディレクトリを指す . も、io/fs パッケージの階層構造ファイルシステムで禁止されている。したがって embed パッケージも(以下省略)。これについては、禁止にする明示的な理由を示す文献を見つけられなかった。

イレギュラーなファイル

シンボリックリンクやデバイスファイルなど、一部のファイルは go:embed ディレクティブで埋め込むことができない。具体的には ls -l でファイル一覧を表示したときに -rw-r--r-- などといった形でファイルモードが表示されると思うが、その1文字目が通常ファイル - でないファイルは埋め込むことができない。他にどのようなファイルが埋め込めないかは ここ に記載されている。

その他

他にも go:embed ディレクティブで指定できないパターンはいくつかあるが、これを読んでいるあなた自身が見つけるために、楽しみは取っておこう。

おわりに

この記事では go:embed の使い方や、ファイルパスを指定する際の注意点について述べてきた。次回後編では go:embed の中身について覗いていく。


  1. ドラフトに書かれているだけでもたくさんある。 

  2. io/fs.FS インターフェースはファイルを開く Open 関数を備えるだけでファイルシステムとして最小限のことしかできない。embed.FS 構造体は io/fs.FS インターフェースに加えて、ファイルを読む io/fs.ReadFileFS と、ディレクトリを読む io/fs.ReadDirFS も実装している。 

  3. ローカル変数へのディレクティブによる埋め込みは、関数間で埋め込んだファイルを共有するか、関数ごとに埋め込んだファイルを分けるか、など問題が出てきたため Go1.16 では実装から削除される方向で進んでいる。 https://github.com/golang/go/issues/43216 

  4. https://tip.golang.org/pkg/io/fs/#ValidPath
    https://go.googlesource.com/proposal/+/master/design/draft-embed.md#Dot_dot_module-boundaries_and-file-name-restrictions 

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

GoによるFyneを使ったGUI開発

GUI開発は、コマンドラインアプリケーションよりも作成が複雑なのは当然だ。
Fyneは、Goの優れた設計を使用して、GUIの構築を単純かつ迅速にするとのことで、一応は、Fyneを選んだ。
ゆくゆくは、Go謹製のshinyを使えるようになりたい。
んで、以下のプログラムをGUIに移行したい。

りそなIB用CSVファイルまとめGoプログラム
minecraftのセーブデータを管理するGoプログラム

と言うことで、参加する予定は無かったアドベントカレンダだが、枠があったので急遽投稿することにした。
(空きに入ったとは言え、遅延させた感じが嫌だな・・・投稿忘れに思われるよ・・・悲しい)

入手(環境構築)方法

以下のコマンで使えるようになる。

コンソール
go get fyne.io/fyne

上記コマンドで環境構築が終わる。
以下はデモ設定

コンソール
go get fyne.io/fyne/cmd/fyne_demo
fyne_demo

以下がデモの起動直後画面。

demo.jpg

大概のオブジェクトは試せる。
※私の環境で、直接 fyne_demo を実行した場合、コマンドエラーになった。そのため、直接叩いて起動したのだが、Path設定がうまくいっていないのかもしれない。

Hello World

まずは、初歩の初歩。

サンプルコード
package main

import (
    "fyne.io/fyne/app"
    "fyne.io/fyne/widget"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウタイトル

    myWindow.SetContent(widget.NewLabel("Hello World!"))    // 文字入りラベルをウィンドウコンテンツに配置

    myWindow.ShowAndRun()   // アプリケーションの実行(Show & Run)
    // ここ以降に処理を記述する場合、アプリケーションの実行が終わるまで実行されない。
}

1.jpg

Fyneのパッケージ構成

型の種類によってパッケージを分けている。

fyne.io/fyne

すべてのFyneコードに共通の基本定義の提供。
データ型とインターフェースを含む。

fyne.io/fyne/app

新しいアプリケーションを起動するAPIを提供する。
通常、必要なのはapp.New()のみ。

fyne.io/fyne/canvas

Fyne内のすべての描画APIを提供する。
完全なFyne toolkitは、これらの基本的なグラフィカルタイプで構成されている(単純構成なキャンバス)。

fyne.io/fyne/dialog

確認やエラーなどのダイアログウィンドウは、このパッケージによって処理される。

fyne.io/fyne/layout

さまざまなレイアウト実装を提供する。
コンテナが利用できる。

fyne.io/fyne/test

このパッケージにて、アプリケーションをより簡単にテストできる。

fyne.io/fyne/widget

このパッケージにて、さまざまなウィジェットコレクションが提供される。
ウィジェットとの相互作用もこのパッケージで提供されている。

その他

その他の特徴

よく分かっていないが、理解したと思い込んでいる箇所をかいつまんで述べる。

クロスコンパイラ

OSごとにアイコンとメタデータが関連付けられており、それらの環境に必要な形式が求められる。
その複雑な環境に対して、fyneコマンドはtoolkitがサポートするOSであれば、対応できるようになっている。

"fyne package"を実行することで、各OSごとに異なるインストール形式のアプリケーションが生成されるのだろうか・・・(理解できず)。

WindowsOSの場合は、アイコン込みのexeファイルが作られる。
MacOSの場合は、appバンドルを生成する。
Linuxの場合は、tar.xzファイルを作る。

コンソール
go get fyne.io/fyne/cmd/fyne

go build
fyne package -icon mylogo.png

残念ながら私の環境ではできなかった(Pathを通す必要があるのだろう)。

上記サンプルコードの詳細

GUIが立ち上がるには、描画イベントを処理するランループ(Runloop・イベントループ)が必須になる。
Fyneでは、 Window.ShowAndRun() もしくは、 App.Run() で行う。
また、これは、main()関数の最後に呼び出す必要がある(サンプルコード参照)。
処理途中に呼び出した場合のそれ以降の処理は、ウィンドウが終了するまで実行されないことに注意すること。

このrunloopは、アプリケーション内で1回のみ実行可能なため、複数実行した場合エラーが発生する(1回の呼び出しは必須)。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")
    myWindow.SetContent(widget.NewLabel("Hello"))

    myWindow.Show() // ウィンドウ表示
    myApp.Run() // ウィンドウ実行
    tidyUp()    // ウィンドウ終了後に実行される。
}

func tidyUp() {
    // GUIウィンドウ終了後に呼び出される関数。
    fmt.Println("Exited")
}

コード上で App.Quit() を実行することで、アプリケーションの終了が出来る。
しかし、ソースコードから直接終了指示を出すことで予期しない出来事の発生があるため、ユーザからの呼び出しで使う必要がある(ウィンドウ上部のいつもの×ボタンね)。
※ソースコード上で、開発者が任意に呼び出すのは避けるべき。

さらなる詳細

ウィンドウは、App.NewWindow()を使用して作成され、Show()関数にて表示する。

一部抜粋
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウ生成
        
        
        
    myWindow.Show() // ウィンドウ表示
    myApp.Run() // ウィンドウ実行

上記サンプルコード通り、ShowAndRun()関数にて、ShowとRunの両方を兼ねた実行が出来る。
このShowAndRun関数は、fyne.Windowのヘルパーメソッドのひとつ。

一部抜粋
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウ生成
        
        
        
    myWindow.ShowAndRun()   // ウィンドウ表示と実行

子ウィンドウを作る場合は、Show()関数のみを呼び出す必要がある(ゴルーチン関数呼び出し先でできる)。

ゴルーチン関数

ここ以降の説明は、goroutine(並行処理)の知識が要求される。

キャンバスについて

Fyneでは、Canvasはアプリケーションが描画される。
各ウィンドウには、Window.Canvas()で描画するが、使わないことも出来る。
そもそもFyneで描画できるものはすべて、CanvasObjectの一種になる。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Canvas") // ウィンドウタイトル
    myCanvas := myWindow.Canvas()         // キャンバス生成

    text := canvas.NewText("Text", color.Black) // キャンバスに黒文字準備
    text.TextStyle.Bold = true                  // 太字
    myCanvas.SetContent(text)                   // 上記テキスト描画
    go changeContent(myApp, myCanvas)           // ゴルーチン関数呼び出し

    myWindow.Resize(fyne.NewSize(200, 100))
    myWindow.ShowAndRun()
}

func changeContent(a fyne.App, c fyne.Canvas) {
    time.Sleep(time.Second * 2)

    c.SetContent(canvas.NewRectangle(color.Black)) // キャンバスを黒で塗りつぶす(長方形固定)。

    time.Sleep(time.Second * 2)
    c.SetContent(canvas.NewLine(color.Gray{0x66})) // キャンバスに線引き

    win := a.NewWindow("子ウィンドウ")
    win.SetContent(widget.NewLabel("5 seconds later"))
    win.Resize(fyne.NewSize(200, 200))
    circle := canvas.NewCircle(color.Black) // キャンバスに円形(黒で塗りつぶし)
    circle.StrokeWidth = 8
    circle.StrokeColor = color.RGBA{0x00, 0x00, 0xFF, 0x00} // 円形の線は青色
    win.SetContent(circle)
    win.Show() // 子ウィンドウ表示
    time.Sleep(time.Second * 2)
    win.Hide() // 子ウィンドウ非表示

    time.Sleep(time.Second * 2)
    c.SetContent(canvas.NewImageFromResource(theme.FyneLogo())) // ロゴ表示
}

child.jpg

再描画させることもできるようだ?
canvas.Refresh(circle)
しかし、使用前後で違いが分からなかった。

canvas.NewLine は、デフォルトでは左上から右下に線を引く。

子ウィンドウを表示させるまでの過程が合っているのか不安だ。
また、githubから導入した外部のパッケージは、補完が効かず、大変不便を強いられる。

MSCode.jpg

MSCode特有と思うのだが、解消して欲しい。

複数のキャンバスについて

上記は1つのみ利用した。
しかし、それでは実運用に耐えられない。
複数のキャンバスを使うには、コンテナを併用する。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Container")

    text1 := canvas.NewText("Hello", color.Black)   // テキストオブジェクトの作成
    text2 := canvas.NewText("There", color.Black)   // テキストオブジェクトの作成
    text2.Move(fyne.NewPos(20, 20))                 // 2つ目のテキストオブジェクトの移動
    text3 := canvas.NewText("World", color.Black)   // テキストオブジェクトの作成
    container := fyne.NewContainer(text1, text2, text3) // 3つのテキストオブジェクトをコンテナに配置。

    myWindow.SetContent(container)  // 上記コンテナをコンテンツとしてウィンドウに設定
    myWindow.ShowAndRun()   // ウィンドウの表示及び実行。
}

当然コンテナもキャンバスオブジェクトの一部であるため、キャンバスのコンテンツに設定できる。
手順

  1. テキストオブジェクトの作成
  2. それを3回行う。
  3. 2つ目のテキストオブジェクト(text2)を移動させる。
  4. 3つのテキストオブジェクトをコンテナに配置する。

と言う流れになる。

2.jpg

オブジェクトの移動を行っているのは、レイアウトセットを使っていないのが原因。
要は、移動させていないtext1とtext3がかぶってしまっている。
以下は、グリッドレイアウト。

container := fyne.NewContainerWithLayout(layout.NewGridLayout(2), text1, text2, text3)

以下は、グリッドラップレイアウト。

layout.NewGridWrapLayout(fyne.NewSize(50, 50))

以下は、ボーダーラインレイアウト。
4つの引数は配置場所。不要であればnilを指定する。

layout.NewBorderLayout(top, bottom, left, right)

fyne.Layoutは、コンテナ内のアイテムを整理するためのメソッドを実装する。
2列のグリッドレイアウトを使用するようにコンテナーを変更する。
ウィンドウのサイズ変更にて、テキストがレイアウトによって変更されることが確認できる。
また、text2の手動変更したコードが無視されていることも確認すること。

3.jpg

ウィジェット

fyne.Widgetは、特殊タイプのコンテナになる(追加のロジックが関連付けられたようだが、意味不明)。
(WidgetRendererとも呼ぶようだ)。
よく分かっていないが、ウィジェットもキャンバスオブジェクトの一部。

widget.Entryをウィンドウコンテンツに追加する(以下の例は1つのみ)。
複数のウィジェットを追加するには、コンテナではなく、ウィジェットのセットを利用する。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("ウィジェット")

    myWindow.SetContent(widget.NewEntry())
    myWindow.ShowAndRun()
}

今回のウィジェットでは、文字入力できる箱だが、多バイトは入力できない。
※ダイアログでのディレクトリ名に日本語が含まれていた場合は表示されない(フォント指定で改善する?)。

4.jpg

しかし、ソースコードへのべた書きであれば表示が可能になっている。
今のところ困らないかな・・・。

一応は、環境変数である FYNE_FONT にフォントを指定することにより日本語表示も可能なようだ。

参考URL

大本(?)のfyneサイト
package fyne
Go 製 UI ツールキット Fyne で始めるクロスプラットフォーム GUI アプリケーション開発

バージョン

今回は1.4を使っているようだ。

以上。

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

ECSでService Discoveryを使ってみよう

皆さんこんにちは!
シュークリーム大好きエンジニアのくろちゃんです。

こちらの記事は、Applibot Advent Calendar 2020の23日目の記事です!
昨日は @h_km さんのUnityプロジェクトにおけるSmartBeatの活用例紹介でした。是非チェックしてみてくださいね!

背景

11月と12月の2ヶ月間、株式会社アプリボットさんで内定者アルバイトをしていました。
そのうち、初めの1ヶ月間はゲームの新規タイトル開発プロジェクトのサーバサイド開発チームでお世話になっていました。

そこで一任していただいたタスクを実装した経験を元に、ECSでサービスディスカバリを使うための実装方法についてアウトプットしようと思います!

プロジェクトが抱えていた課題について

僕がジョインしたプロダクトでは、Terraformを使ってECS on EC2で構築されていました。

構成としては、1つのService内にAPP側のコンテナとRedisコンテナが共存しているような構成になっており、下記の画像のような構成でAPIサーバが稼働していました。

ECSServiceDiscovery_origin.png

このような構成では下記のような課題が生まれてしまいます。

名称未設定のデザイン.png

  • RedisがEFSからデータをフェッチするのに遅延が発生すると、タイミングによってはApp側がNot Ready状態のRedisにアクセスしようとしてしまうため、タスク自体がエラーとして処理され再起動されてしまう
  • そもそもApp側のデプロイがされる度に新しいRedisを立ち上げる必要がない

どうしたか?

今回の場合は、データストア領域であるRedisコンテナがApp側のコンテナと同じタスク(もっというと同じサービス)内に存在している事が大きな問題となってしまっていました。

そこで単純ではありますが、RedisコンテナをApp側と完全に分離して別サービスに切り出すという対応をしました。
イメージとしては、こんな感じになります。

ECSServiceDiscovery_multiService.png

このような構成にする事で、Redis単体でヘルスチェックができるようになり、App側がデプロイされる度に新しいRedisが立ち上がるという大きなコストを払わなくて済むようになります。

ECS Service Discoveryの導入

ここからが本題になるのですが、今回のようにECSサービス間で通信をする場合にはService Discoveryという機能を使う必要があります。

Service Discoveryを使う事で、ECSのサービス単位に他サービスがアクセスするためのDNSが登録され、その名の通りサービス間の通信をよしなにやってくれるようになります。

ただし、対象となるECSサービスのタスク定義でネットワークモードがhostまたはbridgeを使用している場合はAWSドキュメントにも書かれている通り、AレコードではなくSRVレコードが登録されます。

サービスタスクで指定されたタスク定義が bridge または host ネットワークモードを使用する場合、SRV のレコードのみがサポートされる DNS レコードタイプです。各サービスタスクの SRV レコードを作成します。SRV レコードのコンテナ名とコンテナポートの組み合わせをタスク定義から指定する必要があります。

こちらのSRVレコードの扱い方が特殊で、アプリケーションの実装側で対応しなければならないという事実に気づかず、てんやわんやしてしまいましたw

SRVレコードというもの自体初めて触れた概念だったので、苦戦を強いられましたが最終的にはアプリケーション側でどのように実装すれば良いのか理解して実装する事ができたので、具体的なコード例を示しながら説明していきたいと思います!

SRVレコードを元にサービス検出する実装方法

まず前提知識をお伝えします。
今回僕が働いていたプロジェクトでは、開発言語にGolangを用いており、Dev環境・Product環境のように、環境ごとにRedisへの接続情報を切り替えて実装しているという状況でした。

そのため、今から出てくるコード例もあくまでGo言語の場合はこうやって実装するというモノになっていますので予めご了承ください。

SRVレコードはアプリケーション側でホスト解決をしてあげる必要があり、MongoDBなどのようにパッケージ側でSRVレコード検出を自動で行ってくれるモノも多く存在しています。

mongo-example.go
clientOpts := options.Client().ApplyURI("mongodb+srv://mongodb.example.com")
client, err := mongo.Connect(context.TODO(), clientOpts)
if err != nil {
    log.Fatal(err)
}
_ = client

しかし今回改修対象となったRedisにはそのような機能は組み込まれておらず、下記のように自前でDialerを設定してあげる必要がありました。

redis-srv.go
&redis.RedisConfig{
    UniversalOptions: GoRedis.UniversalOptions{
        Addrs:  []string{fmt.Sprintf("%s-redis.dev-ecs", cfg.Env)},
        Dialer: createDevRedisDialer,
        ...(中略
    },
}

// ----

func createDevRedisDialer(ctx context.Context, network, addr string) (net.Conn, error) {
    _, addrs, err := net.LookupSRV("", "", addr)
    if err != nil {
        return nil, exception.Stack(err)
    }
    if len(addrs) == 0 {
        return nil, exception.New(exception.UnExpected, "SRVレコードが1件も見つかりませんでした")
    }

    // サービスディスカバリに登録するのはRedisサービスのみであるためインデックスは0で固定
    const redisSrvRecordIndex = 0
    return net.Dial(network, fmt.Sprintf("%s:%d", addrs[redisSrvRecordIndex].Target, addrs[redisSrvRecordIndex].Port))
}

まずは、RedisのOptionsで設定しているAddrsについて説明します。
Service Discoveryを使う場合は、

[Service Discoveryで設定した名前].[DNS名前空間名]

というようなフォーマットで文字列を渡してあげる必要があります。

Addrsで指定したアドレスが、Dialerに指定しているcreateDevRedisDialer()addr(第3引数)にコールバック関数のように代入されます。(ちなみにnetworkには何が入ってくるかというと、"tcp"などのような使用したプロトコル情報が入ってきます)

addrを受け取ったcreateDevRedisDialer()では何をしているのかというと、

  1. netパッケージのLookupSRV()に受け取ったaddrを受け渡す事で、登録されているSRVレコードの一覧を取得している
  2. 取得したSRVレコードから該当のレコードを抽出(今回はRedisサービス1つのみを登録しているので必ずIndex番号=0)
  3. net.Dial()を使ってSRVレコードから抽出したTarget(ホスト):Port(ポート番号)に対してコネクションを張りに行く

こんなことをしています。

今回はRedisを取り上げてSRVレコードへの接続方法について見てきましたが、基本的な流れは一緒になると思うので、net.LookupSRV()を使ってSRVレコードを取得し、該当のサービスのレコードを取得してnet.Dial()するという実装を自前で用意してみてくださいね!

おわりに

ここまで読んでいただきまして、ありがとうございました!

ECSでService Discoveryを使う上で、SRVレコードというものを避けて通れない場面が今後も出てくるかと思いますが、自分自身今回実装した経験を忘れないようにしたいなと思ったので今回記事にできてよかったなと感じています。

Applibot Advent Calendar 2020 次回は @Sigsiguma さんです!お楽しみに!!!

それでは、良いクリスマスとお年をお迎えください!ではっ:wave:

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

goで ~~ is not a typeのエラーが出た原因と対応

goで独自で定義した型を使用していた時に~~ is not a typeといったエラーが出たのでその原因と対応です。

何があったのか

type responce struct {
    Ok                  bool      `json:"ok"`
    Messages            []message `json:"messages"`
}

var responce responce

var replyResponce responce  //responce is not a type

このように独自で定義した構造体を使っていたらresponce is not a typeと言うエラーが出てきました。

原因

変数を定義する時の型が1回目はtypeを参照しているが、2回目は1回目で定義した変数を参照している。

対応

定義した構造体とは別の名前をつける。

type responce struct {
    Ok                  bool      `json:"ok"`
    Messages            []message `json:"messages"`
}

var apiResponce responce

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

Firestore Emulator を使って GitHub Actions でテストを動かしてみる

この記事は フラー Advent Calendar 2020 の23日目の記事です。
22日目は @ujikawa1026 さんで
小さな開発組織のマネージャーになってから1on1で考えていることを少しだけ書く」でした。

はじめに

はじめまして。
今年の4月から、フラー株式会社のエンジニアとしてアルバイトをさせていただいております。
普段は、サーバーサイドエンジニアとしてGoを用いたAPI開発を行っています。

今回は、Firestore EmulatorとGitHub Actionsによる自動テストを作成したので、記事にまとめてみました。

きっかけ

3ヶ月ほど前にFirestoreとGoで書かれたAPIを作成する機会があり、
ついでにテストも書くことになったので、Github Actionsを使ってテストを自動化することにしました。

本題

今回作成したAPIはDB(Firestore)が絡んだものでした。
当たり前な話かもしれませんが、
テストを行う際に本番環境で使われるDBに接続するのはよく無いです。テストによってDBの中身に変更が加わったり、壊れるのを避ける必要があります。

テスト用のDBを用意する

Firestore Emulatorを使用して、テストに用いるDBを作成します。
Emulatorを使うことで、わざわざテスト用にDBを作成する必要がなく、
テストの度に同じ条件のDBをEmulatorに用意することができます。


Emulatorはgcloudコマンドを使って起動します。
コマンド名は、
gcloud beta emulators firestore
https://cloud.google.com/sdk/gcloud/reference/beta/emulators/firestore

使い方は次の通り

$ gcloud beta emulators firestore start [--host-port=HOST_PORT]

オプションでホスト名とポート番号を指定して、gcloudコマンドを実行することでFirestore Emulatorが起動します。

*注意点として、コマンド名にも書いてありますがbeta版です...

This command is currently in BETA and may change without notice.

使用する場合は、急な仕様変更に注意する必要があります。

Go で Firestore Emulator を使用する

GoでFirestore Emulatorに接続します。
Firestore PackageはEmulatorをサポートしており、GoでEmulatorを使うことができます。

https://godoc.org/cloud.google.com/go/firestore#hdr-Google_Cloud_Firestore_Emulator

This package supports the Cloud Firestore emulator, which is useful for testing and development.



Goでは以下のようにしてFirestoreのクライアントを作成します。

ctx := context.Background()
client, err := firestore.NewClient(ctx, "projectID")
if err != nil {
    // TODO: Handle error.
}

このとき、ホスト名とポート番号をFIRESTORE_EMULATOR_HOSTに定義しておくことで、優先的にEmulatorを接続するようになります。

https://github.com/googleapis/google-cloud-go/blob/e0be520e1a5e36548f82823602aacfd3277cb7e9/firestore/client.go#L64-L77

If this environment variable is defined, configure the client to talk to the emulator.

特にGoの方で明示的にEmulatorに接続する処理を書く必要はないです。

GitHub Actions で Firestore Emulator を起動する

GitHub Actions

.github/workflows/ディレクトリにワークフローファイルを置いておくことで、指定したイベントがトリガーとなってデプロイやテスト、その他様々なアクションを自動で行うことができます。
詳しくはドキュメント参照

今回は自動テストを追加するので、テストを行うワークフローを書いたtest.yamlファイルを.github/workflows/ディレクトリに置いておきます。

SetUp

テストで使用するツールのセットアップを行います。

Goのセットアップ

- name: Set up Go
  uses: actions/setup-go@v1
  with:
    go-version: "1.13"

参考したもの
https://github.com/actions/setup-go
https://github.com/mvdan/github-actions-golang
他にも設定があった気がします。

gcloudとFirestoreのセットアップは以下の通り

- name: setup gcloud
  uses: google-github-actions/setup-gcloud@master
  with:
    version: "latest"

- name: setup firestore emulator
  run: |
    gcloud components install beta --quiet
    gcloud components install cloud-firestore-emulator --quiet

gcloudのセットアップはgoogle-github-actionsのREADMEを参考にしました。

Firestore Emulatorのセットアップは公式に書いてある通りです。
必要なコンポーネントのインストール↓

$ gcloud components install [COMPONENT-ID]

COMPONENT-IDの検索↓

$ gcloud components list

オプションで--quietを指定しないと、インタラクティブモードが有効になって、セットアップが進まないです(たしか..)

Testのフローを追加

最後に、テストを実行するフローを追加します。
今回、テストの作成にはtestingパッケージを使用しました。
https://golang.org/pkg/testing/

テストを実行するには、適切なディレクトリに移動して次のコマンドを実行するだけです。

$ go test ./... 

また、テストを実行する前にEmulatorを起動しておく必要があります。
起動方法は上で書いた通りで、追加するフローは以下の通りです。

- name: Test
  run: |
    gcloud beta emulators firestore start --host-port=localhost:8080 &
    curl http://localhost:8080 --silent --retry 30 --retry-delay 1 --retry-connrefused
    go test ./... -v
  env:
    FIRESTORE_EMULATOR_HOST: localhost:8080

curlコマンドの部分では、Emulatorが立ち上がる前にテストが実行されるのを防ぐために、ディレイをかけています。
また、Emulatorに接続させるために、環境変数FIRESTORE_EMULATOR_HOSTを追加しています。


以上で、トリガーとして指定したイベントが発生した特に、Emulatorの起動からテストの実行まで自動で行ってくれるようになると思います。
ちなみに私は、プッシュするたびにテストが走るように設定しておきました。

最後に

初めて書きました。
次回(?)は、もっと綺麗にわかりやすく書けたらなと思います。

あと、ここ違う!とか、こうした方が良さそう
っていうところがあったら、指摘していただけると助かります:bow:

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