- 投稿日:2019-12-22T23:50:53+09:00
【Mac】goenvを使ったGoのインストール
はじめに
Macデビューしたのに当たって、Goの開発環境構築を"goenv"を使って行ったので、そのまとめです。
goenvとは?
goenvは、pyenvから複製されたGo用のバージョン管理ツールです。
プロジェクトに応じて、GoのVersionを変更がある場合などに、Versionを切り替えて開発をすることができます。goenvのインストール
まずは
goenv
をインストールする前にhomebrewをアップデートします。$ brew update
アップデートが終了したら、次に
goenv
をインストールしていきますが、最新のgoのVersionをインストールしたい場合は、HEAD
をオプションにつけた方が良いです。
(ちなみに後からでもgoenvのリポジトリを最新にすることは可能です。)$ brew install --HEAD goenv
goenv
がインストールできたら、~/.bash_profile
に下記の設定を追加します。~/.bash_profileexport GOENV_ROOT="$HOME/.goenv" export PATH="$GOENV_ROOT/bin:$PATH" eval "$(goenv init -)"これでTerminalから
goenv
がコールできるようになっているはずなので、Version
を確認します。
設定が有効になっていない場合があるので、その場合はShellの再起動を行って設定を有効にします。$ exec $SHELLここまで実行したら、goenvのVersionを確認します。
$ goenv --version goenv 2.0.0beta11Version情報が表示されれば、goenvの準備は完了です。
Goのインストール
次にgoをインストールしていきます。
今回は、私事で1.13.0で開発をする必要があるため、このVersionをインストールしていきます。まずは対象のVersionがリポジトリで管理されているかを調べます。
$ goenv install -l . . . 1.13.0 1.13beta1 1.13rc1 . .インストールする対象のVersionが見つかったら、
goenv
を介してgoをインストールします。
と言っても、ここまで来てしまえば、後は対象のVersionを指定するだけで、インストールが始まります。$ goenv install 1.13.0Goの環境設定
最後に使用するGoの設定を行います。
今回は先ほどインストールした1.13.0を使用するので、それを設定していきます。まずは先ほど、インストールしたVersionがLOCAL環境に存在しているか確認します。
$ goenv versions 1.13.0上記のように表示されればOKです。ですが、このままではLOCAL環境でGoを使うことができないので、対象となるVersionを指定して、Goを使える状態にしていきます。
まずは全体に適用するGoのVersionを指定します。$ goenv global 1.13.0文字通り、
goenv global
を指定することで、対象のVersionを設定することができます。
初回の設定では、$GOPATH
,$GOROOT
の設定が出来ていないので、設定を追加します。~/.bash_profile... export PATH="$GOROOT/bin:$PATH" export PATH="$PATH:$GOPATH/bin"上記を追加したら、.bash_profileを再度読み込みます。
$ source .bash_profile最後にGoのVersionが確認できれば、インストールは終了です。
$ go version go version go1.13 darwin/amd64
- 投稿日:2019-12-22T23:38:11+09:00
Go で Go を build して Go を Go で開発しよう
はじめに
有名な話ですが、Go は Go で開発することができます。(Go1.5 以降)
Go の開発を Go でできることは、メリットがたくさんあります。
- 可読性が高いため、他人の書いたソースコードのメンテナンスがしやすい
- 楽しい
- 自分の書くソースコードの品質を担保しやすいので、他人に自分のコードを理解してもらいやすい
- 楽しい
Go はクリーンでシンプルで、パワフルな言語で私は大好きです。
そんな自分が使っている好きなソフトウェアがどのように作られているかは、 とても興味があります。そんな Go を読めて、 build できて、デバッグできて、ソースコードが書けることはとても面白い活動です。
また、処理系の中身がわかると、よりプログラミング言語の気持ちがわかるようになります。というわけで、日本からももっと Go へコントリビュートしようぜという想いをこめて、
Go の build 方法と、ソースの変更をしてみた例を簡単に紹介します。環境
今回試した環境は以下です。
必要なものは、Go1.4 以降の go コマンドと、
Go のソースコードのみのため、環境が変わっても手順などはかわらないはずです。pure Go で、Go のバイナリが作れるって素敵ですよね。
- go command
- Go 1.13
- go source ver
- Go1.14beta1
- host OS
- Windows 10
- host Arch
- amd64
Go のソースコードはどうなっている??
Go のプロジェクト、およびソースコードは、以下の Git リモートリポジトリにて管理されています。
https://go.googlesource.com/go
ルートディレクトリは、以下のような構成になっています。
ディレクトリ名の通り、
src
以下にソースコードが登録されています。aki01@LAPTOP-8UTJLJ8V MINGW64 ~/work/src/go.googlesource.com/goroot (feature/print_gopher) $ ls -al total 320 drwxr-xr-x 1 aki01 197609 0 Dec 22 22:03 . drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 .. drwxr-xr-x 1 aki01 197609 0 Dec 22 11:19 .git -rw-r--r-- 1 aki01 197609 347 Dec 21 08:36 .gitattributes drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 .github -rw-r--r-- 1 aki01 197609 928 Dec 21 08:36 .gitignore -rw-r--r-- 1 aki01 197609 55383 Dec 21 08:36 AUTHORS -rw-r--r-- 1 aki01 197609 1339 Dec 21 08:36 CONTRIBUTING.md -rw-r--r-- 1 aki01 197609 84309 Dec 21 08:36 CONTRIBUTORS -rw-r--r-- 1 aki01 197609 1479 Dec 21 08:36 LICENSE -rw-r--r-- 1 aki01 197609 1303 Dec 21 08:36 PATENTS -rw-r--r-- 1 aki01 197609 1607 Dec 21 08:36 README.md -rw-r--r-- 1 aki01 197609 397 Dec 21 08:36 SECURITY.md -rw-r--r-- 1 aki01 197609 48 Dec 22 11:24 VERSION.cache drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 api drwxr-xr-x 1 aki01 197609 0 Dec 22 22:03 bin drwxr-xr-x 1 aki01 197609 0 Dec 21 08:37 doc -rw-r--r-- 1 aki01 197609 5686 Dec 21 08:36 favicon.ico drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 lib drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 misc drwxr-xr-x 1 aki01 197609 0 Dec 22 11:31 pkg -rw-r--r-- 1 aki01 197609 26 Dec 21 08:36 robots.txt drwxr-xr-x 1 aki01 197609 0 Dec 22 22:06 sandbox drwxr-xr-x 1 aki01 197609 0 Dec 22 22:04 src < ココ drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 testsrc ディレクトリは以下のようになっています。
そこそこ Go を知っている人ならば、どこにどのソースがあるのかわかるようになっていて、
とてもわかりやすい構成になっています。aki01@LAPTOP-8UTJLJ8V MINGW64 ~/work/src/go.googlesource.com/goroot (feature/print_gopher) $ ls -al src/ total 737 drwxr-xr-x 1 aki01 197609 0 Dec 22 22:04 . drwxr-xr-x 1 aki01 197609 0 Dec 22 22:03 .. -rw-r--r-- 1 aki01 197609 553 Dec 21 08:36 Make.dist -rw-r--r-- 1 aki01 197609 2295 Dec 21 08:36 README.vendor -rwxr-xr-x 1 aki01 197609 407 Dec 21 08:36 all.bash -rw-r--r-- 1 aki01 197609 726 Dec 21 08:36 all.bat -rwxr-xr-x 1 aki01 197609 385 Dec 21 08:36 all.rc drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 archive -rwxr-xr-x 1 aki01 197609 3790 Dec 21 08:36 bootstrap.bash drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 bufio -rwxr-xr-x 1 aki01 197609 2005 Dec 21 08:36 buildall.bash drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 builtin drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 bytes -rwxr-xr-x 1 aki01 197609 521 Dec 21 08:36 clean.bash -rw-r--r-- 1 aki01 197609 565 Dec 21 08:36 clean.bat -rwxr-xr-x 1 aki01 197609 380 Dec 21 08:36 clean.rc drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 cmd -rwxr-xr-x 1 aki01 197609 1519 Dec 21 08:36 cmp.bash drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 compress drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 container drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 context drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 crypto drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 database drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 debug drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 encoding drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 errors drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 expvar drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 flag drwxr-xr-x 1 aki01 197609 0 Dec 22 11:24 fmt drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 go -rw-r--r-- 1 aki01 197609 275 Dec 21 08:36 go.mod -rw-r--r-- 1 aki01 197609 1562 Dec 21 08:36 go.sum drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 hash drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 html drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 image drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 index drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 internal drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 io -rwxr-xr-x 1 aki01 197609 1988 Dec 21 08:36 iostest.bash drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 log -rwxr-xr-x 1 aki01 197609 6957 Dec 21 08:36 make.bash -rw-r--r-- 1 aki01 197609 4007 Dec 21 08:36 make.bat -rwxr-xr-x 1 aki01 197609 3177 Dec 21 08:36 make.rc drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 math drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 mime drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 net drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 os drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 path drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 plugin -rwxr-xr-x 1 aki01 197609 1021 Dec 21 08:36 race.bash -rw-r--r-- 1 aki01 197609 1041 Dec 21 08:36 race.bat drwxr-xr-x 1 aki01 197609 0 Dec 21 08:37 reflect drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 regexp -rwxr-xr-x 1 aki01 197609 2155 Dec 21 08:36 run.bash -rw-r--r-- 1 aki01 197609 1164 Dec 21 08:36 run.bat -rwxr-xr-x 1 aki01 197609 435 Dec 21 08:36 run.rc drwxr-xr-x 1 aki01 197609 0 Dec 21 08:37 runtime drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 sort drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 strconv drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 strings drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 sync drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 syscall drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 testdata drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 testing drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 text drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 time drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 unicode drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 unsafe drwxr-xr-x 1 aki01 197609 0 Dec 21 08:36 vendorGo で Go を build する
取得したソースコードで Go を build していきましょう。
build 方法については、オフィシャル で詳しくドキュメンテーションされています。
まず、そのオフィシャルの指示にしたがって、
all.bat
(all.bash
) を実行します。C:\Users\aki01\work\src\go.googlesource.com\goroot\src>all.bat ERROR: Cannot find C:\Users\aki01\Go1.4\bin\go.exe Set GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.おっと、エラーで fail してしまいました。
どうやら、ツールチェインを build する go コマンドのデフォルトパスが/users/myUser/Go1.4
となっているようです。Go1.4 以降の go コマンドであればよいはずなので、
普段の go コマンドの場所をエラーメッセージの指定する環境変数 (GOROOT_BOOTSTRAP
) へ設定します。C:\Users\aki01\work\src\go.googlesource.com\goroot\src>go version go version go1.13 windows/amd64 C:\Users\aki01\work\src\go.googlesource.com\goroot\src>where go C:\Go\bin\go.exe C:\Users\aki01\work\src\go.googlesource.com\goroot\src>set GOROOT_BOOTSTRAP=c:\Go改めて、
all.bat
を実行すると、ツールチェインの build から build が始まり、
go の各コマンドと、標準パッケージが無事 build されました。C:\Users\aki01\work\src\go.googlesource.com\goroot\src>all.bat Building Go cmd/dist using c:\go Building Go toolchain1 using c:\go. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for windows/amd64.Go 本体の build が終わった後は、テストが流れていきます。
テストも Goではおなじみのtesting
package で行っているように見えます。いい感じ。
テストのログ
##### Testing packages. ok archive/tar 1.349s ok archive/zip 0.860s ok bufio 0.841s ok bytes 0.590s ok compress/bzip2 0.642s ok compress/flate 1.857s ok compress/gzip 0.948s ok compress/lzw 1.574s ok compress/zlib 1.124s ok container/heap 0.774s ok container/list 1.368s ok container/ring 0.465s ok context 1.684s ok crypto 0.406s ok crypto/aes 0.446s ok crypto/cipher 0.638s ok crypto/des 0.755s ok crypto/dsa 0.388s ok crypto/ecdsa 0.790s ok crypto/ed25519 0.597s ok crypto/elliptic 1.032s ok crypto/hmac 0.831s ok crypto/internal/subtle 0.617s ok crypto/md5 0.746s ok crypto/rand 0.444s ok crypto/rc4 0.559s ok crypto/rsa 0.810s ok crypto/sha1 0.756s ok crypto/sha256 0.485s ok crypto/sha512 0.717s ok crypto/subtle 0.702s ok crypto/tls 2.917s ok crypto/x509 1.847s ok database/sql 1.023s ok database/sql/driver 0.458s ok debug/dwarf 0.665s ok debug/elf 0.878s ok debug/gosym 0.444s ok debug/macho 0.651s ok debug/pe 29.414s ok debug/plan9obj 0.707s ok encoding/ascii85 1.012s ok encoding/asn1 0.706s ok encoding/base32 0.951s ok encoding/base64 0.599s ok encoding/binary 0.497s ok encoding/csv 0.612s ok encoding/gob 0.932s ok encoding/hex 0.561s ok encoding/json 0.962s ok encoding/pem 0.805s ok encoding/xml 0.686s ok errors 0.567s ok expvar 0.537s ok flag 0.507s ok fmt 0.476s ok go/ast 0.896s ok go/build 11.796s ok go/constant 0.423s ok go/doc 1.546s ok go/format 0.728s ok go/importer 1.182s ok go/internal/gccgoimporter 0.788s ok go/internal/gcimporter 1.543s ok go/internal/srcimporter 4.109s ok go/parser 0.593s ok go/printer 1.159s ok go/scanner 0.467s ok go/token 0.528s ok go/types 3.538s ok hash 0.403s ok hash/adler32 0.494s ok hash/crc32 0.898s ok hash/crc64 0.542s ok hash/fnv 1.127s ok hash/maphash 1.282s ok html 0.891s ok html/template 0.552s ok image 0.879s ok image/color 0.642s ok image/draw 0.454s ok image/gif 1.577s ok image/jpeg 1.200s ok image/png 0.781s ok index/suffixarray 1.100s ok internal/cpu 0.499s ok internal/fmtsort 0.391s ok internal/poll 0.569s ok internal/reflectlite 0.657s ok internal/singleflight 0.461s ok internal/syscall/windows 0.664s ok internal/syscall/windows/registry 0.623s ok internal/trace 0.732s ok internal/xcoff 0.511s ok io 0.389s ok io/ioutil 0.423s ok log 0.867s ok math 0.721s ok math/big 1.865s ok math/bits 0.465s ok math/cmplx 0.429s ok math/rand 0.688s ok mime 0.450s ok mime/multipart 1.220s ok mime/quotedprintable 0.684s ok net 16.751s ok net/http 23.643s ok net/http/cgi 1.529s ok net/http/cookiejar 0.909s ok net/http/fcgi 0.458s ok net/http/httptest 2.944s ok net/http/httptrace 0.450s ok net/http/httputil 1.120s ok net/http/internal 0.475s ok net/http/pprof 2.738s ok net/internal/socktest 0.449s ok net/mail 0.586s ok net/rpc 0.698s ok net/rpc/jsonrpc 0.679s ok net/smtp 1.110s ok net/textproto 0.916s ok net/url 0.654s ok os 10.443s ok os/exec 52.157s ok os/signal 5.964s ok os/user 0.757s ok path 0.522s ok path/filepath 2.734s ok plugin 0.545s ok reflect 1.883s ok regexp 0.758s ok regexp/syntax 0.876s ok runtime 158.522s ok runtime/debug 0.444s ok runtime/internal/atomic 0.749s ok runtime/internal/math 0.756s ok runtime/internal/sys 0.438s ok runtime/pprof 19.055s ok runtime/pprof/internal/profile 0.373s ok runtime/trace 4.164s ok sort 0.548s ok strconv 1.288s ok strings 0.767s ok sync 2.595s ok sync/atomic 0.758s ok syscall 0.475s ok testing 0.749s ok testing/iotest 0.615s ok testing/quick 0.642s ok text/scanner 0.846s ok text/tabwriter 0.492s ok text/template 0.652s ok text/template/parse 0.608s ok time 3.131s ok unicode 0.485s ok unicode/utf16 0.436s ok unicode/utf8 0.411s ok cmd/addr2line 16.158s ok cmd/api 0.882s ok cmd/asm/internal/asm 2.619s ok cmd/asm/internal/lex 0.502s ok cmd/compile 0.757s ok cmd/compile/internal/gc 42.494s ok cmd/compile/internal/logopt 3.110s ok cmd/compile/internal/ssa 1.713s ok cmd/compile/internal/syntax 0.747s ok cmd/compile/internal/test 0.515s [no tests to run] ok cmd/compile/internal/types 0.633s ok cmd/cover 26.559s ok cmd/doc 1.812s ok cmd/fix 15.478s ok cmd/go 248.974s ok cmd/go/internal/auth 0.536s ok cmd/go/internal/cache 3.530s ok cmd/go/internal/generate 0.750s ok cmd/go/internal/get 0.979s ok cmd/go/internal/imports 1.040s ok cmd/go/internal/load 0.714s ok cmd/go/internal/lockedfile 5.678s ok cmd/go/internal/lockedfile/internal/filelock 0.771s ok cmd/go/internal/modconv 1.050s ok cmd/go/internal/modfetch 1.070s ok cmd/go/internal/modfetch/codehost 0.631s ok cmd/go/internal/modfetch/zip_sum_test 0.678s ok cmd/go/internal/modload 0.771s ok cmd/go/internal/mvs 0.549s ok cmd/go/internal/par 1.073s ok cmd/go/internal/renameio 7.345s ok cmd/go/internal/search 0.886s ok cmd/go/internal/txtar 0.891s ok cmd/go/internal/web 0.721s ok cmd/go/internal/work 1.080s ok cmd/gofmt 1.621s ok cmd/internal/buildid 1.417s ok cmd/internal/dwarf 0.943s ok cmd/internal/edit 0.549s ok cmd/internal/goobj 5.877s ok cmd/internal/obj 0.468s ok cmd/internal/obj/arm64 0.908s ok cmd/internal/obj/x86 9.071s ok cmd/internal/objabi 0.780s ok cmd/internal/src 0.533s ok cmd/internal/test2json 1.604s ok cmd/link 45.006s ok cmd/link/internal/ld 22.251s ok cmd/link/internal/sym 0.473s ok cmd/nm 26.542s ok cmd/objdump 22.501s ok cmd/pack 18.809s ok cmd/trace 0.768s ok cmd/vet 31.327s ##### os/user with tag osusergo ok os/user 0.612s ##### GOMAXPROCS=2 runtime -cpu=1,2,4 -quick ok runtime 35.922s ##### Testing without libgcc. ok crypto/x509 1.085s ok net 0.618s ok os/user 0.399s ##### sync -cpu=10 ok sync 1.689s ##### Testing race detector ok runtime/race 25.388s ok flag 1.331s ok net 1.486s ok os 1.890s ok os/exec 3.251s ok encoding/gob 1.125s ok flag 1.445s ok os/exec 3.224s ##### ../misc/cgo/stdio PASS ##### ../misc/cgo/life PASS ##### ../misc/cgo/test PASS scatter = 00000000005681C0 sqrt is: 0 hello from C ok misc/cgo/test 25.251s PASS scatter = 0000000000401ED0 sqrt is: 0 hello from C ok misc/cgo/test 25.257s PASS scatter = 00000000005681C0 sqrt is: 0 hello from C ok misc/cgo/test 25.106s PASS scatter = 00000000005681C0 sqrt is: 0 hello from C ok misc/cgo/test 25.464s ##### ../misc/cgo/testgodefs PASS ##### ../misc/cgo/testso ok misc/cgo/testso 6.517s ##### ../misc/cgo/testsovar ok misc/cgo/testsovar 7.198s ##### ../misc/cgo/testcarchive SKIP - short mode and $GO_BUILDER_NAME not set ##### ../misc/cgo/testcshared SKIP - short mode and $GO_BUILDER_NAME not set ##### ../test/bench/go1 ##### ../test ##### API check ALL TESTS PASSED--- Installed Go for windows/amd64 in C:\Users\aki01\work\src\go.googlesource.com\goroot Installed commands in C:\Users\aki01\work\src\go.googlesource.com\goroot\bin *** You need to add C:\Users\aki01\work\src\go.googlesource.com\goroot\bin to your PATH.build が完了すると、リポジトリのルートディレクトリの下の
bin
ディレクトリにバイナリが作成されます。C:\Users\aki01\work\src\go.googlesource.com\goroot\src>..\bin\go.exe version go version devel +a5bfd9da1d Tue Dec 17 14:59:30 2019 +0000 windows/amd64 C:\Users\aki01\work\src\go.googlesource.com\goroot\src>git log commit a5bfd9da1d1b24f326399b6b75558ded14514f23 (HEAD, tag: go1.14beta1) commit a5bfd9da1d1b24f326399b6b75558ded14514f23 (HEAD, tag: go1.14beta1) Author: Bryan C. Mills <bcmills@google.com> Date: Tue Dec 17 09:00:57 2019 -0500 go/build: rename WorkingDir to Dir Fixes #36168 Change-Id: If2b7368671e83657a3a74dd030e66e7c68bf2361 Reviewed-on: https://go-review.googlesource.com/c/go/+/211657 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>Go で Go を開発する
簡単に Go で Go が build できるようになりました。
というわけで、早速 Go のソースコードをオレオレにしてみましょう。
おなじみのfmt
パッケージのソースに、 Gopher の AA を標準出力へ出力するPrintGopher()
関数を追加してみました。
(AA は tenntenn さんのツイート がソースです)src/fmt/print.go// Print formats using the default formats for its operands and writes to standard output. // Spaces are added between operands when neither is a string. // It returns the number of bytes written and any write error encountered. func Print(a ...interface{}) (n int, err error) { return Fprint(os.Stdout, a...) } // PrintGopher writes gopher to standard output. func PrintGopher() (n int, err error) { return Fprint(os.Stdout, "ʕ◔ϖ◔ʔ\n") }もう一度 Go 本体を build して、追加した関数をコールしてみましょう。
sandbox/main.gopackage main import "fmt" func main() { fmt.PrintGopher() }C:\Users\aki01\work\src\go.googlesource.com\goroot\sandbox>..\bin\go.exe build C:\Users\aki01\work\src\go.googlesource.com\goroot\sandbox>sandbox.exe ʕ◔ϖ◔ʔやったー!! 動きました。
これで Go が Go で開発できるようになりました。
さあーコントリビュートするぞー。RoundUp
今回、Go を Go で build して、ソースコードを変更してみましたが、
普段 Go で書いているアプリケーションを書くような感覚でサクッとできてしまいました。いざ Go 本体へ commit となると、普段からプロジェクトを watch したり、
適切な英文のプロポーザルを書いたりとまだ頑張りどころがありますが、
いい commit アイテムとの出会いがあれば、Go にコントリビュートできそうだと思うことができました。ぜひ、Go を普段使っている皆さんも、Go の処理系への理解を深めて、GO への commit を目指してみてはいかがでしょうか。
Reference
- 投稿日:2019-12-22T22:13:22+09:00
Go gRPC-Gatewayでカスタムエラーを返す方法
VISITS Technologies Advent Calendar 2019 23日目は@istshが担当します。
この記事はサンプルコードを使って解説してきます。エラーをカスタマイズしたい
おそらくgRPCのエラー実装において、
status.New(codes.Unauthenticated, "not authenticated").Err()のように書いたことがあると思います。
しかし、これではnot authenticated
というエラーメッセージしか扱えません。私がいるプロジェクトでは、下記の3点を返す必要がありました。
- エラーコード(StatusCodeではない)
- ロケール
- エラーメッセージ
この記事では、これらをgRPCで返し、さらにgRPC-Gateway(HTTP)で返すところまでの実装を紹介します。
LocalizedMessage
go get -u google.golang.org/genproto/googleapis/rpc/errdetails// Provides a localized error message that is safe to return to the user // which can be attached to an RPC error. type LocalizedMessage struct { // The locale used following the specification defined at // http://www.rfc-editor.org/rfc/bcp/bcp47.txt. // Examples are: "en-US", "fr-CH", "es-MX" Locale string `protobuf:"bytes,1,opt,name=locale,proto3" json:"locale,omitempty"` // The localized error message in the above locale. Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` }error_details.pb.go#L670
このLocalizedMessage
を使うことで、ロケール
とエラーメッセージ
を扱えそうです。WithDetails
status.New(codes.Unauthenticated, "not authenticated").Err()
で上記のLocalizedMessage
を扱うには、下記のようにWithDetails
というメソッドを使います。status.New(status.New(codes.Unauthenticated, "not authenticated").WithDetails( &errdetails.LocalizedMessage{ Locale: LocaleJaJp, Message: "ユーザーの認証ができませんでした。", }, ))ちなみに
WithDetails
は、可変長引数を取ります。func (s *Status) WithDetails(details ...proto.Message) (*Status, error) { // 省略... }よって、下記のように
LocalizedMessage
を複数渡すことが可能です、status.New(status.New(codes.Unauthenticated, "not authenticated").WithDetails( &errdetails.LocalizedMessage{ Locale: LocaleJaJp, Message: "ユーザーの認証ができませんでした。", }, &errdetails.LocalizedMessage{ Locale: LocaleEnUs, Message: "Unauthenticated", }, ))これで、あるエラーにおいて日本語や英語のエラーメッセージを扱えることがわかりました。
エラーコード
エラーコードを扱うには、自前で用意する必要があるようです。
app/pb/v1/error.pb.gotype ErrorCode struct { ErrorCode string `protobuf:"bytes,1,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *ErrorCode) Reset() { *m = ErrorCode{} } func (m *ErrorCode) String() string { return proto.CompactTextString(m) } func (*ErrorCode) ProtoMessage() {}これは
error.proto
から生成したコードです。
WithDetails
は、下記のproto.Message
のインターフェースを実装した構造体であれば渡せるので、自動生成しなくても問題ありません。// Message is implemented by generated protocol buffer messages. type Message interface { Reset() String() string ProtoMessage() }カスタムエラーの定義
ここまでのコードをまとめると、下記のようになります。
app/status/status.gostatus.New(codes.Unauthenticated, "not authenticated").WithDetails( &pbv1.ErrorCode{ ErrorCode: "USER_UNAUTHENTICATED", }, &errdetails.LocalizedMessage{ Locale: LocaleJaJp, Message: "ユーザーの認証ができませんでした。", }, &errdetails.LocalizedMessage{ Locale: LocaleEnUs, Message: "Unauthenticated", }, )カスタムエラーをgRPC-Gateway(HTTP)で返す
カスタムHTTPエラーの実装
app/cmd/client/http_error.gofunc HTTPError(_ context.Context, _ *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, _ *http.Request, err error) { s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } // 省略... ed := &pbv1.Error_ErrorDetail{} if len(s.Details()) > 0 { for _, detail := range s.Details() { switch v := detail.(type) { case *pbv1.ErrorCode: ed.ErrorCode = v.GetErrorCode() case *errdetails.LocalizedMessage: if ed.GetMessage() != "" { // Already set error message. continue } if v.GetLocale() == appstatus.LocaleJaJp { ed.Locale = v.GetLocale() ed.Message = v.GetMessage() } } } } else { ed.Message = s.Message() } e := pbv1.Error{ Error: ed, } // 省略... }
status.FromError(err)
からgrpc.Status
を取得でき、Details
メソッドでWithDetails
に渡したproto.Message
を取得できます。gRPC-Gateway
gRPC-GatewayでHTTPエラーを扱う関数を変更する必要があります。
app/cmd/client/main.goruntime.HTTPError = HTTPErrorまとめ
【前編】【後編】に続き、gRPC-Gatewayに関する実装で、カスタムエラーを扱う方法を紹介しました。
WithDetails
メソッドを使えば、紹介したフィールド以外でも返せるので、より詳細なエラーをクライアントに伝えることが可能になります。また、カスタムエラーの記事はいくつかありますが、metadataでを使う方法が多く、おそらく古い方法なのでお勧めしません。
もし、他にいい実装方法を知っている方がいたら、コメント等でご紹介いただければ幸いです。
- 投稿日:2019-12-22T21:30:45+09:00
docker pullをちょっとだけ楽にするCLIつくりました
つくったもの
dockerのimageからtagを検索&選択してpullできるCLI
Goでつくった人生初のCLIツール
— kohbis (@kohbis) December 22, 2019
(docker pull をちょっとだけ楽にする) pic.twitter.com/F7qA86Inmp※動画の圧縮になれておらず、、画質が悪いです。。
バージョン等
- Go 1.13.5
- spf13/cobra ... CLIアプリケーションのライブラリ。
- manifoldco/promptui ... 対話型プロンプトのライブラリ。
- macOS Catalina 10.15.1
- Docker version 19.03.5
Goの経験は、以前ポストしたLチカセブンくらいでほぼ触ったことがありません。
「shellなら楽じゃん」と言われそうですが、今回は Dockerのアドベントカレンダー 。
必然的にDockerの開発言語であるGo言語を使うしかありません!CLIのライブラリは、urfave/cliも情報がたくさんありました。
しかし、今回は Dockerのアドベントカレンダー 。
必然的にDockerに採用されているcobraを使うしかありません!ソース
https://github.com/kohbis/dimg
※ 201919/12/22時点では、公式イメージ(library)だけ対応しています。
ポイント
タグ一覧取得
curlコマンドだと、下記でタグ一覧が取得できます。
(後述の公式ドキュメント参照)curl -s https://registry.hub.docker.com/v2/repositories/library/alpine/tags/ | jq -r '.results|.[]|.name'
返ってくるJSONを、Goの構造体で表すとこうなります。
JSON->Structには、JSON-to-Goというサイトが、とても便利でした。type Tags struct { Count int `json:"count"` Next string `json:"next"` Previous interface{} `json:"previous"` Results []struct { Name string `json:"name"` FullSize int `json:"full_size"` Images []struct { Size int `json:"size"` Digest string `json:"digest"` Architecture string `json:"architecture"` Os string `json:"os"` OsVersion interface{} `json:"os_version"` OsFeatures string `json:"os_features"` Variant interface{} `json:"variant"` Features string `json:"features"` } `json:"images"` ID int `json:"id"` Repository int `json:"repository"` Creator int `json:"creator"` LastUpdater int `json:"last_updater"` LastUpdaterUsername string `json:"last_updater_username"` ImageID interface{} `json:"image_id"` V2 bool `json:"v2"` LastUpdated time.Time `json:"last_updated"` } `json:"results"` }このままだと1ページあたり10件しか取得できないため
Next
に次ページのURLがnullにならない限りループして、、、という面倒くさいことになります。
そのため、最初からクエリパラメータ?page_size=10000
をつけて取得しています。
(そんなにタグ数があるイメージってあるのかしら)タグ検索
promptuiは自前のsearcherを実装することができます。(感動)
これにより、マイナーバージョンが多い言語の実行環境イメージも見つけやすくなっています。$ go run main.go Image Name: ruby Searching "ruby" tags... "ruby" has 615 tags. Search: 2.6.█ ? Select Tag: ▸ 2.6.5-stretch 2.6.5-slim-stretch 2.6.5-slim-buster 2.6.5-slim ↓ 2.6.5-bustercobraも、promptuiも色々できることがありそうなので、今後拡張していきたいです。
まとめ
Docker アドベントカレンダー担当日の1日前に思いたったのですが、ライブラリが非常に強力で1日かからずにものをつくることができました!
今回作ったものは足りないところがたくさんありますが、実際つくってみると個人的には欲しい機能がどんどん思いついていきたので、今後継続的に開発していきたいと思います。
実は今年最初やりたいことのひとつに「CLIツールをつくる」がありまして、ぎりぎり達成することができたのでよかったです!!
(この記事を書いている最終に思い出しました笑)参考
Examples using the Docker Engine SDKs and Docker API
How do I authenticate with the V2 API?
- 投稿日:2019-12-22T21:28:37+09:00
Go言語入門 ~概要と基本構文~
はじめに
TechCommit Advent Calendar 2019
22日目担当のSatoshi
です。筆者について
エンジニア歴2年
22歳
業務ではJavaScriptを使ってなにやらやっています。
Goを使ってバックエンド開発がやりたいので、Goを学習中本記事の内容
本記事では、Go言語の入門編として、基礎的な内容を書いていきます。
筆者は、業務ではJavaScript(動的言語)を触っているので、それと比較してGo言語(静的言語)の良いところにもちょくちょく触れていけたらなと思います。本記事のゴール
- Go言語について、概要レベルで理解できる
- 基礎的な部分をさらっと学習、復習できる
Go言語について
Go言語は、2009年にGoogleによって開発された言語です。
特徴としては、一部ですが以下のようなものがあります。
- シンプルな言語
- 静的型付け
- クロスプラットフォーム対応
- コンパイル・実行速度が早い
- 並列処理が得意
- フォーマットの統一性 etc...
Go言語では、標準でフォーマットを整えてくれる機能(go fmt)や、使ってない変数があるとコンパイルできないように設計されているので、属人性は出にくいのかなと思いました。
本記事では、
クロスプラットフォーム対応
やコンパイル・実行速度が早い
、並列処理が得意
などには触れません。導入
サクッと簡単なプログラムだけ組んで遊んでみたいということであれば、Go公式ページの
Try Go
もしくはOpen in Playground
リンクを踏むと、環境構築無しにWeb上でプログラムが実行可能です。Go公式 - Downloadsにアクセスし、ご自身の環境に合ったファイルをダウンロードして下さい。
各環境の細かいインストール方法を記述すると大変なので、各自で調べてください。公式の導入方法
以下ページ(英語)に行くとシステム要件やGoツールのインストール方法、アンインストール方法などが載っています。
Getting Started
チュートリアル
Goには、公式が提供しているチュートリアル、A Tour of Goというものがあります。
上記リンクを踏んで、ページを1つ進めると日本語や英語、中国語など様々な言語が選択できます。
ただ、説明がとても短い章があるので、別のサイトで調べつつ取り組むことをオススメします。世界に挨拶
以下のソース内で、
import "fmt"
と記述していますが、fmt
とはGoが標準で提供しているパッケージです。
Goが提供しているパッケージの一覧は、Go公式 - Packagesで確認できます。Goでは、以下のように挨拶します。
HelloWorld.go//ソースファイルがどこに所属しているか必ず宣言 package main //入出力フォーマットを実装した標準パッケージ"フォーマット"をインポート import "fmt" //関数はfuncキーワードで定義 func main(){ fmt.Println("Hello, World!") // Hello, World! }Goでは、決まりとして
プログラムは何らかのpackageに属している必要があり、そのうちの 1 つは必ず main でなければならない。
というものがあります。
また、mainパッケージ
のmain関数
があれば、それが最初(初期化処理後)に実行されます。
別の関数を定義して呼び出したいときは、main関数の中で
呼び出します。HelloNeighbor.gopackage main import "fmt" //stringの部分は、戻り値の型を示す func Say() string{ return "Neighbor!" } func main(){ fmt.Println("Hello,", Say()) //Hello, Neighbor! }特別な関数
init
世界に挨拶
では、Goでは、mainパッケージのmain関数が最初(初期化処理後)に実行されます
と記述しました。
しかし、mainパッケージ
にinit
という関数を定義することで、main関数よりも先に関数を実行することができます。
このinit関数
を利用することで、変数の初期化
や外部リソースの読み込み
などを一番最初に実行できます。Initialize.gopackage main import "fmt" func init(){ fmt.Println("Initializing...") } func main(){ fmt.Println("Hello, World!") } //Initializing... //Hello, World!Import
Goでは、様々なパッケージをインポートしつつプログラムを書いていきます。
以下は、現在の時刻を出力するプログラムです。PrintTime.gopackage main //import()と記述することで、複数のパッケージをインポートできる import( "fmt" "time" ) func main(){ fmt.Println("Now: ", time.Now()) // Now: 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 }変数宣言
Goでは、変数の型を
変数名の後ろに定義
する決まりとなっています。
また、様々な型が提供されています。Go - Packages - Variables
var
での型宣言は、関数外でも宣言できますが、ショートデクラレーション(:=)
は関数内でしか宣言できません。
JavaScriptだと型宣言はしないので、ソースの型宣言を見るだけで変数の型が分かるのは良いですね!
また、予期せぬ型が入ってくることも少ないので、安全です。var_type.gopackage main import "fmt" func main(){ //varで宣言 var i int = 1 //var()で複数宣言 var ( f64 float64 = 1.5 str string = "foo" ) //カンマ区切りで複数宣言、かつショートデクラレーションで型推論 t, f := true, false fmt.Println(i, f64, str, t, f) // 1 1.5 foo true false //ちなみに、初期化だけして値を代入しなかった場合、それぞれの型の初期値が出力される }関数
Goでは、関数の戻り値の型を指定します。
基本構文は
func <関数名>([引数]) [戻り値の型] {
[関数の本体]
}
です。
JavaScriptと違い、関数定義時に戻り値の型を指定できる
ので、実行せずともある程度どの型が戻り値かが分かります。func.gopackage main import "fmt" //int型の戻り値 func add(x int, y int) int{ return x + y } //複数の戻り値の型を指定 func sub(a, b int) (int, string){ return a - b, "subtraction!" } //戻り値"result"は予約語ではない。この関数内で戻り値としてresultを使用するということ func calc(price, item int) (result int){ //resultをショートデクラレーション(:=)で再定義はできない result = price * item return result } func main(){ radd := add(10, 20) fmt.Println(radd) // 30 rsub, rsubStr := sub(10 , 5) fmt.Println(rsub) // 5 fmt.Println(rsubStr)// subtraction! //以下のように書くことで、即時に関数を実行できる func(a int){ fmt.Println(a) //9999 }(9999) }型変換
以下に、いくつかの型変換を行うプログラムを記述します。
cast.gopackage main //string->intへキャストする際に、strconvを使う import ( "fmt" "strconv" ) func main(){ //int -> float var x int = 1 f64x := float64(x) //%Tは、型を出力、%vは値を出力、%fは指数なしの小数を出力する fmt.Printf("%T %v %f\n", i, i, i) //int 1 %!f(int=1) fmt.Printf("%T %v %f\n", f64x, f64x, f64x)// float64 1 1.000000 //float64 -> int var y float64 = 1.5 inty := int(y) fmt.Printf("%T %v %f\n", y, y, y) //float64 1.5 1.500000 fmt.Printf("%T %v %f\n", inty, inty, inty)// int 1 %!f(int=1) } //string -> int var str string = "72" //strconvの "Atoi(ASCII to integer)" は "int" と "error" の2つを返すため、 "_" でerrorを捨てる。 //今回はエラーハンドリングを実装しませんが、ハンドリングするときは"_"ではなく"err"などの変数を定義し、ハンドリングしてください。 i, _ := strconv.Atoi(str) //stringで72を文字列型にキャストすることで、ASCIIコードに沿って文字が出力される fmt.Printf("%T %v %v\n", i, i, string(i)) // int 72 H配列とスライス
配列は
固定長
、スライスは可変長の配列のようなもの
です。配列
array.gopackage main import "fmt" func main(){ //配列 //int型の配列で、要素数2 var x[2] int x[0] = 1 x[1] = 2 fmt.Println(x) // [1 2] //初期化時に値を入れたい場合、ブラケットを使って代入する var y[3]int = [3]int{1, 2, 3} fmt.Println(y) // [1 2 3] fmt.Println(y[0:2]) // [1 2] fmt.Println(y[1:2]) // [2] fmt.Println(y[:2]) // [1 2] fmt.Println(y[1:]) // [2 3] }スライス
slice.gopackage main import "fmt" func main(){ //型宣言時に、要素数を指定しない var z []int = []int{1, 2, 3} //z := []int {1, 2, 3} でもOK fmt.Println(z)// [1 2 3] //配列と違い、後から値を追加(append)できる z = append(z, 4) fmt.Println(z)// [1 2 3 4] fmt.Println(z[0:2]) // [1 2] fmt.Println(z[1:2]) // [2] fmt.Println(z[:2]) // [1 2] fmt.Println(z[1:]) // [2 3 4] }makeとcap
makeの詳細は、実践Go言語 - makeによる割り当てを参照。
make_cap.gopackage main import "fmt" func main(){ //integerのスライスで、長さが3つ、キャパシティは5 a := make([]int, 3, 5) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) // len=3 cap=5 val=[0 0 0] //長さが3なので、valは0が3つ並んでおり、キャパシティは5なので、メモリ上にはあと2つ確保してある a = append(a, 0, 1) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) // len=5 cap=5 val=[0 0 0 0 1] //appendで "0" と "1" を追加したので、長さが5になった。 //キャパシティ5以上追加してみる a = append(a, 6, 7, 8) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) //len=8 cap=12 val=[0 0 0 0 1 6 7 8] }上記プログラムの最終行で実行しているfmt.Printfの出力結果に注目してください。
キャパシティも長さも5
のスライスに、appendで3つ
追加しただけのはずが、追加後のスライスのキャパシティが12
になっています。
こんな挙動をする原因を、以下のプログラムで暴きます。detective_p.gopackage main import "fmt" func main(){ //引数を省略することで、以下だと長さもキャパシティも3ということになる a := make([]int, 3) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) fmt.Printf("*p= %p\n", a) // *p= 0x40e020 a = append(a, 0) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) // len=5 cap=5 val=[0 0 0 0 1] fmt.Printf("*p= %p\n", a) // *p= 0x456020 //キャパシティ5以上追加してみる a = append(a, 6, 7, 8) fmt.Printf("len=%d cap=%d val=%v\n", len(a), cap(a), a) fmt.Printf("*p= %p\n", a) // *p= 0x456020 }上記の通り、キャパシティ以上に要素を追加しようとすると、
append関数内部
でメモリ領域の再確保が行われ、新しい領域のアドレスが参照
されるようになります。
元の値は、新しい領域へコピー
されることになるので、メモリ領域の再確保にも、コピー処理にも計算コスト
がかかります。map
Goでは
ハッシュ(連想配列)のことをmap
と呼びます。map.gopackage main import "fmt" func main(){ m := map[string]int{"foo": 1, "bar": 2} fmt.Println(m) //map[bar:2 foo:1] fmt.Println(m["foo"]) // 1 m["bar"] = 999 fmt.Println(m) //map[bar:999 foo:1] //存在しないオプション名にアクセスすると、0が返ってくる fmt.Println(m["nothing"]) //0 //mapに存在しているかチェックする方法は以下。戻り値は "値"と"bool"の2値 val, ok := m["foo"] fmt.Println(val, ok)//1 true }可変長引数
関数やメソッドやマクロの
引数
が固定ではなく任意の個数
となっている引数のことです。var_length_arguments.gopackage main import "fmt" func foo(params ...int){ fmt.Println("len=",len(params), "param=",params) //for rangeを使うことで、paramsをループできる for _, param := range params{ fmt.Println(param) } } func main(){ foo(10, 20) //len= 2 param= [10 20] foo(10, 20, 30, 40, 50) //len= 5 param= [10 20 30 40 50] }最後に
唐突ですが、以上で入門編を終わりにしたいと思います。(続編を書くかは分かりませんが...)
本記事では細かい部分を結構削っています。
理由としては、話が脱線しそうだということと、さらっと学習、復習できる
ということを目標にしていたたためです。
ただ、入門として知っておくべきことは他にもあります
。
ここまで学習できれば、あとはトライ&エラー
で学習を進めていき、細かい部分のキャッチアップは可能かなと思っているので、一緒に頑張って行きましょう!Qiita初投稿の記事で時間は結構かかりましたが、書くのは面白いですね。
これは絶対に入門として必要だ
というものがあれば、コメントで教えて下さい!
筆者は最近Goを触り始めたので、何か間違いなどあった場合には、遠慮なくご指摘いただけると大変嬉しいです...!あと、Gopherくん可愛いですよね。
可愛すぎてLINEスタンプ買っちゃいました。
以上です。
- 投稿日:2019-12-22T21:22:03+09:00
Prometheus の exporter 自作
はじめに
品川 Advent Calendar 2019 の22日目です。
Prometheus はご存知の通り、監視対象にアクセスしてデータを収集し、モニタリングや通知が可能な Pull 型の監視ソフトウェアの1つです。
監視対象は、データを取得して Prometheus にレスポンスする exporter や、バッチやアプリケーション等が Push したデータを蓄積しておき、Prometheus にレスポンスする PushGateway を用意しておく必要があります。
exporter は、公式非公式を含め 多数公開 されており、また exporter を自作するための クライアントライブラリも公開 されています。
exporter を自作する機会は、今のところ全く無いのですが、いつか来たるべき日に備えて、今更ですがクライアントライブラリを利用して自作してみたいと思います。取得するメトリクス
とりあえず Linux のメモリ使用率を取得する exporter を作ります。
Linux の場合、メモリ使用率は/proc/meminfo
を見ればわかるので、ここを読み取るような作りにします。自作してみる
hirano00o/sample-exporter: Sample Exporter for Prometheus
作成にあたっては、クライアントライブラリの example や node_exporter を参考にしました。
node_exporter
では、取得する情報を引数で指定してフィルタしたりしてましたが、今回取得情報に関する引数は取らないので、こんな感じになりました。sample-exporter/main.gofunc main() { flag.Parse() c, err := collector.NewSampleCollector() if err != nil { log.Fatal(err) } // NewしたCollectorを登録する。ここに登録したもののメトリクスが取得できる prometheus.MustRegister(c) http.Handle("/metrics", promhttp.Handler()) log.Println("Listening on ", *addr) if err := http.ListenAndServe(*addr, nil); err != nil { log.Fatal(err) } }
collect.go
は、メモリ以外にも情報取得したくなったときに、簡単に追加できるようになるベースです。sample-exporter/collector/collect.govar ( ... factories = make(map[string]func() (Collector, error)) collectorState = make(map[string]int) ) // memory.goでinit()で呼ぶ func registCollector(collector string, f func() (Collector, error)) { factories[collector] = f collectorState[collector] = 0 } type SampleCollector struct { Collectors map[string]Collector // keyがstring型, valueはCollectorと言う名のinterface型 } // memory.goでも利用するし、メモリ以外にもCPU等取得情報を追加したいときに備えて、interfaceで定義しておく type Collector interface { Update(ch chan<- prometheus.Metric) error } func NewSampleCollector() (*SampleCollector, error) { collectors := make(map[string]Collector) for k, _ := range collectorState { f, err := factories[k]() if err != nil { return nil, err } collectors[k] = f } // 今回はメモリだけだが、CPU等の他情報の取得も簡単に追加できる return &SampleCollector{Collectors: collectors}, nil } // Describe と Collect は、Collector interface に実装されている // https://godoc.org/github.com/prometheus/client_golang/prometheus#Collector func (sc SampleCollector) Describe(ch chan<- *prometheus.Desc) { ch <- scrapeDurationDesc ch <- scrapeSuccessDesc } // goroutine で複数の(今回はメモリだけだが)情報を並列に取得(execute) func (sc SampleCollector) Collect(ch chan<- prometheus.Metric) { wg := sync.WaitGroup{} wg.Add(len(sc.Collectors)) for name, c := range sc.Collectors { go func(name string, c Collector) { execute(name, c, ch) wg.Done() }(name, c) } wg.Wait() } func execute(name string, c Collector, ch chan<- prometheus.Metric) { begin := time.Now() err := c.Update(ch) duration := time.Since(begin) var success float64 if err != nil { log.Printf("ERROR: %s collector failed after %fs: %s", name, duration.Seconds(), err.Error()) success = 0 } success = 1 // 集計したい情報は chan に Metric を渡す ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name) ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, name) }メモリ情報の取得は、
node_exporter
では、/proc/meminfo
をパースしていましたが、今回は下記ライブラリを利用しました。
shirou/gopsutil: psutil for golangsample-exporter/collector/memory.goconst ( subsystem = "memory" ) type memoryCollector struct{} // MustRegister するために、init() で collect.go の factories に追加する func init() { registCollector(subsystem, NewMemoryCollector) } func NewMemoryCollector() (Collector, error) { return &memoryCollector{}, nil } func (c *memoryCollector) Update(ch chan<- prometheus.Metric) error { var metricType prometheus.ValueType // メモリ情報の取得 v, err := mem.VirtualMemory() if err != nil { return fmt.Errorf("could not get memory info: %s", err) } ... for i := 0; i < t.NumField(); i++ { ... if strings.Contains(t.Field(i).Name, "Total") == true { // Total (例えばMemTotal)が入っていたら、Counter // メトリクスの種類は https://prometheus.io/docs/concepts/metric_types/ metricType = prometheus.CounterValue } else { metricType = prometheus.GaugeValue } ... // 集計したい情報なので chan に Metric を渡す ch <- prometheus.MustNewConstMetric( prometheus.NewDesc( // メトリクス名を BuildFQName() で作成し、指定 prometheus.BuildFQName(namespace, subsystem, t.Field(i).Name), fmt.Sprintf("Memory information filed %s", t.Field(i).Name), nil, nil, ), // メトリクスタイプと値を指定、値はfloat64 metricType, f64, ) } return nil }結果
長いので一部だけ出力しましたが、ちゃんと取得できてそうですね・
$ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent" # HELP sample_memory_UsedPercent Memory information filed UsedPercent # TYPE sample_memory_UsedPercent counter sample_memory_UsedPercent 1.9555757729632135 $ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent" # HELP sample_memory_UsedPercent Memory information filed UsedPercent # TYPE sample_memory_UsedPercent counter sample_memory_UsedPercent 1.9468305687203793おわりに
結果的に、
node_exporter
の劣化版みたいな感じになってしまいましたが、意外と簡単に exporter を作ることができました。
例えば会社で、全プロジェクトで汎用的に簡単に使える exporter を作って配布して、とりあえず入れておけば OK みたいな形にするとかですかね。
grafana で長期安定化試験のときに見られると便利ですしね。
- 投稿日:2019-12-22T20:50:41+09:00
Code-Server x Dart with fmi x Go x Clang
Dart Advent Calendar 2019 の 記事です。
Dart の Native Interface に ついての記事です。
Go や C言語の機能をDartで利用する方法について解説します。また、すぐに Native Interface を試せるように、 Docker Image を 用意しました。Native Interface とは : C言語 の ライブラリーを使用することができる機能です
https://dart.dev/guides/libraries/c-interop
C言語 の ライブラリーを使用することができる機能です。ただし、C言語と言ってもC言語でのライブラリーに閉じたものではないです。
Go言語 や Rust言語などのシステム言語は、Shared Libraryを作成することができます。
この Shared Library 経由すれば、Dartで GoやRustなどの機能も利用できます。
とても便利 : OSの機能をフルに使える
Dartは、まだまだライブラリーが不足している状態です。ですので、C言語で開発されたライブラリーや、Goで開発されたライブラリーを利用する必要があります。
Native Interface によち、 Dart がより便利になります。開発環境 : Docker 環境を用意しました。
Code-Server という VSCode が オンライン上で動作する エディターを含めました。
このため、 Dockerを起動すれば、即、開発ができます。https://github.com/kyorohiro/my-code-server/tree/master/w/dart_and_go_and_c/
https://github.com/kyorohiro/advent-2019-code-server/tree/master/extra/dart_fmi_and_go_and_c
FROM ubuntu:20.04 WORKDIR /works # install dart RUN apt-get update RUN apt-get install -y wget gnupg1 RUN apt-get install apt-transport-https RUN sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' RUN sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list' RUN apt-get update RUN apt-get -y install dart # install go RUN apt-get install software-properties-common -y #RUN add-apt-repository ppa:longsleep/golang-backports RUN apt-get update RUN apt-get install golang-go -y RUN apt-get install git -y RUN go get github.com/ramya-rao-a/go-outline RUN go get github.com/mdempsky/gocode RUN go get github.com/uudashr/gopkgs/cmd/gopkgs RUN go get github.com/sqs/goreturns RUN go get github.com/rogpeppe/godef # install c RUN apt-get install musl-dev -y # code-server RUN wget https://github.com/cdr/code-server/releases/download/2.1692-vsc1.39.2/code-server2.1692-vsc1.39.2-linux-x86_64.tar.gz RUN tar -xzf code-server2.1692-vsc1.39.2-linux-x86_64.tar.gz -C ./ --strip-components 1 RUN /works/code-server --install-extension Dart-Code.dart-code RUN /works/code-server --install-extension ms-vscode.go RUN /works/code-server --install-extension ms-vscode.cpptools WORKDIR /app ENV PATH=${PATH}:/lib/dart/bin ENV PATH="${PATH}:/root/.pub-cache/bin" RUN pub global activate webdev RUN pub global activate stagehand CMD ["/works/code-server", "--auth","none", "--host","0.0.0.0","--port","8443", "/app"]docker-compose.ymlversion: '3' services: app: build: ./app ports: - "8080:8080" - "8443:8443" volumes: - ./app:/app # - /var/run/docker.sock:/var/run/docker.sock command: /works/code-server --auth none --host 0.0.0.0 --port 8443 /app具体的には、github を参照してください。
開発環境を起動する。
$ git clone https://github.com/kyorohiro/advent-2019-code-server.git $ cd advent-2019-code-server/extra/dart_fmi_and_go_and_c $ docker-compose build $ docker-compose up -dブラウザーで、
http://127.0.0.1:8443/
を開くGo で Shared Library を 作成する。
VSCode->File(Menu)->/app/wgo
に移動する。hello.gopackage main import "C" import "fmt" //export PrintHello func PrintHello() { fmt.Print("Hello,World") } func main() {}Goの PrintHello 関数を、Dart から呼ばれる予定です。
$ go build -o libhello.so -buildmode=c-shared hello.goとすると、
libhello.h
とlibhello.so
というファイルができます。
Dart で読み込む前に、C言語から読み込んでみましょう。main_hello.c#include <stdio.h> #include "libhello.h" int main(int argc, char const *argv[]) { PrintHello(); return 0; }Terminal$ gcc -Wall -o main_hello.exe main_hello.c -L. -lhello $ LD_LIBRARY_PATH=. ./main_hello.exe -L. -lhello Hello,Worldおっ!! 上手く動作しました。
Dart から読み込んでみる。
VSCode->File(Menu)->/app/wdart
に移動する。bin/main.dartimport 'dart:ffi' as ffi; typedef PrintHello_func = ffi.Void Function(); typedef PrintHello = void Function(); void main(List<String> arguments) { var path = "/app/wgo/libhello.so"; ffi.DynamicLibrary dylib = ffi.DynamicLibrary.open(path); final PrintHello hello = dylib .lookup<ffi.NativeFunction<PrintHello_func>>('PrintHello') .asFunction(); hello(); }Terminal$ dart ./bin/main.dart Hello,World
PS
C言語の環境も用意しました。
先ほどの、github レポジトリーから見れます。
Go言語で開発された機能をDartでフルに利用できます。ので、Dart から ほぼなんでも出来る状態になりました。
FMIがサポートされたことで、気軽に Native Interface を書けるようになったと思います。
サーバーサイド向けでしたら、開発には、Docker Image などが便利だと思います。
今回、使用した Code-Server について、下記 Advent にて
20回以上に分けて解説していますので、興味ある方は、ご参照ください。https://qiita.com/advent-calendar/2019/code-server
※ Docker Image として固めてHub に 配置するなどすれば、必ず動作するようになりますが、Image には固めていません。個人利用する際は、 Image に固めて保管しておくことをお勧めします。
Code
https://github.com/kyorohiro/my-code-server/tree/master/w/dart_and_go_and_c/
https://github.com/kyorohiro/advent-2019-code-server/tree/master/extra/dart_fmi_and_go_and_c
- 投稿日:2019-12-22T20:50:41+09:00
Code-Server x Dart fmi x Go x Clang
Dart Advent Calendar 2019 の 記事です。
Dart の Native Interface に ついての記事です。
Go や C言語の機能をDartで利用する方法について解説します。また、すぐに Native Interface を試せるように、 Docker Image を 用意しました。Native Interface とは : C言語 の ライブラリーを使用することができる機能です
https://dart.dev/guides/libraries/c-interop
C言語 の ライブラリーを使用することができる機能です。ただし、C言語と言ってもC言語でのライブラリーに閉じたものではないです。
Go言語 や Rust言語などのシステム言語は、Shared Libraryを作成することができます。
この Shared Library 経由すれば、Dartで GoやRustなどの機能も利用できます。
とても便利 : OSの機能をフルに使える
Dartは、まだまだライブラリーが不足している状態です。ですので、C言語で開発されたライブラリーや、Goで開発されたライブラリーを利用する必要があります。
Native Interface によち、 Dart がより便利になります。開発環境 : Docker 環境を用意しました。
Code-Server という VSCode が オンライン上で動作する エディターを含めました。
このため、 Dockerを起動すれば、即、開発ができます。https://github.com/kyorohiro/my-code-server/tree/master/w/dart_and_go_and_c/
https://github.com/kyorohiro/advent-2019-code-server/tree/master/extra/dart_fmi_and_go_and_c
FROM ubuntu:20.04 WORKDIR /works # install dart RUN apt-get update RUN apt-get install -y wget gnupg1 RUN apt-get install apt-transport-https RUN sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' RUN sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list' RUN apt-get update RUN apt-get -y install dart # install go RUN apt-get install software-properties-common -y #RUN add-apt-repository ppa:longsleep/golang-backports RUN apt-get update RUN apt-get install golang-go -y RUN apt-get install git -y RUN go get github.com/ramya-rao-a/go-outline RUN go get github.com/mdempsky/gocode RUN go get github.com/uudashr/gopkgs/cmd/gopkgs RUN go get github.com/sqs/goreturns RUN go get github.com/rogpeppe/godef # install c RUN apt-get install musl-dev -y # code-server RUN wget https://github.com/cdr/code-server/releases/download/2.1692-vsc1.39.2/code-server2.1692-vsc1.39.2-linux-x86_64.tar.gz RUN tar -xzf code-server2.1692-vsc1.39.2-linux-x86_64.tar.gz -C ./ --strip-components 1 RUN /works/code-server --install-extension Dart-Code.dart-code RUN /works/code-server --install-extension ms-vscode.go RUN /works/code-server --install-extension ms-vscode.cpptools WORKDIR /app ENV PATH=${PATH}:/lib/dart/bin ENV PATH="${PATH}:/root/.pub-cache/bin" RUN pub global activate webdev RUN pub global activate stagehand CMD ["/works/code-server", "--auth","none", "--host","0.0.0.0","--port","8443", "/app"]docker-compose.ymlversion: '3' services: app: build: ./app ports: - "8080:8080" - "8443:8443" volumes: - ./app:/app # - /var/run/docker.sock:/var/run/docker.sock command: /works/code-server --auth none --host 0.0.0.0 --port 8443 /app具体的には、github を参照してください。
開発環境を起動する。
$ git clone https://github.com/kyorohiro/advent-2019-code-server.git $ cd advent-2019-code-server/extra/dart_fmi_and_go_and_c $ docker-compose build $ docker-compose up -dブラウザーで、
http://127.0.0.1:8443/
を開くGo で Shared Library を 作成する。
VSCode->File(Menu)->/app/wgo
に移動する。hello.gopackage main import "C" import "fmt" //export PrintHello func PrintHello() { fmt.Print("Hello,World") } func main() {}Goの PrintHello 関数を、Dart から呼ばれる予定です。
$ go build -o libhello.so -buildmode=c-shared hello.goとすると、
libhello.h
とlibhello.so
というファイルができます。
Dart で読み込む前に、C言語から読み込んでみましょう。main_hello.c#include <stdio.h> #include "libhello.h" int main(int argc, char const *argv[]) { PrintHello(); return 0; }Terminal$ gcc -Wall -o main_hello.exe main_hello.c -L. -lhello $ LD_LIBRARY_PATH=. ./main_hello.exe -L. -lhello Hello,Worldおっ!! 上手く動作しました。
Dart から読み込んでみる。
VSCode->File(Menu)->/app/wdart
に移動する。bin/main.dartimport 'dart:ffi' as ffi; typedef PrintHello_func = ffi.Void Function(); typedef PrintHello = void Function(); void main(List<String> arguments) { var path = "/app/wgo/libhello.so"; ffi.DynamicLibrary dylib = ffi.DynamicLibrary.open(path); final PrintHello hello = dylib .lookup<ffi.NativeFunction<PrintHello_func>>('PrintHello') .asFunction(); hello(); }Terminal$ dart ./bin/main.dart Hello,World
PS
C言語の環境も用意しました。
先ほどの、github レポジトリーから見れます。
Go言語で開発された機能をDartでフルに利用できます。ので、Dart から ほぼなんでも出来る状態になりました。
FMIがサポートされたことで、気軽に Native Interface を書けるようになったと思います。
サーバーサイド向けでしたら、今回用意した Docker Image などが便利です。
今回、使用した Code-Server を使いましたので、
Auto Complete が 使えたりして便利です。今回 20回以上に分けて解説していますので、興味ある方は、ご参照ください。
https://qiita.com/advent-calendar/2019/code-server
※ Docker Image として固めてHub に 配置するなどすれば、必ず動作するようになりますが、Image には固めていません。個人利用する際は、 Image に固めて保管しておくことをお勧めします。
Code
https://github.com/kyorohiro/my-code-server/tree/master/w/dart_and_go_and_c/
https://github.com/kyorohiro/advent-2019-code-server/tree/master/extra/dart_fmi_and_go_and_c
- 投稿日:2019-12-22T20:33:52+09:00
go langでslackと連携 api
main.gopackage main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) var ( // change token IncomingUrl string = "https://hooks.slack.com/services/hogehoge/hogehoge" ) type Slack struct { Text string `json:"text"` Username string `json:"username"` Icon_emoji string `json:"icon_emoji"` Icon_url string `json:"icon_url"` Channel string `json:"channel"` } func main() { arg := "nekosasa" params, _ := json.Marshal(Slack{ //json形式のbinary 数字がいっぱい並んでる fmt.Sprintf("%s", arg), "arigatou", "dossy_bot", "http://www.icons101.com/icons/66/NuoveXT_by_Alexandre_Moore/128/slackware.png", "#test"}) //paramsがjson形式の[]uint8のbinary string(params)で中身は見れる resp, _ := http.PostForm( //postを整形して実行まで行う IncomingUrl, url.Values{"payload": {string(params)}}, ) body, _ := ioutil.ReadAll(resp.Body) //ioutil.Readallはerrがないか確認するもの defer resp.Body.Close() //respはreadしたらちゃんとcloseすべきである println(string(body)) //err == nilならreturn ok }ほぼ自分用
- 投稿日:2019-12-22T19:18:17+09:00
Goを真面目に勉強する〜3.関数と型〜
はじめに
Goをはじめて1年半。アウトプットが進まない私が、専門家の@tenntennさんから受けたマンツーマンレッスンの内容をまとめて、Goのスキルアップを目指します。Goの基礎から丁寧に学んでいきます。
記事のまとめは以下の通りで順次作成していきます。
今回は「3.関数と型」になります。シリーズの一覧
- Goについて知っておく事
- 基本構文
- 関数と型(今回)
- パッケージとスコープ(次回予定)
本記事の内容
今回学ぶ内容は以下の通りです。
- 組込み型
- コンポジット型
- 構造体
- 配列
- スライス
- マップ
- ユーザ定義型
- 関数
- 組込み関数
- 関数定義
- 無名関数
- メソッド
- メソッド値
- メソッド式
組込み型
(本項目は前回記事に記載のものと同様です)
Goでは以下の型が組込み型として用意されています。組込み型の種類と同時に、Goでは重要になってくるゼロ値も記載しました。
型名 説明 ゼロ値 int, int8, int16, int32, int64 符号付き整数型. intは32または64bit 0 uint, uint8, uint16, uint32, uint64 符号なし整数型. uintは32または64bit 0 float32, float64 浮動小数点数 0 uintptr ポインタ値を格納するのに十分な大きさの符号なし整数型 0 byte uint8のエイリアス 0 rune int32のエイリアス 0 string 文字列 "" bool 真偽値 false error エラー nil 参考:https://golang.org/ref/spec#Types
コンポジット型
コンポジット型は、複数のデータ型を集めて1つのデータ型にしたものです。構造体や配列がコンポジット型に当たります。Goではその他にスライス、マップといったコンポジット型が用意されています。
種類 説明 構造体 型の異なるデータを集めたデータ型 配列 同じ型のデータを集めて並べたデータ型 スライス 配列の一部を切り出したデータ型 マップ キーと値をマッピングしたデータ型 型リテラルとコンポジットリテラル
参考:https://golang.org/ref/spec#Composite_literals
コンポジット型を変数で利用するためには、型リテラルかコンポジットリテラルで宣言します。型リテラル
型リテラルは、型の具体的な定義を書き下した型の表現方法で、例えば構造体の場合次のように表現します。
// 構造体の型リテラル struct { name string age int }コンポジットリテラル
コンポジットリテラルは、型と値がセットになったもので、以下の書式で表現します。
書式
型リテラル "{" 値リスト "}"
struct { name string age int }{ name: "Gopher", age: 10, }コンポジット型の変数宣言
コンポジット型の変数宣言は、組込み型の変数宣言と同様、以下の構文で行います。
"var" 変数名 型リテラル
"var" 変数名 "=" コンポジットリテラル
変数名 ":=" コンポジットリテラル
次は構造体、配列、スライス、マップのそれぞれの変数宣言をみていきます。
構造体
構造体は型の異なるデータを集めたデータ型で、各変数はフィールドと呼ばれます。各フィールドは異なる型にすることができます。
struct { name string // フィールド nameはstring型 age int // フィールド ageはint型 }フィールドの型をコンポジット型にすることもできます。
struct { name string age int address struct { // addressを構造体の型リテラルで宣言する street string city string state string } number []string // numberをstringのスライスで宣言する }構造体の変数宣言
書式
"var" 変数名 型リテラル
各フィールドの値は、フィールドの型のゼロ値で初期化されます。// 変数pを構造体の型リテラルで宣言する var p struct { name string // nameはstring型のゼロ値=""で初期化される age int // ageはint型のゼロ値=0で初期化される }書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時に構造体のフィールドを初期化することができます。// 変数pを構造体のコンポジットリテラルで宣言する var p = struct { name string age int }{ name: "Gopher", age: 10, }書式
変数名 ":=" コンポジットリテラル
変数宣言時に構造体のフィールドを初期化することができます。この書き方は関数内でしか利用できません。// 変数pを構造体の型リテラルで宣言する p := struct { name string age int }{ name: "Gopher", age: 10, }構造体のフィールドが構造体の場合
フィールドが構造体の場合は少し注意が必要です。フィールド値は、型が明示されているか、型推論できる必要があります。
int
やstring
のような組込み型の場合は値リテラルを指定すれば型推論してくれますが、コンポジットの場合はコンポジットリテラルを与える必要があります。
次の例では、フィールドを構造体で宣言している場合の初期化をおこなっていますが、これを念頭に見れば理解できると思います。// 変数pを構造体の型リテラルで宣言する var p = struct { name string age int address struct { // addressを構造体の型リテラルで宣言する street string city string } }{ name: "Gopher", age: 10, address: struct { street string city string }{ street: "1-23-4", city: "Osaka", }, }構造体のフィールド参照
構造体のフィールドを参照するには
.
を使います。var p = struct { name string age int }{ name: "Gopher", age: 10, } p.age++ // p.ageをインクリメントする fmt.Println(p.name, p.age) // p.nameとp.ageを表示するこれを実行すると以下のような結果が得られます。
Gopher 11配列
配列は同じ型のデータを集めて並べたデータ型で、途中で要素数を変更することはできません。また、要素数が違えば違う型になります。
[5]int // 型と要素がセット [10]int // [5]intとは違う型配列の変数宣言
書式
"var" 変数名 型リテラル
各要素の値は、要素の型のゼロ値で初期化されます。var n [5]int // nの要素はint型のゼロ値=0で初期化される要素の型にはコンポジット型も使えます。この場合、各要素のフィールド値は、ゼロ値で初期化されます。
var ps [5]struct { name string // string型のゼロ値=""で初期化される age int // int型のゼロ値=0で初期化される } // 要素の型にはコンポジット型も使える書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時に配列の要素を初期化することができます。var n = [5]int{0, 1, 2, 3, 4}書式
変数名 ":=" コンポジットリテラル
変数宣言時に配列の要素を初期化することができます。この書き方は関数内でしか利用できません。n := [5]int{0, 1, 2, 3, 4}配列リテラルで
...
表記を使った場合、要素数を指定しなくても値から要素数を推論してくれます。また、一部の要素の値だけ指定することもできます。n := [...]int{0, 1, 2, 3, 4} // len=5, cap=5 m := [...]int{5: 10, 10: 100} // len=11, cap=1スライス
配列の一部を切り出したデータ型で、型情報に要素数は含めません。その特性から、スライスの背後には配列が存在することが前提になります。
スライスを構成している配列、要素数、容量がどのように管理されているのか、以下のスライスの内部構造を見れば分かります。
https://golang.org/search?q=slice#Global_pkg/runtimetype slice struct { array unsafe.Pointer len int cap int }スライスの型リテラルは以下の通りです。
[]int // 配列から要素数を省いた形で宣言するスライスの変数宣言
書式
"var" 変数名 型リテラル
この書き方でスライスを宣言した場合、背後の配列が存在しないため、スライスはnil
で初期化されます。var ns []int // nsはint型のスライスで、ゼロ値=nilで初期化される書式
変数名 ":=" コンポジットリテラル
背後の配列を用意してスライスを宣言することができます。// nsは要素数3, 容量3で初期化される // スライスが指す要素は1, 2, 3で初期化される ns := []int{1, 2, 3}要素数と容量を指定して初期化することもできます。
// nsは要素数3, 容量10で初期化される // 各要素は要素の型のゼロ値で初期化される ns := make([]int, 3, 10)スライスの拡張
スライスは
append
関数を利用して要素を追加することができます。append
関数を利用した際の挙動は、背後に用意された配列の容量によって変わります。
- 容量が足りる場合は、既存の領域に新しい要素をコピーして、スライスの要素を拡張します。
- 容量が足りない場合は、容量が十分足りるように、新しい配列を確保して、そこに元の配列から全ての要素をコピーします。
ns := make([]int, 3, 5) // 要素3、容量5で宣言する ns[0] = 1 ns[1] = 2 ns[2] = 3 fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns)) ns = append(ns, 4, 5) // 要素を2つ追加する。要素5、容量5になる。 fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns)) ns = append(ns, 6, 7) // 要素を2つ追加する。要素7、容量10になる。 fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns))この結果は次のように出力されます。
初期化時、要素3までが利用されていて、1回目のappend
関数で要素2つを追加したため、確保した容量5に収まっています。
次にappend
関数で2つ要素を追加すると、容量が足りないため、別の領域を確保して全ての要素をコピーしていることがわかります。確保される容量は、元の容量のおよそ2倍です。0x456000 len3 cap5 [1 2 3] 0x456000 len5 cap5 [1 2 3 4 5] // 最初と同じ領域 0x454030 len7 cap12 [1 2 3 4 5 6 7] // 新しく確保した領域マップ
マップはキーと値をマッピングしたデータ型です。宣言にはキーと値の型を指定します。キーには
==
で比較できる型しか使うことができません。
関数やスライスは==
で比較できないためキーには使えません。また、それらを要素として持つコンポジット型(構造体の場合はフィールド)もキーに使うことはできません。マップの変数宣言
書式
"var" 変数名 型リテラル
初期値はnil
で初期化されます。// キーがstring、要素がint型のmap mを宣言する var m map[string]int // キーが構造体、要素がint型のスライスのmap cを宣言する var c map[struct { x, y float64 }][]int書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時にマップの要素を初期化することができます。var m = map[string]int{"x": 10, "y": 20}書式
変数名 ":=" コンポジットリテラル
変数宣言時にマップの要素を初期化することができます。この書き方は関数内でしか利用できません。m := map[string]int{"x": 10, "y": 20}マップの操作
要素の参照
マップから値を取得するには、以下のように対応するキーを指定します。
// 参照 m := map[string]int{"x": 10, "y": 20} fmt.Println(m["x"]) // 10が表示される要素の追加
キーを指定して値を入力することで、マップの要素を追加することができます。
// 入力 m := map[string]int{"x": 10, "y": 20} m["z"] = 30 // 要素が追加される要素の削除
delete
を利用してマップから要素を削除することができます。// 削除 m := map[string]int{"x": 10, "y": 20} delete(m, "y") // キー="y"の要素を削除する要素の存在の確認
次のように書くことでキーの存在の確認することができます。
- キーが存在する場合、1つめの戻り値に要素の値が、2つめの戻り値に結果がbool型で得られます。
- キーが存在しない場合、1つめの戻り値は要素の型のゼロ値が、2つめの戻り値は結果falseが得られます。
var m = map[string]int{"x": 10, "y": 20} x, ok := m["x"] fmt.Println(x, ok) // 10 true z, ok := m["z"] fmt.Println(z, ok) // 0 falseユーザー定義型
type
で名前を付けると、任意の型を基底型としてユーザ定義型を定義することができます。// 組込み型を基にする type MyInt int // 他のパッケージの型を基にする type MyWriter io.Writer // 型リテラルを基にする type Person struct { Name string }構造体をユーザ定義型で定義した場合の例
構造体の節で「構造体のフィールドが構造体の場合」の注意点を挙げていましたが、ユーザ定義型を利用すると分かりやすくなります。type MyAddress struct { street string city string } var ns = struct { name string age int address MyAddress }{ name: "Gopher", age: 10, address: MyAddress{ street: "1-23-4", city: "Osaka", }, }ユーザ定義型のキャスト
基底型とユーザー定義型はお互いにキャストすることができます。
type MyInt int // int型を基底にしてMyIntを定義する var mi MyInt = 10 fmt.Printf("%[1]T %[1]d\n", mi) // main.MyInt 10 var i int = int(mi) fmt.Printf("%[1]T %[1]d\n", i) // int 10 mi = MyInt(i) fmt.Printf("%[1]T %[1]d\n", mi) // main.MyInt 10定数のデフォルトの型からキャストが可能な場合、型無しの定数から明示的なキャストをする必要はありません。
d := 10 * time.Second fmt.Printf("%T\n", d) fmt.Printf("%T\n", time.Second)型エイリアス
参考:https://golang.org/ref/spec#Type_declarations
Go 1.9以上では型のエイリアスを定義することができます。エイリアスは別名のことで、オリジナルの型と同じ動きをします。ただし、型エイリアスでつけた別名を使ってメソッドを定義することはできません。(メソッドはこのあとの節で説明します)
- 同じ型として機能する
- 基となる型とはキャスト不要
書式
"type" 識別子(エイリアス名) "=" 基となる型
識別子(エイリアス名)のスコープ内でエイリアスとして機能し、%T
で出力すると基となる型が表示されます。type Applicant = http.Client fmt.Printf("%T\n", Applicant{}) // http.Client関数
関数は一連の処理をまとめたもので、引数で受け取った値を基に処理を行い、戻り値として結果を返す機能を持ちます。引数や戻り値は複数持つことも、1つも持たないこともできます。
組込み関数
Goには次のような組込み関数が用意されています。
関数名 説明 print, println 表示を行う make コンポジット型の初期化 new 指定した型のデータ領域を確保 len, cap 配列やスライスの長さ、容量を取得する append スライスの要素を追加する delete 指定した要素を削除する panic, revocer パニックを発生、回復する 参考:https://golang.org/ref/spec#Predeclared_identifiers
ちなみに、これらは予約語ではないので、同じ名前の関数を定義することもできます。
関数の定義
参考:https://golang.org/ref/spec#Function_declarations
関数定義の書式は以下の通りです。引数、戻り値は省略できます。書式
"func" 関数名"("[引数]")" [戻り値] "{" 処理 "}"
// xとyの加算結果を返す関数addを定義する func add(x int, y int) int { return x + y }// 引数、戻り値が無い関数printHogeを定義する func printHoge() { println("Hoge") }関数の引数は、型が同じであればまとめることができます。
// xとyの加算結果を返す関数addを定義する func add(x, y int) int { return x + y }多値を返す関数を定義する場合、戻り値のリストを括弧で括ります。
// 多値を返す場合は、戻り値のリストに括弧をつける func swap(x, y int) (int, int) { return x, y // returnはカンマ区切りにする }多値の受け取りかた
多値を返す関数の場合、呼び出し側で複数の変数を用意して受け取ります。
// xが第一戻り値、yが第二戻り値 x, y := swap(10, 20)Goでは未使用の変数を許していないため、一部の戻り値を利用しない場合は、受け取りを省略します。省略にはブランク変数
_
を利用します。// 第一戻り値を省略 _, y := swap(10, 20) // 第二戻り値を省略 x, _ := swap(10, 20)名前付きの戻り値
引数と同様、戻り値にも変数名をつけて関数内で利用することができます。
// 戻り値にrx, ryという名前をつける func swap(x, y int) (rx, ry int) { rx, ry = x, y return }無名関数(関数リテラル、クロージャ)
参考:https://golang.org/ref/spec#Function_literals
無名関数はその名の通り、名前を付けていない関数のことで、関数リテラルで表現するものです。クロージャとも呼ばれます。
無名関数定義の書式は以下の通りで、引数、戻り値は省略可能です。書式
"func""("[引数]")" [戻り値] "{" 処理 "}"
無名関数では関数外の変数を参照することができます。
// msgの値を表示する無名関数を定義し、実行している msg := "Hello world!" func() { fmt.Println(msg) }()関数は値の一種なので、変数に代入することができます。
i := 0 f := func() { // func()型の変数fに代入する i++ fmt.Println(i) } f() // 関数fを実行する無名関数でも引数、戻り値を使えます
r := func(x, y int) int { return x + y } fmt.Println(r(1, 2)) // 3無名関数を使用する上で注意するべきは、無名関数内で使っている変数のスコープです。次の場合、f()を実行すると何が表示されるでしょうか?
ns := []int{1, 2, 3} fs := make([]func(), len(ns)) for i, n := range ns { fs[i] = func() { fmt.Println(i * n) } } for _, f := range fs { f() // どうなる? }
f()
を実行すると次の結果が得られます。
0×1, 1×2, 2×3ではなく、2×3, 2×3, 2×3が実行されます。6 6 6
f()
で使われている変数i
とn
は、fs[i]
に代入したタイミングではなく、f()
を実行する時点でのi
とn
の値で計算されます。メソッド
参考:https://golang.org/ref/spec#Method_declarations
Goにはクラスという概念はありませんが、データと操作を紐付ける仕組みはメソッドとレシーバによって作ることができます。
関数名 説明 メソッド レシーバと紐付けられた"関数"。メソッドにはメソッド名を付ける必要がある。レシーバの基本型にバインドされている。 レシーバ メソッドと紐付けられた"値"。レシーバの型には定義済みの型か、定義済みの型のポインタを使用できる。ただし、レシーバの型はメソッドと同じパッケージ内で定義している必要がある。 メソッドの定義と呼び出し
メソッドを定義する構文は以下の通りで、引数、戻り値は省略可能です。
書式
"func" "("レシーバ")" メソッド名"("[引数]")" [戻り値] "{" 処理 "}"
メソッドを呼び出す構文は以下の通りです。
書式
レシーバ "." メソッド名 "(" [引数] ")"
type Hex int func (h Hex) String() string { // Hex型の変数hをレシーバとして受け取る return fmt.Sprintf("%x", int(h)) } func main() { var hex Hex = 100 fmt.Println(hex.String()) // Stringメソッドを呼び出す }レシーバとして渡す値にはコピーが発生する
メソッド呼び出し時にレシーバとして渡した値は、通常の引数と同じ扱いになるためコピーが発生します。したがって、メソッド内で変更を加えてもレシーバには反映されません。type T int func (t T) f() { t++ // インクリメント } func main() { var t T = 100 t.f() fmt.Println(t) // 100 }レシーバをポインタにすることで、メソッド内の変更を反映することができます。レシーバがポインタの場合でも、
.
でメソッドを呼び出すことができます。type T int func (t *T) f() { *t++ // インクリメント } func main() { var t T = 100 t.f() // (&t).f()でも同じ fmt.Println(t) // 101 }*T型はTメソッドも自身のメソッドとして扱われる
t.f()
は(&t).f()
と同じであることから分かるように、*TはTのメソッドを呼び出すことができます。ただし、その逆、Tが*Tのメソッドを呼び出すことはできません。func (t T) f() {} func (t *T) g() {} func main() { (T{}).f() // T (&T{}).f() // *T (*&T{}).f() // T (T{}).g() // T これはできない (&T{}).g() // *T (*&T{}).g() // T }メソッドの定義における禁止事項
- ポインタ型を基底とする型やインターフェース型は利用できない
type T *int func (t T) f() { // Tはint型のポインタなのでレシーバに使えない fmt.Println("hello world!") }
- 構造体をレシーバとする場合、構造体のフィールド名と同じメソッド名を付けることはできない
type Member struct { name string // フィールド名 "name" } func (m Member) name() { // フィールド名と同じメソッド名 "name"は付けられない fmt.Println(m.name) }メソッド値
参考:https://golang.org/ref/spec#Method_values
メソッド値はメソッドを値として表したもので、レシーバは束縛された状態になります。書式
レシーバ "." メソッド名
以下の例では、
f
、g
にメソッド値を値として保存しているので、保存した時点のhexの値を使って関数が実行されます。type Hex int func (h Hex) String() string { return fmt.Sprintf("%x", int(h)) } func main() { var hex Hex = 100 f := hex.String // メソッド値はレシーバとしてHex型の値100を束縛している hex++ g := hex.String // メソッド値はレシーバとしてHex型の値101を束縛している fmt.Println(f(), g()) // 64 65 }次の例のようにレシーバをポインタにした場合、束縛されるレシーバ値が変数へのポインタなので、
f
、g
にメソッド値を値として保存するのもポインタになりますので、上の結果とは異なります。type Hex int func (h *Hex) String() string { return fmt.Sprintf("%x", int(*h)) } func main() { var hex Hex = 100 f := hex.String // メソッド値はレシーバとしてHex型の値へのポインタを束縛している hex++ g := hex.String // メソッド値はレシーバとしてHex型の値へのポインタを束縛している fmt.Println(f(), g()) // 65 65 }メソッド式
参考:https://golang.org/ref/spec#Method_expressions
メソッド式はメソッドを式として表したもので、レシーバを第1引数とした関数になります。書式
レシーバ型 "." メソッド名
type Hex int func (h Hex) String() string { return fmt.Sprintf("%x", int(h)) } func main() { var hex Hex = 100 f := Hex.String // メソッド式 fmt.Printf("%T\n%s\n", f, f(hex)) // 実行時にレシーバを第一引数に渡す }余談
メソッドの説明を書くためにサンプルコード作っていてハマったもの。
https://play.golang.org/p/dRwiBklOuD9最後に
無名関数はほぼ使ったことが無かったため理解するのに少し時間がかかりました。メソッド値、メソッド式については触れたことが無いので、これを書いている時点でも不安がありました。
前回までよりも量が多くて、書き終えるのに時間がかかってしまったので、次に書くパッケージとスコープについては、もう少しスピーディに進めようと思います。
- 投稿日:2019-12-22T17:31:39+09:00
Go言語でつくるインタプリタを Haskell で書く
この記事は?
- Haskell の基本機能だけでプログラミング言語作成したのでご紹介します
- 既存のライブラリやパーサジェネレータ等を使用せずにプログラミング言語を作成
- 元ネタは Go 言語で書かれた「Go言語でつくるインタプリタ」という書籍
- https://www.oreilly.co.jp/books/9784873118222/
- この書籍で作成するプログラミング言語の名は Monkey
- Haskell 等の関数型言語でいちからプログラミング言語作ってみたい方の一助となれば・・
作成した理由
- 元ネタである「Go言語でつくるインタプリタ」に感銘を受けました
- 非常に簡潔に書かれた書籍であり僕の様な CS の基礎知識のない人間でもある程度理解できた
- Go で書かれているというのも「簡潔さ」と「理解のしやすさ」の一因ではないかと思います
- Haskell を好むため同じものを Haskell でも作りたくなりました
作成した Haskell 製インタプリタ
https://github.com/kita127/haskey
- 名前は Haskey です
- 前述の Go 言語製インタプリタ Monkey をもじったネーミングです
Haskey の機能紹介
「Go言語でつくるインタプリタ」第4章までのハッシュを除いた機能全てを実装しました。
詳細は GitHub リポジトリを参照願います以下 Haskey を REPL として実行した際の結果です
prompt$ haskey Hello! This is the Haskey programming language! Feel free to type in commands Usage: haskey --help >> let a = 100; >> let b = 200; >> a + b; 300 >> if(a > 1000){ true; }else{ false; }; False >> let add = fn(x, y) { x + y }; >> let sub = fn(x, y) { x - y }; >> let applyFunc = fn(a, b, func) { func(a, b) }; >> applyFunc(2, 2, add); 4 >> applyFunc(10, 2, sub); 8 >> "Hello" + " " + "World"; Hello WorldHaskey における関数は値(ファーストクラス)として扱えます
>> fn(a, b){ a + " " + b }("love", "Haskell"); love Haskell >> let applyFunc = fn(a, b, func) { return func(a, b); }; >> applyFunc(2, 2, add); 4 >> applyFunc(10, 2, sub); 8クロージャもサポートしています
>> let mulXFunc = fn(x){ fn(y){ y * x; } }; >> let doubleFunc = mulXFunc(2); >> doubleFunc(100); 200 >> doubleFunc(200); 400 >> let threeTime = mulXFunc(3); >> threeTime(100); 300 >> threeTime(200); 600作成時の取り決め
- 元ネタである Go 製インタプリタ Monkey とほぼ同程度の機能を実装する
- Haskell における基本機能のみを使用
- 例外で使用したパッケージ
- text
- 文字列に使用
- containers
- HashMap で使用
- optparse-applicative
- コマンドライン引数の処理に使用
- HUnit
- ユニットテストで使用
- raw-strings-qq
- 主にテスト時の生文字列作成に使用
例外で仕様したパッケージは文字列処理のパフォーマンスの向上やユニットテストを簡潔に扱うためであり、インタプリタ作成における主要な箇所には基本的に使用していません。
Haskey 作成の概要
作成の大まかな手順は以下の通りです。これらをテストファーストで実装していきました。
- 字句解析実装
- 構文解析実装
- 評価器実装
- 配列、ビルトイン関数などのその他機能を実装
ひとつひとつをもう少し掘り下げて説明します。
字句解析
- 対象モジュール
- src/Haskey/Lexer.hs
入力されたソースコード
let hoge = 123;
などを[LET, IDENTIFIRE "hoge", ASSIGN, INT 123, SEMICOLON]
のように
トークン化していく工程です。特に Haskell 的な型システムによる抽象化などはなくひたすら泥臭く実装しました。Go との違いはほとんどなかった気がします。
Go 版は構文解析の際に必要な数のトークンのみ都度作成するのに対して、Haskell 版は入力を一気に先頭からトークンにして
それをまるっと次の処理(構文解析器)に渡す設計です。そのくらいしか Go との違いはなかった気がします。以下の
nextToken
関数内のガード記法で文字を引っ掛けてトークンを作成していきます。-- | nextToken -- nextToken :: T.Text -> (Tok.Token, T.Text) nextToken s | T.null s = (eof, "") | C.isSpace ch = nextToken remain | -- skip white space T.isPrefixOf "==" s = readFix Tok.Eq "==" s | T.isPrefixOf "!=" s = readFix Tok.NotEq "!=" s | ch == '=' = (newToken Tok.Assign ch, remain) | ch == '+' = (newToken Tok.Plus ch, remain) | ch == '-' = (newToken Tok.Minus ch, remain) | ch == '!' = (newToken Tok.Bang ch, remain) | ch == '*' = (newToken Tok.Asterisk ch, remain) | ch == '/' = (newToken Tok.Slash ch, remain) | ch == '<' = (newToken Tok.Lt ch, remain) | ch == '>' = (newToken Tok.Gt ch, remain) | ch == ';' = (newToken Tok.Semicolon ch, remain) | ch == '(' = (newToken Tok.Lparen ch, remain) | ch == ')' = (newToken Tok.Rparen ch, remain) | ch == ',' = (newToken Tok.Comma ch, remain) | ch == '{' = (newToken Tok.Lbrace ch, remain) | ch == '}' = (newToken Tok.Rbrace ch, remain) | ch == '[' = (newToken Tok.Lbracket ch, remain) | ch == ']' = (newToken Tok.Rbracket ch, remain) | ch == '"' = readString s | isLetter ch = readIdentifire s | C.isDigit ch = readNumber s | otherwise = (newToken Tok.Illegal ch, remain) where ch = T.head s remain = T.tail s eof = Tok.Token { Tok.tokenType = Tok.Eof, Tok.literal = "" }構文解析
- 対象モジュール
- src/Haskey/Parser.hs
前工程で作成したトークンから抽象構文木(AST)を作成する工程です。
AST を構成する要素としては文(Statement)があり文はまた式(Expression)などから構成されます。Haskell での攻略方法ですが、既存のライブラリこそ使用していないものの結局自前でパーサコンビネータを作成して対応しました。
パーサコンビネータとは小さなパーサを組み合わせてより大きなパーサを組み立てる手法です。
色々考えたんですが Haskell に於いて「パースする」という作業はパーサコンビネータを使用するのが一番簡潔という自分なりの結論に達したわけです。
具体的には Parser 型を定義し、Monad 型クラスのインスタンスとしました。Parser 型が処理したい内容は主に以下です。
- Token から抽象構文木を生成する
- Token 配列を入力に受け取る
- Token が構文エラーの場合はエラー処理をする
上記のうち 2, 3 は Haskell の文脈処理に落とし込みました。つまり Monad 型クラスのインスタンスである Parser 型がサポートするバインド関数(>>=)や fmap 関数などに
処理を任せてプログラマ(僕)はトークンから文や式を生成することだけに集中します。そのための下準備として Parser 型を Monad 型クラスのインスタンスとするための実装をしました。-- Parser combinator newtype Parser a = Parser { runParser :: [Tok.Token] -> Result a} data Result a = Done a Remaining | Fail Reason Remaining deriving (Eq, Show) type Remaining = [Tok.Token] type Reason = String instance Functor Parser where -- fmap :: (a -> b) -> Parser a -> Parser b fmap g p = Parser (\input -> case runParser p input of (Fail reason remaining) -> Fail reason remaining (Done result remaining) -> Done (g result) remaining ) instance Applicative Parser where -- pure :: a -> Parser a pure v = Parser (\input -> Done v input) -- <*> :: Parser (a -> b) -> Parser a -> Parser b pg <*> px = Parser (\input -> case runParser pg input of (Fail reason remaining) -> Fail reason remaining (Done result remaining) -> runParser (fmap result px) remaining ) instance Monad Parser where -- (>>=) :: Parser a -> (a -> Parser b) -> Parser b p >>= f = Parser (\input -> case runParser p input of (Fail reason remaining) -> Fail reason remaining (Done result remaining) -> runParser (f result) remaining ) -- return :: a -> Parser a -- return's default implementation is pure -- fail :: String -> m a fail s = Parser (\input -> Fail s input)一応、自前でモナドを作成する場合は「モナド則」というのを満たす必要があるのですがこの Parser 型はモナド則を満たせているかの確認は全くしていません。
Monad 型クラスのインスタンスにするためには Applicative 型クラスのインスタンスにする必要があり、そのためには Functor 型クラスのインスタンス
にする必要がある・・・といった具合で Monad のインスタンスにするためにはそれなりに手間が必要でこのあたり少し面倒でした。しかし、作りきってしまえばその後のパーサの作成は驚くほどすんなり進みました。
まず、パースするのについてまわる構文エラーの処理やトークンの入力処理など自分の手で書く必要がなくなりました。
それらは事前に実装した Monad 型クラスがサポートする関数たちがいい感じでやってくれます。この後はテストファーストを心がけながら、小さなパーサを組み合わせていくだけで自然と書きあがりましたいくつかパーサのソースをピックアップします
Let 文(識別子の定義)のパーサ
-- | parseLetStatement -- parseLetStatement :: Parser Ast.Statement parseLetStatement = Ast.LetStatement <$> next parseLet <*> next parseIdentifire <* next parseAssign <*> parseExpression Lowest <* goAheadIfNextSemicolonif 式のパーサ
-- | parseIfExpression -- parseIfExpression :: Parser Ast.Expression parseIfExpression = Ast.IfExpression <$> next (expectCur Tok.If) <*> parentheses (parseExpression Lowest) <* nextToken <*> parseBlockStatement <*> (parseElseBlock <|> pure Ast.NilStatement)Haskell の構文をあまり知らない方でも「ああ, Let 文のパーサは let のパースと Identifire のパースと・・(略)を組み合わせ作っているんだな」
というのがわかるかと思います。Go との違いはやはりパーサコンビネータを中心に構文解析を構築したところでしょうか。
それによりパーサを組むという処理以外の煩わしい部分(エラー処理や Token の入力)などは自前で書く必要もなくなりました。
Go 版はソースコードのテキスト先頭から順にトークン化しエラー処理を交えながら構文解析をするという感じの設計です。評価
- 対象モジュール
- src/Haskey/Evaluator.hs
前工程で構築した AST を実際に評価し結果を得ます。
5 + 5;
という入力に対しては10
という結果を、let f = fn(){ return "hello"; }; f();
に対してはhello
という結果を得ます。こちらも色々考えた結果「評価する型」を作成し、Monad による文脈処理で攻略することにしました。
Evaluator 型を定義し、処理させたい内容として以下を実装しました。
- 環境を入力に評価した結果を返す
- 環境の状態を扱う
- 評価が失敗した際のエラー処理を行う
ここに現れる「環境」というのは定義した識別子とデータを紐付けた情報です。
評価時に出現した識別子の情報を環境に持たせて併せて評価します。
これもまた 2, 3 の処理は自動で処理するよう文脈に落としこみました。以下の通り Evaluator 型を定義し Parser 型を定義した時と同じ要領で Monad 型クラスのインスタンスにします。
環境(Environment)と組になっている Buffer は後にご説明します。newtype Evaluator a = Evaluator {runEvaluator :: (Obj.Environment, Buffer) -> Result a}これもまた モナド則の確認はしていません。
次に評価関数(eval) を実装する Node 型クラスを定義し AST を構築する 文(Statement 型)や式(Expression 型)をそのインスタンスとすることにより再帰的に評価を行っていきます。
-- | class Node -- class Node a where eval :: a -> Evaluator Obj.Object instance Node Ast.Program where eval = evalProgram . Ast.statements instance Node Ast.Statement where eval (Ast.LetStatement _ name v) = eval v >>= set (Ast.expValue name) >> return Obj.Void eval (Ast.ReturnStatement _ e) = Obj.ReturnValue <$> eval e eval (Ast.ExpressionStatement _ e) = eval e eval (Ast.BlockStatement _ stmts) = evalBlockStatement stmts eval (Ast.FailStatement _ s) = return $ Obj.Error s eval _ = return $ Obj.Error "unknown node" instance Node Ast.Expression where eval (Ast.Identifire _ v) = evalIdentifire v eval (Ast.IntegerLiteral _ v) = pure $ Obj.Integer v eval (Ast.Boolean _ v) = pure (if v then Obj.Boolean True else Obj.Boolean False) eval (Ast.PrefixExpression _ op r) = eval r >>= evalPrefixExpression op eval (Ast.FunctionLiteral _ params' body') = Obj.Function params' body' <$> getEnv eval (Ast.CallExpression _ func args) = join $ liftM2 applyFunction (eval func) (mapM eval args) eval (Ast.InfixExpression _ l op r) = join $ liftM2 (evalInfixExpression op) (eval l) (eval r) eval (Ast.IfExpression _ cond cons alte) = eval cond >>= (\c -> if isTruthy c then eval cons else evalIfExists alte) eval (Ast.StringLiteral _ s ) = pure $ Obj.String s eval (Ast.ArrayLiteral _ es) = Obj.Array <$> mapM eval es eval (Ast.IndexExpression _ left index) = join $ liftM2 evalIndexExpression (eval left) (eval index)Go との違いはこちらも構文解析と同じよう評価コンビネータ?を定義しエラー処理や環境の引き回しを自動で行うよう設計しました。
ここまでで評価の実装も完了プログラム言語として最低限機能するようになりました。
その他の実装
その後以下の機能を実装しました。
- 配列の実装
- ビルトイン関数の実装
- 標準出力の実装
これらはもはやは消化試合と考えていたんですが標準出力関数
puts()
の実装でつまずきました。
恐らくちゃんとした Haskeller なら事前に気づくと思うようなことなのですが。Go 版は
puts()
評価時に Print するのですが Haskell は言語使用上副作用や IO の伴う処理は原則専用の型の関数でしか扱えません。
これまでの設計で評価関数は IO を処理できる関数として設計していなかったため、そのままでは標準出力できません。
色々悩みましたが解決策としては評価時に入力として環境と共に Buffer を引数にとり、標準出力が生じた際にはバッファリングし main 関数内で評価の合間にフラッシュする設計としてお茶を濁しました。まとめ
Haskell の基本機能のみでプログラミング言語が書けるのかは不安でしたが、実際は「Go言語でつくるインタプリタ」の内容を写経 + αくらいの感覚で書けました。
もともとの Go 版の実装がシンプルであり、他の言語に簡単に置き換え可能な Go 自体のポテンシャルにも改めて気きづきました。Haskell としての良さはやはりエラー処理、状態の引き回しなど本質的な部分以外を
型システムに落とし込むことにより、本来やりたい作業だけに集中できるというのが非常に気持ちよかったです。
- 投稿日:2019-12-22T17:05:38+09:00
Goで「HackerNews頻出英単語 1200」をつくる
こんにちは、この記事はGo Advent Calendar 2019 22日目の記事です。
英語の勉強における英単語
いきなりですが、最近、趣味で英語の勉強をしています。
せっかく作った趣味なので、目標としてTOEFL 100点という目標を置いて
TOEFLテスト英単語3800という単語帳をやってます。この単語帳 TOEFL界隈では非常に有名な単語帳でして、これさえやればTOEFLの点数 10点上がると言われています。(ほんまかいな)
実際にこの単語帳をやって、TOEFLの試験をやるとおぉ、めっちゃ単語出てくる!と言った感じで
単語の意味がわかると、難解で有名なTOEFLの文章も読めるようになります。HackerNews英単語帳を作れないか
ここから派生して、エンジニア界隈で有名なテックニュースサイト HackerNewsも
英単語帳を作って覚えれば、スラスラHackerNewsのサイトが読めるのでは?というアイデアが湧いてきました。「TOEFLの英単語帳覚えちゃえばHackerNewsもスラスラ読めるようになるんじゃないの?」と思ったあなた、さすがですね。
自分も最初はそう思ってました。しかし、TOEFLはあくまで大学・大学院の留学用の試験のため、さまざまな学術的な単語が出てきます。
例えば、platypus(カモノハシ)とか。カモノハシは実際に、TOEFL英単語にも載っています。HackerNewsには、そういった学術的な単語というより、技術的な用語が多いです。そのため、TOEFLの単語を覚えてもHackerNewsは読めるようにはなりません。
HackerNewsを読めるようになるには、技術的英単語を覚える必要があります。そこで、今回はGoを用いて、HackerNewsの頻出英単語帳を作ってみたいと思います。
幸いにも、Goには自然言語処理やスクレイピングのライブラリがたくさんあるので、簡単に作ることができます。
ソースコード
ソースコードは以下です。
https://github.com/pco2699/hackernews1200処理の流れ・プロジェクト構成
処理の流れは次の通りです。
- HackerNews APIからHTMLを取得する(fetch)
- HTMLからタグを取り除きTextだけにする(extract)
- TextをToken化・Taggingする(tokenize)
- 単語の集計を行う(count)
プロジェクトの構成も上記の処理の流れに沿っています。
. ├── LICENSE ├── README.md ├── cmd │ ├── fetcher.go # 1. HackerNews APIからHTMLを取得する │ ├── extractor.go # 2. HTMLからタグを取り除きTextだけにする │ ├── tokenizer.go # 3. TextをToken化・Taggingする │ └── counter.go # 4. 単語の集計を行う ├── collections │ └── counter.go # 集計用のデータ構造 ├── go.mod ├── go.sum └── main.goHackerNews APIからHTMLを取得する
実はHackerNewsはAPIが公開されており、自由に記事を取得することができます。
こちらのGithubにAPIの仕様が載っています。HackerNews APIの仕様
今回は以下の2つエンドポイントを叩いてみます。
New, Top and Best Stories
Up to 500 top and new stories are at /v0/topstories (also contains jobs) and /v0/newstories. Best stories are at /v0/beststories.
Example: https://hacker-news.firebaseio.com/v0/topstories.json?print=prettyこのエンドポイントで「New or Top or Best」な記事 500コが取得できます。
取得すると、以下のようなStoryのIDのArrayが取れます。[ 9129911, 9129199, 9127761, 9128141, 9128264, 9127792, 9129248, 9127092, 9128367, ..., 9038733 ]今回はこの中でも「Top」のエンドポイントを利用してみます。
(TopとBestの違いがあまりわかっていない。)A story
a story: https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty
上記でとってきた500のTopStoriesの詳細を、Story APIで取得します。
{ "by" : "dhouston", "descendants" : 71, "id" : 8863, "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ], "score" : 111, "time" : 1175714200, "title" : "My YC app: Dropbox - Throw away your USB drive", "type" : "story", "url" : "http://www.getdropbox.com/u/2/screencast.html" }この中でも必要なのは
url
のみなので、URLのみフィルタリングします。GoでHackerNews APIにアクセスしてみる
まずは、
fetcher.go
で、HackerNews APIへアクセスし、HTMLをgoquery.Document
形式のArrayで取得します。
(goqueryは後ほど、詳しく説明します。)GoでAPIへのアクセス、JSONのMarshall, Unmarshallはすべて標準パッケージで行えます。
fetcher.gopackage cmd import ( "encoding/json" "fmt" "io/ioutil" "net/http" "strconv" "strings" "github.com/PuerkitoBio/goquery" ) const ( TopStories = "https://hacker-news.firebaseio.com/v0/topstories.json" Story = "https://hacker-news.firebaseio.com/v0/item/{{number}}.json" ) func Fetch() ([]*goquery.Document, error) { // TopStoriesを取得する stories, err := fetchTopStories() if err != nil { return nil, err } // TopStoriesからそれぞれの記事情報を取得 articles, err := fetchArticles(stories) if err != nil { return nil, err } // 記事情報のURLからHTMLを取得し、goqueryのarrayに docs := fetchDocuments(articles) return docs, nil } // TopStoriesを取得する関数 func fetchTopStories() ([]int, error) { if stories, err := fetch(TopStories); err == nil { return stories, nil } else { return nil, err } } // TopStoriesへアクセスする実態の関数 func fetch(url string) ([]int, error) { // httpで該当のURLへアクセス resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() // レスポンスのbodyをすべてbytes[]型で取得 bytes, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } // json.Unmarshalでjsonをint[]のarrayにマッピングする var array []int err = json.Unmarshal(bytes, &array) if err != nil { return nil, err } return array, nil } // 以降の処理は省略HTMLからタグを取り除きTextだけにする
HackerNewsから記事を取得したので、
goquery
を使って、不要なタグを除去してテキストのみにします。
goquery
はjQueryライクにHTMLの内容抽出を行えるライブラリです。
Goを用いた、スクレイピングの際に非常に便利です。
extractor.go
の処理は非常にシンプルです。
script
タグやstyle
タグなど、HTMLタグ中で単語と集計して困るテキストをすべて除去し
最後にbodyタグ内のテキストを抽出しています。extractor.gopackage cmd import ( "github.com/PuerkitoBio/goquery" ) func Extract(docs []*goquery.Document) ([]string, error) { var texts []string for _, doc := range docs { text := extract(doc) texts = append(texts, text) } return texts, nil } func extract(doc *goquery.Document) string { doc.Find("script").Each(func(i int, el *goquery.Selection) { el.Remove() }) doc.Find("style").Each(func(i int, el *goquery.Selection) { el.Remove() }) doc.Find("noscript").Each(func(i int, el *goquery.Selection) { el.Remove() }) return doc.Find("body").Text() }TextをToken化・Taggingする
HTMLタグをテキストにした後は、TextのToken化・Taggingを行います。
Token化は、スペース・改行などを余計な情報を取り除き、単語の集計をしやすくします。
Taggingは、それぞれの単語が何なのかの情報を付与します。
たとえば「write -> 現在形の動詞」といった形です。この一連の処理は、
prose
というライブラリですべて行うことができます。tokenizer.gofunc Tokenize(texts []string) ([]prose.Token, error) { var tokens []prose.Token for _, text := range texts { if t, err := tokenize(text); err == nil { tokens = append(tokens, t...) } else { return nil, err } } return tokens, nil } func tokenize(text string) ([]prose.Token, error) { doc, err := prose.NewDocument(text) if err != nil { return nil, err } return doc.Tokens(), nil }
prose.NewDocument
でテキスト情報をほおりこむだけでToken化、Taggingをやってくれるのはびっくりしました。単語の集計を行う
最後に単語の集計を行います。
集計には、以下の複数のデータ構造を組み合わせたものを用います。
データ構造 用途 ハッシュマップ すでに出ている単語の管理 優先度付きキュー(ヒープ) 出てきた頻度の集計・頻度が高いものを取り出し ちょうど同じようなデータ構造を作ってくれている方がいたので、このデータ構造をベースにさせていただきました。
counter.gopackage cmd import ( "github.com/pco2699/hackernews1200/collections" "gopkg.in/jdkato/prose.v2" ) func Count(tokens []prose.Token) []collections.CounterItem { counter := collections.NewCounter() for _, token := range tokens { counter.AddItems(token.Text) } return counter.MostCommon(1200) }こちらで一通り、処理ができたので、
main.go
でそれぞれを呼び出します。
main.go
の最後では、できた単語をhackernews1200.txt
に吐き出しています。main.gopackage main import ( "bufio" "fmt" "log" "os" "github.com/pco2699/hackernews1200/cmd" ) func main() { fmt.Println("Fetching HackerNews API...") docs, err := cmd.Fetch() if err != nil { fmt.Println(err.Error()) } fmt.Println("Extracting HTML...") texts, err := cmd.Extract(docs) if err != nil { fmt.Println(err.Error()) } fmt.Println("Tokenize HTMLs...") tokens, err := cmd.Tokenize(texts) if err != nil { fmt.Println(err.Error()) } fmt.Println("Counting tokens...") items := cmd.Count(tokens) file, err := os.OpenFile("hackernews1200.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Fatalf("failed creating file: %s", err) } datawriter := bufio.NewWriter(file) for _, item := range items { fmt.Fprintf(datawriter, "Text: %v Count: %v\n", item.Value, item.Count) } datawriter.Flush() file.Close() }動かしてみよう
動かして10分ほど待つと、
hackernews1200.txt
ができています。
早速、中身を見てみましょう。こちらからGithub上のものも確認できます。hackernews1200.txtText: , Count: 41753 Text: . Count: 33799 Text: the Count: 33142 Text: to Count: 20756 Text: of Count: 17875 Text: and Count: 16858 Text: a Count: 16147 Text: " Count: 13158 Text: in Count: 11258 Text: that Count: 9044 Text: is Count: 8931 Text: ) Count: 7260 Text: : Count: 7008 Text: for Count: 6881 Text: ( Count: 6475 Text: 's Count: 6306 Text: it Count: 5769 Text: on Count: 5663 Text: with Count: 5120 Text: The Count: 4674 Text: I Count: 4657 Text: you Count: 4597 Text: are Count: 3981 Text: as Count: 3775 Text: be Count: 3675 Text: this Count: 3549 Text: by Count: 3519 Text: at Count: 3306 Text: was Count: 3151 Text: from Count: 3056 Text: or Count: 3037 Text: have Count: 2962 Text: an Count: 2837 Text: can Count: 2717 Text: not Count: 2706 Text: we Count: 2601 Text: n't Count: 2387 Text: your Count: 2208現状、Taggingの内容によって内容をフィルタリングしていないため、ピリオドやカンマが最頻出単語になってしまってます。(当たり前)
面白い点としては、実装をUPDATE次第、こちらの単語帳も更新していきたいと思います。
今後の改善ポイント
英語全体で頻出な単語が集計されている
is
とかa
などの英語全体での頻出単語が出力されている状態です。
そのため、英語全体での頻出英単語は取り除く処理を入れる必要があります。幸いにも、Googleをスクレイピングした頻出英単語集はあるので、それをもとに、集計から除外する処理を入れればよいです。
Goルーチンで並行処理化を行う
現状、一切、Goルーチンを使ってません。
TopStoriesが取れれば、それ以降は処理を並列化できます。そのため、処理を並列化してより処理の高速化を図りたいと思います。
- 投稿日:2019-12-22T16:47:44+09:00
ポケモン名だけで作った「いろは歌」を高速に生成する(ビット演算+DFS+Go+GCP)
みなさんポケットモンスター ソード&シールドやってますか?私は開会式に参加したところで中断しており、まだバッジ0個です。
さて、ポケモンを愛する皆さんであれば、一度はポケモン名でいろは歌を作りたいと思ったことがあるはずです。つまりどういうことかというと、ア〜ンまでのカタカナ45音を重複なく一度ずつ利用したポケモン名の列挙をしたいということです。例えば、以下の12体のポケモンの組み合わせは上記条件を満たしているので、いろは歌として成立しています。
ではこのような組み合わせが、初代〜ソード&シールドまでの890匹の全ポケモンで一体何パターン存在するのでしょうか。私は気になって夜も眠れません。そこでこの記事では、高速にポケモンいろは歌の全列挙を行うためのアルゴリズムと実装の考察を行います。
先行研究として、このようなポケモン名によるいろは歌生成は9-8. vcoptでポケモン「いろは歌」できるかな(世界初) | Vignette & Clarity(ビネット&クラリティ)で既に取り組まれています。こちらでは遺伝的アルゴリズムを用いた探索を行われているようです。このアプローチでは汎用的なライブラリで問題を解くことができる一方で、特定の文字を持つポケモンのスロットを決め打ちするなど、問題設定の工夫が必要になっていそうです。
問題設定
- カナ45音を一度ずつ全て使ったポケモン名の組み合わせが何パターン存在するかを計算する
- 本来いろは歌は「ン」を含めて48音ですが、今回は「ゐ」「ゑ]「を」を省いた45音とします。
- 記号を含むポケモン名は、読みのカタカナに変換する
- 例)
ポリゴンZ
→ポリゴンゼット
- 長音や読みが無い記号は無視
タイプ:ヌル
のコロンは無視します- メガシンカやアローラの姿などは除外する
濁音、半濁音は取り除く
小文字は大文字に直す
下2つのルールにより、例えば
ピカチュウ
はヒカチユウ
となります。
ちなみにイーブイ
はイ
が2回含まれているので絶対にいろは歌で登場することはありません。ポケモン名の一覧はWikipediaの全国ポケモン図鑑順のポケモン一覧ページを元データとします。
ChromeのDevToolを開き,コンソールで以下のコマンドを実行することで、1行1ポケモン名の文字列を得ることができます。コピーしてあらかじめ適当なファイル名とヘッダを付けて、csv形式として保存しておきます。(後ろの方は関係ない文字列が含まれてしまうので、890行目のムゲンダイナ以降は省いてください)pokemon_list.csvName フシギダネ フシギソウ フシギバナ ...アプローチ
さて、いろは歌への意気込みはあるものの、atcoder灰色の私には、具体的な実装方針がまるで思いつきません。
そこで、ポケモンでいろは歌を作りたいという熱い気持ちを弊社チャットにぶつけてみたところ、競技プログラミングのガチ勢から執行役員まで、いろいろな方にアドバイスを頂けました。なんてすばらしい職場なのでしょう。いろは歌を愛してやまない就活生の方、転職をお考えの方がいらっしゃれば弊社がおすすめです。
話がそれましたが、頂いたアドバイスの一つとして先行研究の論文の存在を教えていただいたので、この論文のアルゴリズムをベースに以下の方針で実装することにしました。
- 文字ごとの出現頻度をカウントし、頻度が少ない順に並べた木に対して深さ優先探索を行う
- かな重複はビット演算で検出
- コアスケールさせて高速化
- ノードごとの計算結果をキャッシュ
次節以降ではこれらについて具体的に解説していきます。
コアスケール時の実装容易性を考慮し、言語はGoを選択しています。
なお、この記事で紹介したアルゴリズムは、irohaというCLIとしてGitHubに公開しています。
より詳細な実装が知りたい方はこちらもご覧ください。文字ごとの出現頻度で深さ優先探索(DFS: Depth First Search)
皆さんはポケモン名で一番使われているカタカナが何か、考えたことはありますか?それは「ン」です。
全890匹中192匹が「ン」を含むポケモンであり、全体の21.5%に相当します。以下「ル」「ト」「ス」と続きます。
逆に一番使われていない文字は「ヌ」で、この文字を持つポケモンは10匹しかいません。
そのへんの伝説ポケモンよりヌメルゴンの方がよっぽど珍しいことが分かっていただけるかと思います。iroha CLIでは
analyze
サブコマンドとして文字の出現頻度を出力できます。出力が長いのでfoldしておきますが、どのカナがどれぐらい使われているのか知りたい方は以下をクリックしてみてください。
カナごとの出現頻度
$ iroha analyze --file pokemon_list.csv ヌ: 10 セ: 16 ソ: 16 ヘ: 22 ネ: 23 ヨ: 23 ワ: 24 ミ: 25 モ: 34 ノ: 35 ケ: 36 エ: 38 メ: 38 サ: 40 ウ: 43 ナ: 44 レ: 44 ム: 46 ユ: 46 ニ: 47 ホ: 49 ヒ: 52 チ: 55 テ: 58 ヤ: 58 オ: 60 ロ: 66 ア: 67 キ: 74 マ: 78 タ: 80 リ: 92 カ: 93 コ: 99 ハ: 104 イ: 105 ツ: 106 フ: 107 ラ: 110 シ: 113 ク: 121 ス: 126 ト: 137 ル: 147 ン: 192次に、各ポケモンについて、もっとも出現頻度が低い文字でグルーピングします。例えばヌメルゴンという名前に含むカナ5文字「ヌ」「メ」「ル」「コ」「ン」のうち、最も出現頻度が低いカナは「ヌ」なので、「ヌ」グループとします。
iroha CLIのanalyze
コマンドで、グルーピング結果を出力できるので、こちらも興味がある方は以下をクリックしてみてください。
カナごとのグルーピング
$ iroha analyze --file pokemon_list.csv ... ヌ: ヌオー,クヌギダマ,ヌマクロー,ヌケニン,ヌメラ,ヌメイル,ヌメルゴン,アシレーヌ,ヌイコグマ,タイプ:ヌル, セ: ゼニガメ,トランセル,パラセクト,ハリーセン,セレビィ,イルミーゼ,ロゼリア,ブイゼル,フローゼル,クレセリア,アルセウス,ゼブライカ,ゼクロム,ゲノセクト,ゼルネアス,ザマゼンタ, ソ: フシギソウ,ナゾノクサ,ニョロゾ,ウソッキー,ソーナンス,ゴマゾウ,ケムッソ,ソルロック,アブソル,ソーナノ,ウソハチ,ゾロア,ゾロアーク,コソクムシ,ソルガレオ,メッソン, ヘ: ペルシアン,ベロリンガ,ベイリーフ,ヘラクロス,ヘルガー,ペリッパー,ヘイガニ,ジュペッタ,タツベイ,エンペルト,ペラップ,ゴンベ,ペンドラー,オーベム,ツンベアー,ジヘッド,ペロッパフ,ペロリーム,クレベース,スナ ヘビ,ベロバー,モルペコ, ネ: フシギダネ,カモネギ,ハネッコ,ハガネール,タネボー,エネコ,バネブー,サボネア,ハブネーク,ネンドール, ネオラント,フィオネ,チョロネコ,タブンネ,チュリネ,トルネロス,カラマネロ,ネマシュ,ネッコアラ,ネクロズマ,クスネ,ネギガナイト, ヨ: ピジョン,ピジョット,ニョロモ,ニョロボン,ヨルノズク,ニョロトノ,ヨーギラス,ドジョッチ,ヨマワル,サマヨール,ヨノワール,ヨーテリー,チョボマキ,マッギョ,コジョフー,コジョンド,ヨワシ,アマージョ,アーゴヨン, ヨクバリス,カマスジョー, ワ: ワンリキー,イワーク,サワムラー,エビワラー,シャワーズ,ワニノコ,ワタッコ,キマワリ,ワカシャモ,ポワルン,ユキワラシ,フワンテ,フワライド,イワパレス,スワンナ,ワシボン,クワガノン,イワンコ,ワタシラガ,ワンパ チ,パルスワン, ミ: マダツボミ,スターミー,ミニリュウ,ミュウツー,ミュウ,ヤミカラス,ミルタンク,ミズゴロウ,スボミー,ミノムッチ,ミノマダム,ミツハニー,ドーミラー,ミカルゲ,シェイミ,ミジュマル,ミルホッグ,チラーミィ,ゴチミル, トリミアン,カミツルギ,ミブリム,マホミル,ユキハミ, モ: モルフォン,モンジャラ,メタモン,キモリ,アチャモ,バシャーモ,キャモメ,アメモース,ヤルキモノ,コモルー,モウカザル,モジャンボ,コロモリ,モグリュー,エモンガ,カブルモ,モロバレル,ヒトモシ,モノズ,ウルガモス,クズモー,モクロー,シズクモ,オニシズクモ,ヤトウモリ,コスモッグ,コスモウム,デンジュモク,ギモー,モスノウ, ノ: ニドリーノ,メノクラゲ,ヒノアラシ,ノコッチ,イノムー,コノハナ,キノガッサ,マクノシタ,マルノーム,ノクタス,アノプス,ユキノオー,ダイノーズ,ユキメノコ,アグノム,ジャノビー,オノンド,ヒノヤコマ,ガメノデス,メ テノ,サルノリ,ウオノラゴン, ケ: ヒトカゲ,ケーシィ,ユンゲラー,ゲンガー,ケンタロス,アリゲイツ,トゲピー,トゲチック,アゲハント,ドクケイル,ナマケロ,ケッキング,カゲボウズ,ケイコウオ,トゲキッス,ダイケンキ,ケンホロウ,ナゲキ,ダゲキ,アーケ ン,アーケオス,ケルディオ,ケロマツ,ゲコガシラ,ゲッコウガ,バケッチャ,ボルケニオン,マケンカニ,トゲデマル,オーロンゲ,ムゲンダイナ, エ: シェルダー,パルシェン,エレブー,エイパム,エーフィ,エアームド,カポエラー,エレキッド,エンテイ,ポチエナ,グラエナ,ホエルコ,ホエルオー,ナエトル,チェリンボ,チェリム,エテボース,エレキブル,エルレイド,エムリ ット,エンブオー,エルフーン,メロエッタ,フラエッテ,フラージェス,メェークル,エリキテル,エレザード,マシェード,エンニュート,エースバーン,エレズン,イエッサン, メ: カメール,カメックス,メガニウム,メリープ,ヒメグマ,スバメ,アメタマ,サメハダー,ドンメル,メタング,メ タグロス,ハヤシガメ,ガーメイル,メガヤンマ,マメパト,メグロコ,メブキジカ,メラルバ,メレシー,バクガメス, メルタン,ジメレオン,ヒメンカ,ドラメシャ, サ: リザード,リザードン,サンド,クサイハナ,オコリザル,サイホーン,サイドン,サンダース,フリーザー,サンダー,ハッサム,サニーゴ,サナギラス,カラサリス,サーナイト,アサナン,ザングース,シザリガー,タマザラシ,サク ラビス,レックウザ,ヒコザル,ゴウカザル,サッチムシ,サダイジャ,サシカマス,タチフサグマ,サニゴーン,ザシアン, ウ: ピカチュウ,ライチュウ,キュウコン,ウツドン,アズマオウ,デンリュウ,ウパー,ムウマ,ウリムー,テッポウオ,ライコウ,トロピウス,ムウマージ,ドリュウズ,ウォーグル,シルヴァディ,ウツロイド,ウールー,バイウールー, バチンウニ,ウオチルドン, ナ: フシギバナ,ニドリーナ,ナッシー,オムナイト,キレイハナ,ハピナス,マイナン,ナックラー,ルナトーン,ナマズン,カラナクシ,ギラティナ,マナフィ,ヤナップ,ヤナッキー,ムンナ,ムシャーナ,ナットレイ,コマタナ,バルジ ーナ,テールナー,ジュナイパー,スナバァ,シロデスナ,ナマコブシ,ルナアーラ,マギアナ, レ: ラフレシア,レアコイル,レディバ,レディアン,フォレトス,ハスブレロ,チャーレム,ユレイドル,カクレオン,レジロック,レジアイス,レジスチル,レントラー,ロズレイド,グレイシア,レジギガス,レパルダス,ドレディア,リグレー,レシラム,キュレム,クレッフィ,ボクレー,ヤレユータン,レドームシ,タイレーツ, ム: オムスター,ムチュール,ドゴーム,ムックル,ムクバード,ラムパルド,マンムー,ロトム,ムーランド,ゴチム, クリムガン,コフキムシ,デンヂムシ,テブリム,ブリムオン, ユ: ジュゴン,ルージュラ,ハクリュー,カイリュー,ピチュー,ニューラ,ジュプトル,ジュカイン,マユルド,ユキカブリ,マニューラ,ユクシー,クルマユ,ユニラン,シュバルゴ,バチュル,デンチュラ,クマシュン,カジッチュ,アッ プリュー,ジュラルドン, ニ: オニドリル,ニドラン♀,ニドクイン,ニドラン♂,ニドキング,ニャース,ゴローニャ,ポニータ,ツチニン,テッカニン,キバニア,オニゴーリ,ニャルマー,ブニャット,ビクティニ,バニプッチ,バニリッチ,ニャスパー,ニダンギル,ニンフィア,ニャビー,ニャヒート,ヒバニー,ニャイキング, ホ: アーボ,アーボック,ポリゴン,ポリゴン2,ハスボー,ボスゴドラ,ライボルト,ボーマンダ,ポッチャマ,ポッタ イシ,コロボーシ,ポリゴンZ,ポカブ,ハトーボー,ホイーガ,シンボラー,ボルトロス,ハリボーグ,ホルビー,ホルード,アブリボン,ホシガリス,ポットデス,マホイップ,コオリッポ, ヒ: キャタピー,ビードル,スピアー,ピクシー,ヒトデマン,カビゴン,ピィ,ブビィ,バルビート,ブーピッグ,ビブ ラーバ,ヒンバス,ビッパ,ビーダル,ビークイン,ピンプク,スコルピ,ドラピオン,ヒードラン,ヒヤップ,ヒヤッキ ー,コアルヒー,シビルドン,ゴビット,ビリジオン,ヒトツキ,ヒドイデ,ラビフット, チ: チコリータ,オタチ,クチート,パッチール,チルット,チルタリス,チリーン,ジラーチ,パチリス,チャオブー, フタチマル,マラカッチ,バルチャイ,チゴラス,ガチゴラス,カチコール,バチンキー,パッチラゴン,パッチルドン,ドロンチ, テ: ディグダ,ガーディ,フーディン,イシツブテ,プテラ,デリバード,ダーテング,ハリテヤマ,ハンテール,ラティアス,ラティオス,デオキシス,タテトプス,トリデプス,ディアルガ,ハーデリア,フシデ,デスカーン,テッシード, シャンデラ,テラキオン,ジガルデ,ディアンシー,ドデカバシ,デカグース,キテルグマ,テッカグヤ,ヤクデ,マルヤクデ,デスバーン, ヤ: ギャロップ,ヤドン,ヤドラン,バリヤード,ギャラドス,ファイヤー,ヤドキング,ヤジロン,リーシャン,ツタージャ,ジャローダ,ヤブクロン,オシャマリ,ヤングース,ジャラコ,ジャランゴ,マーシャドー, オ: ダグトリオ,オーダイル,オクタン,ルクシオ,リオル,ルカリオ,グライオン,バオップ,バオッキー,オタマロ, バスラオ,フリージオ,コバルオン,フォッコ,マフォクシー,オーロット,オンバット,アオガラス,イオルブ,フォクスライ,バリコオル, ロ: ロコン,ゴローン,カイロス,クロバット,コロトック,ダンゴロ,ローブシン,プロトーガ,バッフロン,ランドロス,ハリマロン,ブリガロン,ゴロンダ,ブロスター,フクスロー,ドロバンコ,トロッゴン, ア: ルギア,キルリア,アーマルド,ガブリアス,リーフィア,パルキア,ギガイアス,アバゴーラ,ギアル,アギルダー,アマルス,アシマリ,アブリー,アマカジ, キ: マンキー,ゴーリキー,カイリキー,キングラー,ラッキー,コイキング,ブラッキー,キングドラ,バルキー,バンギラス,マスキッパ,キバゴ, マ: マタドガス,イトマル,マリル,リングマ,マグカルゴ,フカマル,ダルマッカ,マーイーカ,マッシブーン, タ: バタフリー,コラッタ,ラッタ,コダック,ゴルダック,ブースター,バクーダ,コータス,ダンバル,スカタンク, ドータクン,ダークライ,ダブラン,クイタラン,ゴリランダー,ストリンダー, リ: プリン,プクリン,スリープ,スリーパー,ゴクリン,コリンク, カ: ドガース,ガルーラ,カブト,グライガー,ラブカス,ドンカラス,スカンプー,ガバイト,カバルドン,ガントル, ズガドーン, コ: コクーン,ゴルバット,コイル,ゴース,ゴースト,コドラ,フライゴン,ジバコイル,ドッコラー,ゴルーグ,コフ ーライ, ハ: ズバット,パラス,ブーバー,バクフーン,ブーバーン,フーパ, イ: ストライク,スイクン, ツ: ズルッグ, フ: クラブ,ブルー,グランブル,ドーブル,プラスル, ラ: シードラ,ラルトス,ジーランス,グラードン,ランクルス, シ: ク: ス: ト: ル: ン:このグループで以下のように木構造を作ります。
まずヌのグループからポケモンをひとつ取り出します。次にセからまたひとつ取り出します。文字がかぶったらそれ以降の探索を打ち切ります。例えば上の図では、ヌメルゴンとトランセルを選択した場合、「ル」が重複しているため、これ以降どうやってもいろは歌は完成しません。これを繰り返して、いろは歌が成立するポケモン集合を列挙します。
ビット演算によるカナ重複検出
前節で、カナが重複した場合は探索を打ち切ると述べました。ではこのカナ重複をどうやって実現すれば良いでしょうか。
Goで素直に実装する場合、以下のようなコードになると思います。func hasDuplicatedRune(word, otherWord string) bool { for _, r := range word { if strings.ContainsRune(otherWord, r) { return true } } return false }しかし、ビット演算を利用することでより高速な重複検出が実現できます。
まず、かな45文字それぞれについて1ビットを割り当てます。こうすると、各ポケモン名がどのカナを含んでいるかも(順序性は失われますが)45ビット列で表現できます。
例えば、ブーバーを表現するビット列は以下のようになります。このように表現されたポケモン名では、カナが重複しているかどうかは、両方で同じ位置のビットが立っているかどうかで判定できます。
例えばアが重複しているかどうかは、1ビット目が両方1になっているかどうかを見ればよいです。
そうすると、これを45ビット全体で判定するには、2つのビット列の論理積でビットが立っているかどうかを見ればよさそうです。これで高速にかな重複が検出できます。ではGoで実装してみましょう。
あるカタカナ一文字やポケモン名を表すのに45ビット必要なので、64ビットの符号なし整数型であるuint64を使うことにします。type WordBits uint64 // あるポケモン名を表すビット列カナの重複があるかどうかを返す関数は以下のようになります。
// 与えられたカナ集合のいずれかと重複するカナを持っているかどうかを返す func (w WordBits) HasDuplicatedKatakana(otherWordBits WordBits) bool { return w&otherWordBits != 0 }また、重複がなかった場合に、利用済みカナ一覧へ新たなポケモン名を追加するのも、論理和を計算するだけです。
Goでは以下のような実装になります。
// 現在のカナ集合と与えられたカナ集合の和集合を返す func (w WordBits) Merge(otherWordBits WordBits) WordBits { return w | otherWordBits }goroutineによる並列計算
ここまで、アルゴリズムレベルでの高速化を行ってきました。
いろは歌の探索には木構造を用いるので、各子ノードごとに並列計算することでさらに高速化できそうです。
Goにはgoroutineという軽量プロセスの仕組みが組み込まれているので簡単に並行処理を実装できます。
ノードごとにgoroutineを立ち上げてしまっても良いのですが、もう少し細かく制御するために、iroha CLIではmin-p-death(ノードごとに並行処理を行う、最小の木の深さ)
とmax-p-depth(ノードごとに並行処理を行う、最大の木の深さ)
というオプションを用意しています。深さは、最初のグループを0として数えます。
例えば、min-p-depth=1
、max-p-depth=5
とすると、深さ1(上の図では「セ」グループ)から深さ5までのグループは並列に、深さ0(上の図では「ヌ」グループ)と深さ6以降のグループでは直列に計算を行います。
これにより、ノード数に応じてgoroutineの数をある程度制御できます。ちなみに、goroutineスケジューラには実行の順序保証がないので、このような並行処理を実装すると深さ/幅優先探索は行えなくなります。今回は途中で計算を打ち切りつつ全探索を行うので問題になりませんが、いろは歌が一つでも見つかればいいケースでは、並列化しない方が早く探索が終わる可能性もあります。
ノードごとの計算結果キャッシュ
前節でコアスケールできるようになったので、コアを積めば積むほど高速化できるはずです。
手元のMacBook Proは4コアですが、例えばGCPでは最大96コアのマシンを利用できます。が、やはりそれなりのお値段になります。
ところで、GCPには通常のVMの他にプリエンプティブルVMというものがあります。これは、通常VMに比べて料金が大幅に安い代わりに、突然終了させられる可能性があるマシンタイプです。
そこで、計算が途中で中断されても続きから再開できる仕組みを実装して、プリエンプティブルVM上で実行できるようにします。基本的な戦略としては、選択したポケモンの組み合わせごとにいろは歌が何個完成したかをキャッシュしておきます。
例えば、ロコン+スリープを選んだ際に2個、ロコン+スリーパーで3個のいろは歌が生成できることが計算により分かったとします。
この結果を保存しておけば、次回以降このノードにたどり着いた時点でキャッシュをチェックし、存在すればそちらから結果を返すことにより計算を省略できます。
(実際には、最後に成立したいろは歌の内容を表示したいので、もう少し色々な情報をキャッシュしていますが、説明の簡略化のためにここでは割愛します)iroha CLIでは、このキャッシュの保存先を、無し/オンメモリ/組み込みDB(bbolt)/GCP CloudStorageの4種類から選択できます。
内部では以下のようなGetter/Setterを持つStorage
インターフェースが定義されており、実装を差し替えることができるようになっています。type Storage interface { Set(ctx context.Context, indices []int, wordsList [][]*ktkn.Word) error Get(ctx context.Context, indices []int) ([][]*ktkn.Word, bool, error) }プリエンプティブルVMを利用する場合は、GCP Cloud Storageをストレージとして選択することで、途中で落とされてもキャッシュを保存しておくことができます。
結果
それではいよいよポケモン名いろは歌を生成してみます。
$ iroha gen -f pokemon_list.csv ... 959 iroha-uta were found!というわけで、ソード&シールドまでのポケモン890匹で生成できるいろは歌は959パターンでした!
おまけ: いらすとや画像だけで「いろは歌」を作る
ここまで高速化について検討しておいてなんですが、実は890匹程度のポケモン数ではアルゴリズム改善までで、数秒で算出できてしまいます。今後ポケモンが5000匹ぐらいになるとコアスケールの恩恵を受けられるはずなのですが、一度確かめておきたいです。
そこで、ポケモンの代わりに「いらすとや」の画像タイトル5000件を、GCPのプリエンプティブVMを使って、CPU96コア/メモリ300GBで計算してみます。
なお、GCPでは24コア以上のCPUを同時に利用する場合は、上限解除の申請を出す必要があります。
対応には2営業日ほどかかると書かれていたのですが、「いろは歌を計算したいので96コア使わせてください」と申請したら、3時間半で対応していただけました。ありがとうございました。こちらは計算の様子です。CPUを使い切っていて気持ちが良いですね。
このマシンもまさか全力でいろは歌を計算させられることになるとは思っていなかったでしょう。
約3分半で、5000枚のいらすとや画像タイトルから2687パターンのいろは歌を生成できることが確認できました。
これでポケモン次回作で登場ポケモン数が爆発的に増えても安心ですね。
- 投稿日:2019-12-22T16:40:10+09:00
GoでランダムなGif画像を生成する
この記事はtomowarkar ひとりAdvent Calendar 2019の22日目の記事です。
はじめに
GoでのGIF画像生成を調べてもあまり出てこなかったので書いてみます。
本記事には関係ないですが、
image.Image
からimage.Paletted
に変換する方法って何かいい方法ありませんかね...考えかた
Int
のスライスを生成する- 数字に対応する色をマッピングする
- マッピングした画像を複数作り、GIFにする
Int スライス
マッピング
コード
package main import ( "image" "image/color" "image/gif" "math/rand" "os" ) func main() { var w, h, scale, delay = 8, 5, 100, 100 var items [][]int var palette = []color.Color{ color.RGBA{255, 0, 255, 255}, color.RGBA{0, 255, 255, 255}, color.RGBA{255, 255, 0, 255}, } for i := 0; i < 5; i++ { items = append(items, generateItem(w, h, len(palette))) } generateGif(w, h, scale, delay, items, "hoge", palette) } func generateItem(w, h, nums int) []int { var item []int = make([]int, w*h) for i := 0; i < w*h; i++ { item[i] = rand.Intn(nums) } return item } func generateGif(w, h, scale, delay int, items [][]int, filename string, palette []color.Color) { var images []*image.Paletted var delays []int for i := 0; i < len(items); i++ { img := image.NewPaletted(image.Rect(0, 0, w*scale, h*scale), palette) images = append(images, img) delays = append(delays, delay) for x := 0; x < w*scale; x++ { for y := 0; y < h*scale; y++ { img.Set(x, y, palette[items[i][y/scale*w+x/scale]%len(palette)]) } } } f, _ := os.OpenFile(filename+".gif", os.O_WRONLY|os.O_CREATE, 0600) defer f.Close() gif.EncodeAll(f, &gif.GIF{ Image: images, Delay: delays, }) }おわりに
カレンダーも残り数日ですね
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019参考
https://golang.org/pkg/image/gif/
https://gist.github.com/nitoyon/10108182cc0c12f54878
- 投稿日:2019-12-22T16:26:11+09:00
令和元年冬 犬は駆け廻り 猫は円くなり 私はCIに目を回す
0. 序文
本稿は TECOTEC Advent Calendar 2019 の21日目の記事です。
「エンジニアがすなる技術ブログといふものを、私もしてみむとしてするなり。はてなブログ、君に決めた!」
「そういえば、小説の原稿をGit管理すると、編集点や更新履歴が共有できて、校正・編集が捗るって聞いたことがあるよ」
「Gitを使うなら……投稿まで自動化しては……どうでしょう……捗りマックスですよ……」
「いいね!それ採用」
というわけでGitHub初心者が見果てぬ夢を追いかけて自動化の塔を目指すまでの物語です。
X. 本稿の内容を3行で
- ブログ記事をGit管理して校正・改稿を可視化したく
- Git使うなら投稿まで自動化できたらいいなとCircleCIに手を出し
- 必要なパッケージがインストールできずに頓挫した
今のところ先生にもなれないしくじり話です、ご容赦ください。
同じような躓き方をしてしまった時に、少しでもご参考になれば幸いです。1. 訓練
1-1. 調査
「まずは冒険に必要な道具を確認しましょう」
「『はてなブログ Git 管理』でググります」
このブログの内容を GitHub で管理するようにした - えいのうにっき
「あなたは【blogsync】という道具が有用であることを知りました。はてなブログの投稿記事を取得する、新しい記事を作成する、既にある記事を更新する――以上のことができます。Go言語で書かれた、AtomPubのラッパーのようです」
「グッド(Go触ったことないけど)。関連記事にもっと詳しい情報がないか調べます」
コード化したはてなブログリポジトリの更新を CircleCI 2.0 で自動化した - えいのうにっき
「あなたは【CircleCI】でblogsyncを動かすという方策を見つけました。」
「【blogsync】と【CircleCI】で自動化ね、完全に理解した(CircleCI使ったことないけど)」
1-2. 地図
「それでは、冒険の地図を作りましょう」
「ブログを管理するGitHubリポジトリを作って、それをCircleCIに登録して、CircleCIの中ではblogsyncを動かして……」
「blogsyncで《記事作成》と《記事更新》を行う場合、ファイル名を指定する必要があるようです」
「じゃあ、pull requestでのmasterブランチとの差分を取得して、それぞれのファイルについて
post
とpush
をする感じで……できた!」1-3. 準備
「まずは自分の家(ローカル)で【blogsync】を入手し、試してみたいと思います」
「よいでしょう。構築環境を決めるので、1d6を振ってください……はい、あなたの環境はWindowsなので、【Chocolatey】を導入するところからです」
Windows10でChocolateyを使ってみた - Qiita
「チョコ美味しいなり。で、次はGoのインストール……
$ choco install golang
を唱えます」
「では、2d6を振ってください」
「えっ、成功判定? こんなところで失敗することなんてあるの……?」コロコロ……
2→ 9-1.に進む
3以上→ 1-4.に進む1-4. 調達
「Go環境を導入して、【blogsync】を入手する準備が整いました」
「唱えます。
$ go get github.com/motemen/blogsync
」
「2d6 + 《Go知識》ボーナスで12以上で成功です」
「高っ!? ボーナス0なんですけど。インストールするだけですよね……?」
11以下→ 9-2.に進む
12→ 1-5.に進む1-5. 実践
「【blogsync】を手に入れました」
「やったね!ではGitリポジトリを作成してルートを決め、configファイルを作ります」
「成功です」
「ここはあっさりなんですね」
.config/blogsync/config.yaml[はてなブログのURL]: username: [はてなアカウントのID] password: [はてなブログの"詳細設定"の"AtomPub"にあるAPIキー] local_root: [Gitリポジトリをcloneしたディレクトリ] omit_domain: [local_rootの階層を深くしたくなければtrue]
「早速サンプルで用意したはてなブログを引っ張ってみます。
$ blogsync pull [はてなブログのURL]
」PS C:\Users\***\go\src\github.com\motemen\blogsync > .\blogsync pull [ブログURL] GET ---> https://blog.hatena.ne.jp/[はてなID]/[ブログURL]/atom/entry 200 <--- https://blog.hatena.ne.jp/[はてなID]/[ブログURL]/atom/entry fresh remote=2019-12-21 00:00:00 +0900 JST > local=0001-01-01 00:00:00 +0000 UTC store [local_rootパス]\entry\2019\12\20\000000.md
「成功です。12/20に作成していた下書き状態の記事を.md形式で取得することができました」
2. 円環
2-1. 導入
「では、自動化の塔に挑みましょう」
「展開が早い……いや実際にはここまでだいぶ死に戻りを繰り返した気がするけれども……。まぁローカルで成功したことですし、同じようにすればできるでしょ。【CircleCI】をGitHubアカウントで利用開始、リポジトリを登録します」
「成功です」
「さすがにそれはね……。では、調査で獲得した資料を参照して、
.circleci/config.yaml
を作成し、masterブランチにpull requestを送ります」
「.yamlファイルの作成でよいですね?」
YES→ 9-4.に進む
NO→ 2-2.に進む2-2. 調整
「間違えました、説明書をよく読むと.ymlと書いてあるので、
.circleci/config.yml
を作成します。記述も先の資料をお手本にしますが、ブログ名称などもCircleCIの環境変数で定義するようにします。また、blogsyncを入れた後は、とりあえず$ blogsync pull
が動くか試してみようと思います」.circleci/config.ymlversion: 2 jobs: build: environment: - GOPATH: /home/circleci/go docker: - image: circleci/golang:1.12 working_directory: /go/src/github.com/[リポジトリのパス] steps: # ここから https://blog.a-know.me/entry/2018/03/04/215345 の記述をそのまま拝借します # GOPATH の設定。environment だけじゃ無理っぽい? https://qiita.com/tomiyan/items/6142113011243c5b5cd1 - run: echo 'export PATH=${GOPATH}/bin/:${PATH}' >> $BASH_ENV # blogsync 用の config ファイルを置く場所の作成 - run: mkdir -p ~/.config/blogsync # blogsync 用の config ファイルの書き出し。コロンをエスケープするために使ったダブルクォートを tr コマンドで無理矢理削除している - run: echo -e "${HATEBLO_URL}\":\"\n username\":\" ${HATEBLO_USERNAME}\n password\":\" ${HATEBLO_PASSWORD}\n local_root\":\" /go/src/github.com/[リポジトリのパス]\n omit_domain\":\" true" | tr -d \" >> ~/.config/blogsync/config.yaml # ここまで # go get github.com/motemen/blogsync がパッケージ依存の関係でエラーになる - run: git clone https://github.com/motemen/blogsync.git ${GOPATH}/src/github.com/motemen/blogsync - run: cd ${GOPATH}/src/github.com/motemen/blogsync && go build -o blogsync # リポジトリからチェックアウト - checkout - run: name: pull blog entries command: | cd ${GOPATH}/src/github.com/motemen/blogsync && ./blogsync pull ${HATEBLO_URL}9-3.に進む
2-3. 死闘
「……importしているパッケージをすべて
$ go get
すれば……いいんですかね……?」.circleci/config.ymlversion: 2 jobs: build: environment: - GOPATH: /home/circleci/go docker: - image: circleci/golang:1.12 working_directory: /go/src/github.com/[リポジトリのパス] steps: # ここから https://blog.a-know.me/entry/2018/03/04/215345 の記述をそのまま拝借します # GOPATH の設定。environment だけじゃ無理っぽい? https://qiita.com/tomiyan/items/6142113011243c5b5cd1 - run: echo 'export PATH=${GOPATH}/bin/:${PATH}' >> $BASH_ENV # blogsync 用の config ファイルを置く場所の作成 - run: mkdir -p ~/.config/blogsync # blogsync 用の config ファイルの書き出し。コロンをエスケープするために使ったダブルクォートを tr コマンドで無理矢理削除している - run: echo -e "${HATEBLO_URL}\":\"\n username\":\" ${HATEBLO_USERNAME}\n password\":\" ${HATEBLO_PASSWORD}\n local_root\":\" /go/src/github.com/[リポジトリのパス]\n omit_domain\":\" true" | tr -d \" >> ~/.config/blogsync/config.yaml # ここまで # go get github.com/motemen/blogsync がパッケージ依存の関係でエラーになるので、パッケージを個別に入れてからbuildする - run: go get gopkg.in/yaml.v2 - run: go get github.com/motemen/go-colorine - run: go get github.com/motemen/go-loghttp - run: go get github.com/motemen/go-wsse - run: go get github.com/cpuguy83/go-md2man - run: go get github.com/urfave/cli - run: git clone https://github.com/motemen/blogsync.git ${GOPATH}/src/github.com/motemen/blogsync - run: cd ${GOPATH}/src/github.com/motemen/blogsync && go build -o blogsync # リポジトリからチェックアウト - checkout - run: name: pull blog entries command: | cd ${GOPATH}/src/github.com/motemen/blogsync && ./blogsync pull ${HATEBLO_URL}9-5.に進む
9. 道場
9-1. Chocolateyを使ったインストールに失敗する
「
ファンブル
です」
【現象】zipパッケージのダウンロードがタイムアウトになる
【再現率】環境による(自宅PCでは100%、別PCでは発生せず)
【原因】不明
【解決策】chocolatey.configに記載の、パッケージソースの参照場所を書き換える(https://
をhttp://
に変更する)Can't get Chocolatey to download files - Stack Overflow
chocolatey.configは
C:\ProgramData\chocolatey\config
にあるが、保護(上書き禁止)になっていることもあり、PowerShellからviで開いて編集したいところ。
Vimが入っていない場合はVimを入れるところから始めましょう。解決したら1-3.に戻ります。
9-2. blogsyncのインストールに失敗する(cli)
「失敗です」
C:\Users\***\go\src\github.com\motemen\blogsync\main.go:17:15: cannot use []cli.Command literal (type []cli.Command) as type []*cli.Command in assignment C:\Users\***\go\src\github.com\motemen\blogsync\main.go:157:15: cannot use cli.BoolFlag literal (type cli.BoolFlag) as type cli.Flag in array or slice literal: cli.BoolFlag does not implement cli.Flag (Apply method has pointer receiver) C:\Users\***\go\src\github.com\motemen\blogsync\main.go:158:17: cannot use cli.StringFlag literal (type cli.StringFlag) as type cli.Flag in array or slice literal: cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver) C:\Users\***\go\src\github.com\motemen\blogsync\main.go:159:17: cannot use cli.StringFlag literal (type cli.StringFlag) as type cli.Flag in array or slice literal: cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver)【現象】コンパイルに失敗してblogsyncをバイナリ化できない
【再現率】100%(2019/12/21現在。おそらく2019年10月以前なら0%)
【原因】main.goでimportしているCLI用パッケージ(urfave/cli)にバージョン互換性がない(v1→v2で破壊的な変化をしている)
【解決策】$ go get
ではなく、対象のリポジトリを$ git clone
して、それから$ go build
するWindowsの場合は、
$ go build -o blogsync.exe
と、実行ファイルであることを明示して作成する必要があります。
blogsyncのパッケージ化に成功したら、1-5.に進みます。9-3. blogsyncのインストールに失敗する(パッケージ不足)
「失敗です」
「わけがわからないよ……」
$ #!/bin/bash -eo pipefail go build -o blogsync blogsync/broker.go:10:2: cannot find package "github.com/motemen/blogsync/atom" in any of: /usr/local/go/src/github.com/motemen/blogsync/atom (from $GOROOT) /home/circleci/go/src/github.com/motemen/blogsync/atom (from $GOPATH) blogsync/log.go:6:2: cannot find package "github.com/motemen/go-colorine" in any of: /usr/local/go/src/github.com/motemen/go-colorine (from $GOROOT) /home/circleci/go/src/github.com/motemen/go-colorine (from $GOPATH) blogsync/loghttp.go:7:2: cannot find package "github.com/motemen/go-loghttp" in any of: /usr/local/go/src/github.com/motemen/go-loghttp (from $GOROOT) /home/circleci/go/src/github.com/motemen/go-loghttp (from $GOPATH) blogsync/loghttp.go:8:2: cannot find package "github.com/motemen/go-loghttp/global" in any of: /usr/local/go/src/github.com/motemen/go-loghttp/global (from $GOROOT) /home/circleci/go/src/github.com/motemen/go-loghttp/global (from $GOPATH) blogsync/broker.go:11:2: cannot find package "github.com/motemen/go-wsse" in any of: /usr/local/go/src/github.com/motemen/go-wsse (from $GOROOT) /home/circleci/go/src/github.com/motemen/go-wsse (from $GOPATH) blogsync/main.go:10:2: cannot find package "github.com/urfave/cli" in any of: /usr/local/go/src/github.com/urfave/cli (from $GOROOT) /home/circleci/go/src/github.com/urfave/cli (from $GOPATH) blogsync/config.go:7:2: cannot find package "gopkg.in/yaml.v2" in any of: /usr/local/go/src/gopkg.in/yaml.v2 (from $GOROOT) /home/circleci/go/src/gopkg.in/yaml.v2 (from $GOPATH) Exited with code exit status 1【現象】パッケージ不足でblogsyncをバイナリ化できない
【再現率】$ go get
で導入しなかった場合ほぼ100%
【原因】main.goでimportしているパッケージが実行環境に存在しない
【解決策】足りないと言われているパッケージを個別にインストールする
$ go get blogsync
ができればこうはならないはずですが……それぞれについて$go get
するようにconfig.yml
を修正するべく、2-3.に進みます。9-4. CircleCIのconfigファイルが読み込めない
「失敗です」
$ #!/bin/sh -eo pipefail # No configuration was found in your project. Please refer to https://circleci.com/docs/2.0/ to get started with your configuration. # # ------- # Warning: This configuration was auto-generated to show you the message above. # Don't rerun this job. Rerunning will have no effect. false Exited with code exit status 1【現象】CircleCIがconfigファイルを検知できずテストができない
【再現率】100%
【原因】configファイルの拡張子が異なる
【解決策】config.yamlではなくconfig .yml を作るDoes not take
.circleci/config.yaml
file for node project - CircleCIドキュメント通りに用意しましょう。
確認したら、2-2.に進みます。9-5. blogsyncのインストールに失敗する(cli-CircleCI)
「失敗です」
「んに"ゃあああああああ! お手上げです _(:3 」∠ )_ 」
$ #!/bin/bash -eo pipefail cd ${GOPATH}/src/github.com/motemen/blogsync && go build -o blogsync # github.com/motemen/blogsync ./main.go:17:15: cannot use []cli.Command literal (type []cli.Command) as type []*cli.Command in assignment ./main.go:157:15: cannot use cli.BoolFlag literal (type cli.BoolFlag) as type cli.Flag in array or slice literal: cli.BoolFlag does not implement cli.Flag (Apply method has pointer receiver) ./main.go:158:17: cannot use cli.StringFlag literal (type cli.StringFlag) as type cli.Flag in array or slice literal: cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver) ./main.go:159:17: cannot use cli.StringFlag literal (type cli.StringFlag) as type cli.Flag in array or slice literal: cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver) Exited with code exit status 2【現象】コンパイルに失敗してblogsyncをバイナリ化できない
【再現率】100%(2019/12/21現在。おそらく2019年10月以前なら0%)
【原因】main.goでimportしているCLI用パッケージ(urfave/cli)にバージョン互換性がない(v1→v2で破壊的な変化をしている)
【解決策】ワカリマセンもうblogsyncにissue記し奉るしか手がないと思われます……。
四方八方試行錯誤も堂々巡りで万策尽きて、終幕に進みます。Z. おお ○○○○よ
「しんでしまうとは なさけない…。」
「GoとCircleCIの知識を経験点として、次のキャンペーンに進んでいいですか……」
- 投稿日:2019-12-22T15:22:42+09:00
Go を一緒に学んでいるときに質問されやすいところ3セット
こんにちは。
Women Who Go Tokyo の micchie と申します。
Go4 Advent Calendar 2019 の22日目の記事を担当します。Women Who Go Tokyo は、毎月1回 Go についての勉強会を行っています。
Go にはじめて触れる方にも安心して楽しく参加していただけるよう、少しずつ改良を重ねながら運営しています。最近は「Go のことを学びながらやっていたら、いつのまにか家計簿アプリができていた!すごい!」を体験していただけるように「家計簿アプリを作ろう!」というタイトルやっています。
教材を提供してくださっている @tenntenn さん、いつもありがとうございます。今日のアドベントカレンダーでは、質問をもらうことが多い箇所について紹介したいと思います。
今後の勉強会の助けになりますように。説明が怪しいところは是非、正していただけますと幸いです。
※ ちなみに、すでに大変良い形でまとめてくださっている素敵なブログが、たくさんあります!
1. ポインタがわからない
ポインタとは、データ本体のアドレスを指すものです。
あまり良い例ではありませんが「上野動物園のクマ舎にいるマレーグマ」を想像してみてください。- データ本体: マレーグマ - アドレス: 上野動物園のクマ舎bear という変数に “マレーグマ” を格納します。
var bear string bear = "マレーグマ"bear は “マレーグマ” の本体です。
fmt.Printf("%s\n", bear) fmt.Printf("%p\n", &bear)上記を実行すると、データ本体とそのアドレスを出力できます。
bearのデータ本体: マレーグマ bearのアドレス: 0x40c138
0x40c138
が「上野動物園のクマ舎」を表す「アドレス」です。
&
をつかってデータ本体からそのアドレスを取り出しています。次に、b に bear を代入します。
b := bear代入は値のコピーです。
fmt.Printf("bのデータ本体: %s\n", b) fmt.Printf("bのアドレス: %p\n", &b)上記を実行すると、
bのデータ本体: マレーグマ bのアドレス: 0x40c150このようになります。
データ本体は同じに見えますが、アドレスが bear と b は異なります。
マレーグマが0x40c150
という「上野動物園のクマ舎 (2)」にコピーされたということになります。(しかしクマがコピーされるなんて…物理的にはありえない…)今度は p というポインタ変数を作って、bear のポインタを代入します。
var p *string p = &bear
*string
は string のポインタ型です。fmt.Printf("pのデータ本体: %s\n", *p) fmt.Printf("pのアドレス: %p\n", p)上記を実行すると、
pのデータ本体: マレーグマ pのアドレス: 0x40c138このようになります。
ポインタ変数に*
を付与することで、そのデータ本体を取り出すことができます。
また、bear のアドレスを代入しているので、同じアドレスとなります。2. 配列とスライスがむずかしい
Go には配列とスライスがあります。この時点で「スライスとは?」みたいな気持ちになる人も少なくはないようです。
配列を作る
配列は、同じ型のデータを集めたもの、スライスは配列の一部となります。
まずは、長さが 3 の string 型の配列を作成します。
array := [3]string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"}配列は、長さも含めて一つの型となります。
そのため、要素を追加したい場合は、その長さにあった配列を作り直さなければなりません。array[0] = "メガネグマ" array[1] = "メガネグマ" array[2] = "メガネグマ"このように値の書き換えはできますが、長さを超えると、
array[3] = "メガネグマ"エラーとなります。
invalid array index 3 (out of bounds for 3-element array)スライスを作る
スライスを作ります。
slice := []string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"}一見、上の配列と同じように見えますが、実態は下記のようになっています。
1. 長さが 3 の string 型の配列が作られる。 2. 1. を参照したスライスが作られる。このようにスライスを作ることもできます。
このmake
の段階では、各要素はスライスの型のゼロ値 (この場合は string なので空文字) が格納されます。slice := make([]string, 3) slice[0] = "シロクマ" slice[1] = "ツキノワグマ" slice[2] = "アメリカクロクマ"スライスから取り出す
スライスの値は範囲を指定して取り出すことができます。
// slice全体: [シロクマ ツキノワグマ アメリカクロクマ] fmt.Printf("slice全体: %v\n", slice) // 最初〜最後まで: [シロクマ ツキノワグマ アメリカクロクマ] fmt.Printf("最初〜最後まで: %v\n", slice[:]) // インデックス0〜最後まで: [シロクマ ツキノワグマ アメリカクロクマ] fmt.Printf("インデックス0〜最後まで: %v\n", slice[0:]) // インデックス1〜最後まで: [ツキノワグマ アメリカクロクマ] fmt.Printf("インデックス1〜最後まで: %v\n", slice[1:]) // インデックス2〜最後まで: [アメリカクロクマ] fmt.Printf("インデックス2〜最後まで: %v\n", slice[2:]) // 最初〜インデックス1-1=0まで: [シロクマ] fmt.Printf("最初〜インデックス1-1=0まで: %v\n", slice[:1]) // インデックス1〜インデックス1-1=0まで: [] fmt.Printf("インデックス1〜インデックス1-1=0まで: %v\n", slice[1:1]) // インデックス1〜インデックス2-1=1まで: [ツキノワグマ] fmt.Printf("インデックス1〜インデックス2-1=1まで: %v\n", slice[1:2]) // インデックス1〜インデックス3-1=2まで: [ツキノワグマ アメリカクロクマ] fmt.Printf("インデックス1〜インデックス3-1=2まで: %v\n", slice[1:3]) // panic: runtime error: slice bounds out of range [5:3] fmt.Printf("インデックス5〜最後まで: %v\n", slice[5:])スライスに追加する
スライスは値の追加ができますが、長さ以上の要素を直接入れようとすると、エラーになります。
slice[3] = "メガネグマ" . . . panic: runtime error: index out of range [3] with length 3しかしスライスは、配列と違って長さを持たないため、
append
を利用して要素の追加ができます。slice = append(slice, "メガネグマ")スライスの cap と len
スライスには
cap
という容量を表すものと、len
という長さを表すものがあります。
下記は、cap と len を指定しながらスライスを作った例です。slice := make([]string, 3, 3) fmt.Println(slice, len(slice), cap(slice))1. 長さが 3 の string 型の配列が作られる。 2. 1. を参照したスライスが作られる。このときの 1. のサイズが
cap
、配列に入っている要素の数がlen
です。
要素は string のゼロ値が入っています。[ ] 3 3ここで、要素をいくつか足します。
slice = append(slice, "アメリカクロクマ") fmt.Println(slice, len(slice), cap(slice)) slice = append(slice, "ツキノワグマ") fmt.Println(slice, len(slice), cap(slice)) slice = append(slice, "ホッキョクグマ") fmt.Println(slice, len(slice), cap(slice)) slice = append(slice, "マレーグマ") fmt.Println(slice, len(slice), cap(slice))最初に3つ、string のゼロ値である空文字が格納されたあとに 1 つ追加したため、
le
n は 4 です。[ アメリカクロクマ] 4 6 [ アメリカクロクマ ツキノワグマ] 5 6 [ アメリカクロクマ ツキノワグマ ホッキョクグマ] 6 6 [ アメリカクロクマ ツキノワグマ ホッキョクグマ マレーグマ] 7 12
cap
に注目してください。
append
を行ったときに、cap
よりも大きくなってしまう場合cap
のメモリを拡張するのですが、拡張前のcap
の倍を確保しに行こうとしています。- アメリカクロクマを追加する前は、cap が 3 で、 6 に拡張。 - マレーグマを追加する前は、cap が 6 で、12 に拡張。ある程度の値が増えることがわかっている場合は、最初からスライスの
cap
を指定することで、メモリの効率を考慮した書き方ができます。3. Range が期待をうらぎる
Go ではスライスのループを利用することがよくあります。
たとえば、下記のようなスライスを作成して、順番にクマの名前とアドレスを表示するプログラムを書きます。bears := []string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"} for _, bear := range bears { fmt.Printf("range bearのデータ本体: %s\n", bear) fmt.Printf("range bearのアドレス: %p\n\n", &bear) }上記を実行すると、
range bearのデータ本体: シロクマ range bearのアドレス: 0x40c138 range bearのデータ本体: ツキノワグマ range bearのアドレス: 0x40c138 range bearのデータ本体: アメリカクロクマ range bearのアドレス: 0x40c138このようになります。何か気づきませんか…?
同じアドレスです。値のコピーは作成されないのです。これに気づかない状態で、サクッと並列化します。
for _, bear := range bears { go func() { fmt.Printf("bearのデータ本体: %s\n", bear) fmt.Printf("bearのアドレス: %p\n\n", &bear) }() } time.Sleep(1 * time.Second)上記を実行すると、
./prog.go:12:47: loop variable bear captured by func literal ./prog.go:13:47: loop variable bear captured by func literal Go vet exited. bearのデータ本体: アメリカクロクマ bearのアドレス: 0x40c138 bearのデータ本体: アメリカクロクマ bearのアドレス: 0x40c138 bearのデータ本体: アメリカクロクマ bearのアドレス: 0x40c138期待した結果になりますでしょうか。期待から外れています。
ループ変数である bear は各ループで変更されるため、goroutine は bears の最後の値を利用します。これを回避するには、下記のようにパラメータで渡したり、値をコピーすると良いでしょう。
for _, bear := range bears { go func(bear string) { fmt.Printf("bearのデータ本体: %s\n", bear) fmt.Printf("bearのアドレス: %p\n\n", &bear) }(bear) }for _, bear := range bears { bear := bear go func() { fmt.Printf("bearのデータ本体: %s\n", bear) fmt.Printf("bearのアドレス: %p\n\n", &bear) }() }最後に
毎月開催されている Women Who Go Tokyo ですが、色んな人に Go を楽しんでもらいたいと思っています。
こちらの記事 Women Who Go Tokyo という取り組みでわたしたちは。|micchie|note でも紹介しているので、興味のある方はぜひ、覗いてみてください。※ 本当はレシーバの話も書きたかった…間に合わなかった…
- 投稿日:2019-12-22T15:16:40+09:00
Dockerのマルチステージビルドで、ビルド環境と実行環境のセットアップを1つのDockerfileで完結させよう
Code Chrysalis Advent Calendar 2019、24日目の投稿です。
こんにちは。現在、CodeChrysalisのイマーシブブートキャンプ(Cohort 10)に参加中のなおとです。
ブートキャンプも残すところ1週間を切りました。今は12/26(木)に行われるDemo Dayという卒業発表会に向けてチーム開発を日々頑張っています。お時間ある方は是非Demo Dayにご参加ください!
Demo Day の詳細はこちらになります。Code Chrysalisのブートキャンプには、多言語週間(Polyglottal Week)と呼ばれる、今まで使ったことのないプログラミング言語を1つ選択し、1週間で言語の習得からアプリケーション開発までを行うという機会があります。
私はこの多言語週間で、今まで扱ったことのなかったGolangを選択し、Golangを使ったフルスタックアプリケーションを作成しました。また、Dockerに興味があったので、Docker上でアプリをビルド・実行する方法も合わせて学びました。今回は、私が学んだ内容の一部についてご紹介したいと思います。
はじめに
本稿では、Dockerの基本的な知識があることを前提として、Dockerの重要な機能の1つであるマルチステージビルドについて解説していきます。
また、以下のレポジトリにサンプルコードを用意しました。今回説明する内容は、すべて以下のレポジトリをもとに検証を行っているので、ご自身の環境で試したい場合は合わせてご覧ください。
https://github.com/Imamachi-n/docker-multi-stage-build-101Docker上でアプリケーションをビルド・実行してみよう
まず始めに、作成したアプリケーションをDockerコンテナ上でビルド・実行したい場合、どのような方法を取ればいいのでしょうか?まずは簡単な例として、Golangで作成したアプリケーション(REST APIサーバ)を以下のDockerfileを用いて、ビルド・実行する場合を考えてみましょう。
以下に、サンプルのDockerfileを示しました。Dockerのベースイメージとして、公式の
golang
イメージを使っています。
ファイルの内容を簡単に説明すると、
1.ENV
でGolangのビルド条件(OS、CPUアーキテクチャ等)を環境変数として設定。
2.WORKDIR
で作業ディレクトリを指定。
3.COPY
で、ローカル環境にあるGolangプロジェクトをDockerコンテナ内にコピー。
4.RUN
でGolangのアプリケーションをビルドするコマンドを実行。
5.EXPOSE
で9000番のポートを開け、このポートを通してDockerコンテナと通信ができるように設定。
6. 最後に、ENTRYPOINT
でDockerコンテナ起動時に、Golangのアプリケーションが起動するように指定。
という内容が記述されています。FROM golang:1.13.4 ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 WORKDIR /docker-multi-stage-build-101 COPY . . RUN go build -o "bin/goServer" EXPOSE 9000 ENTRYPOINT ["/docker-multi-stage-build-101/bin/goServer"]Dockerfileから、Dockerイメージをビルドし実行すると、Golangで作成したREST APIサーバが立ち上がります。
$ docker build . -f docker/01_raw/Dockerfile -t go-server-raw:dev $ docker run --rm -p 9000:9000 go-server-raw:dev [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /api/user/:name/*action --> docker-multi-stage-build-101/route.GetAction (4 handlers) [GIN-debug] GET /api/welcome --> docker-multi-stage-build-101/route.GetWelcome (4 handlers) [GIN-debug] Listening and serving HTTP on :9000 2019/12/22 02:31:38 Defaulting to port :9000続いて、以下の通りに、
curl
コマンドを使ってDockerコンテナで立ち上げたREST APIサーバにアクセスしてみましょう。Hello Code Chrysalis
と表示されれば成功です。これで、Dockerコンテナ上でアプリケーションが起動していることが確認できました。$ curl -X GET 'http://localhost:9000/api/welcome?firstname=Code&lastname=Chrysalis' Hello Code Chrysalis次に、
docker images
コマンドを使って、Dockerイメージのサイズを見てみましょう。なんと、915MBというかなり大きなサイズになっていることがわかります。$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE go-server-raw dev eb8f35015c94 11 seconds ago 915MB
これは、アプリケーションの実行に不要なもの(ビルド時に使用したライブラリ等)が、そのまま同じDockerイメージ内にゴミとして残ってしまっているためです。AWSやGCPなどの環境にデプロイすることを考えると、できればDockerイメージのサイズを小さくしたいですね。
この問題を解決するために、マルチステージビルドが登場する以前は、Builderパターンと呼ばれるコンテナ管理方法が利用されていました。
マルチステージビルド以前: Builderパターン
Builderパターンでは、ビルド用とデプロイ用の2つのDockerfileを用意します。ビルド用のDockerコンテナでアプリケーションをビルドし、アプリケーションの実行に必要なものだけを実行用のコンテナにコピーします。
結果として、Builderパターンでアプリケーションが動作するDockerコンテナを用意した場合、以下のものが必要となります。
- 2つのDockerfile(ビルド用と実行用)
- (ビルド環境から実行環境への)ビルド成果物の受け渡し用のシェルスクリプト
これにより、上述したDockerイメージの肥大化を防ぐことができます。ただ、ちょっと考えてみてください。これってめんどくさくないですか?あと、複数のファイルに設定情報が散らばってしまっています。作成するDockerイメージが増えていった場合、ソースコードの管理が複雑化していく気がします…。Dockerマルチステージビルド
そこで、満を持してマルチステージビルドの登場です。この機能は、Docker 17.05から追加された機能になります。端的に言うと、Builderパターンで実践していた内容を1つのDockerfileにまとめることができます。
ビルドステージ(中間コンテナイメージ)
普段ベースイメージを指定するために、Dockerfile内に
FROM
命令を記述していると思います。マルチステージビルドでは、以下のように、1つのDockerfile内にFROM
命令を複数記述します。
前半のFROM
以下のブロックのことをビルドステージ(中間Dockerイメージ)と呼んでいます。この中間Dockerイメージは、実行用Dockerイメージにビルド成果物を渡した後、削除されます。そのため、最終的に生成される実行用のDockerイメージに含まれることはありません。ビルドステージの命名
続いて、
AS
を使うことで、ビルドステージに対して名前を付けることができます。下図の例は、ビルドステージの中間Dockerイメージに対してbuilder
という名前を指定しています。
ちなみに名前を指定しなかった場合、
FROM
命令の順番に合わせて0, 1, 2,...
という連番名が自動で振られます。例えば、上図の場合だと、ビルドステージは0
、実行用のDockerイメージは1
という連番名が振られます。ビルド成果物をビルド環境から実行環境のDockerイメージへコピー
ビルドステージで作成したビルド成果物を、後半の
FROM
以下のブロック(アプリケーション実行用のDockerイメージ)にコピーすることができます。方法としては、COPY
の--from
オプションでビルドステージ名を指定し、ビルド成果物をビルドステージから実行用のDockerイメージにコピーします。
このように、マルチステージビルドを使うことで、ビルド用と実行用のDockerfileを分けることなく、1つのDockerfileで記述することができるようになります。実はそれ以外にもメリットがあります。ビルド用と実行用にDockerベースイメージをそれぞれ指定することができるので、例えば、
alpine
などの軽量コンテナイメージを実行用のDockerベースイメージとして選択することができます。こうすることで、作成されたDockerイメージ内に、アプリケーションの実行に必要なものだけを配置することができ、コンテナのサイズをよりスリム化させることができます。マルチステージビルドの具体例
それでは、具体的な例として、最初にお見せしたGolangのアプリケーション(REST APIサーバ)をマルチステージビルドを使ったDockerfileに書き換えてみたいと思います。
以下がDockerfileの内容になります。先程とほとんど変わりませんが、
COPY --from=builder
でビルドステージ内のビルド成果物(/docker-multi-stage-build-101/bin/goServer
)を、実行用のDockerイメージに/goServer
としてコピーしています。# Builder image (intermediate container) FROM golang:1.13.4 as builder ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 WORKDIR /docker-multi-stage-build-101 COPY . . RUN go build -o "bin/goServer" # Runtime image FROM alpine COPY --from=builder /docker-multi-stage-build-101/bin/goServer /goServer EXPOSE 9000 ENTRYPOINT ["/goServer"]それでは、Dockerfileをビルドして、Dockerイメージのサイズがどれだけスリム化された確認してみましょう。
$ docker build . -f docker/02_multi-stage-build/Dockerfile -t go-server-multi:dev $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE go-server-multi dev 2d5f5819aa81 4 hours ago 21.4MB21.4MBとかなり小さなDockerイメージとなっていることがわかると思います。最初の例では915MBだったので、Dockerイメージのサイズをおよそ1/40程度までスリム化できています。
最後に
Dockerのマルチステージビルドを利用することで、アプリケーションのビルド環境と実行環境の設定を1つのDockerfileに記述することができます。これは、今まで使われていたBuilderパターンなどと比較しても、より簡素で管理のしやすい方法だと感じました。Dockerfileを記述する際は、積極的に使っていくといいのではないかと思います。
参考
Use multi-stage builds
Dockerの公式ドキュメントの説明です。
https://docs.docker.com/develop/develop-images/multistage-build/docker-multi-stage-build-101
今回の記事で使用したサンプルコードになります。
https://github.com/Imamachi-n/docker-multi-stage-build-101BioRxivGo
Code Chrysalisの多言語週間中に作成したフルスタックアプリケーション(Vue, Golang, PostgreSQLなど)になります。こちらのアプリケーションではマルチステージビルドに加えて、Docker composeでアプリケーションを起動しています。
https://github.com/Imamachi-n/BioRxivGo
- 投稿日:2019-12-22T13:16:08+09:00
【AWSハンズオン】Nuxt + Golang構成のWebアプリをAWSへデプロイしよう!
みなさん、こんにちは!
学生サーバサイドエンジニアのくろちゃんです。今日はAWSを題材に、自分で作ったWebアプリケーションをデプロイできるようになることを目指して記事を書かせていただきます!
この記事を最後まで読むことで、下記のようなことができるようになります!
- 自分のWebアプリケーションを公開できるようになる
- AWSの下記のサービスを使えるようになる
- VPC
- EC2
ぜひ最後まで一緒に手を動かしながら楽しんでいってくださいね?
なお、この記事はCA Tech Dojo/Challenge/JOB Advent Calendar 2019(22日目)の記事になります!
前日の@yawn_yawn_yawn_さんのUbuntu + nginx + LetsEncryptでSSL/TLSを設定するからバトンを引き継いでお送りしています。まだ見てないよって方はそちらも合わせてどうぞ!
では、早速ハンズオン始めていきましょう〜!!
前提条件
これらはできることを前提として話を進めていきますのでご了承ください。
※なお、本ハンズオンはOS Xを対象ユーザとして定めていますので、Windowsその他OSを使用している皆さんにつきましては適時読み替えて頂きますようにお願いします。
- AWSアカウントの作成&AWSマネージメントコンソールへのログイン成功
- GitHubアカウントの作成とSSH接続
- こちらのSSH接続についてわからない方は、GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ をご覧ください!
- Dockerがローカル環境で利用できる状態であること
目次
本ハンズオンでは下記のような流れでWebアプリケーションの公開〜自動デプロイの構築までを行っていきます。
0. 事前準備
1. VPCを構築しよう!
2. EC2インスタンスを起動しよう!
3. Webアプリをデプロイしよう!0. 事前準備
本ハンズオンでは私が事前に作っておいたアプリケーションを使用します。
下記の手順に従ってアプリケーションをダウンロード&動作確認してみましょう!GitHubリポジトリをforkしてこよう!
下記のGitHubリポジトリから自分のリポジトリへforkしてきましょう!
▼まずはアクセス▼
https://github.com/Takumaron/AWS_tutorialフォーク先を指定して、きちんとフォークできていれば下記のような画面になります。(Qiita-test-aws-tutorialの部分は皆さんのGitHubアカウント名になります。)
続いて、Forkしてきたリポジトリを自分のPCにクローンしてきます。
ターミナルを開いて下記のコマンドを入力してください。
git clone [コピーしたURL]
下記のような出力結果が得られれば正常にクローンできています。
$ git clone git@github.com:Qiita-test-aws-tutorial/AWS_tutorial.git Cloning into 'AWS_tutorial'... remote: Enumerating objects: 55, done. remote: Counting objects: 100% (55/55), done. remote: Compressing objects: 100% (45/45), done. remote: Total 55 (delta 9), reused 49 (delta 5), pack-reused 0 Receiving objects: 100% (55/55), 155.68 KiB | 484.00 KiB/s, done. Resolving deltas: 100% (9/9), done.アプリケーションが正常に動作するかチェックしよう!
クローンまでできたら、自分の環境で正しく動作するか検証しましょう。
下記のコマンドを入力してください。$ cd AWS_tutorial $ make start docker-compose up -d Building api_server Step 1/4 : FROM golang:1.12.4-alpine ---> b97a72b8e97d Step 2/4 : COPY ./ /go/aws_tutorial ---> 4fda45b3bf59 Step 3/4 : WORKDIR /go/aws_tutorial ---> Running in 421be46cf16b Removing intermediate container 421be46cf16b ---> 032f324638b8 Step 4/4 : RUN apk update && apk add --no-c ---- 中略(少し時間がかかります。少々お待ちください。) ----- Creating aws_tutorial_api_server_1 ... done Creating aws_tutorial_client_1 ... doneDockerコンテナが正常に立ち上がったら、お好みのWebブラウザを開いて http://localhost:3000/hello へアクセスしてください。
下記のような画面が表示されていれば、OKです!お疲れ様でした。1. VPCを構築しよう!
さっそく、先ほどローカル上で動作確認したアプリケーションをAWSに乗せていきます!
まずはVPCというサービスを使って、グローバルなWebの世界に自分の領域を作っていきます。下記の手順に従って一緒に設定していきましょう!
まずは、AWSマネジメントコンソールへサインインします。
https://console.aws.amazon.com/console/home?region=us-east-1
サインインが完了したら、左上の「サービス」をクリックして下記の画像のようにVPCを検索します。
このような画面は開けましたか?
今回構築するVPCの全体像
出た!訳のわからない図!!って言って諦めないでくださいw
システム構成図も理解できればそんなに大したものではありません。まず押さえていただきたいポイントは、VPCのなかにパブリックサブネットっていう奴があって、その中にEC2っていうものが動いているというざっくりしたイメージです。
分かりやすい例えがないかなと一生懸命考えて思いついた「家の例え」でこの構成を説明していきます。
まずはこの対応表を頭にいれてください。
サービス名 家に例えると? VPC ? 家 インターネットゲートウェイ ? 玄関 サブネット ? 部屋 EC2 ?? 住人 イメージとしては、
家(VPC)に外部から人がやってきたときには、玄関(インターネットゲートウェイ)を通して家に入ります。そして、部屋(サブネット)へ案内をして、住人(EC2)がおもてなしします。
という一連の流れで理解するとそれぞれの役割が明確になるのではないでしょうか?
それぞれのサービスの役割が理解できたところでハンズオンに戻っていきましょう!
VPCを作成する
サブネットを作成する
インターネットゲートウェイを作成する
ルートテーブルを編集する
サブネット(部屋)やインターネットゲートウェイ(玄関)がバラバラに存在している状態なので、それぞれの対応づけをしていきます。
VPCの構築はここまでで完了となります!お疲れ様でした!
次は、VPCの中で動作するEC2インスタンスを作成していきます。2. EC2インスタンスを起動しよう!
Webアプリケーションを動かすための場所作りは1. VPCを構築しよう!にて完了したので、実際に作ったWebアプリケーションを動かしてくれるマシーンを構築していきましょう!
【補足】
EC2インスタンスとは、仮想サーバーのようなものです。
AWSが事前に用意しているOSや独自で作成したOSイメージを使ってサーバを立てることができます。
今回は無料枠で利用できるAmazon Linux 2 AMIを利用します。【変更点】のみ入力内容を修正して「次のステップ」へ
キーペアは後ほどEC2にローカルからつなぎに行くときに必要になるファイルですので、ダウンロード後も無くさないようにしてくださいね!
ダウンロードが完了したら、「インスタンスの作成」をクリックすればEC2インスタンスの作成が始まります。(約2分ほど待てば下記のようにインスタンスの状態が「running」に切り替わると思います。)
お疲れ様でした!
ここまででVPCの構築と、EC2インスタンスの起動まで終わりました!あとは、EC2インスタンス上でWebアプリケーションを動かすだけです。もうちょっとなので一緒に頑張りましょう?
3. Webアプリをデプロイしよう!
さぁ、あとは自作したWebアプリケーションをEC2上で動かすだけですね!
Webアプリのデプロイは下記のような手順で行なっていきます。
- EC2インスタンスにssh接続する
- GitコマンドとDocker・Docker-composeコマンドが使用できるようにパッケージをインストール
- GitHubからプロジェクトをクローンしてくる
- アプリケーションを立ち上げる
それでは、さっそく始めましょう〜!
EC2インスタンスにSSH接続しよう!
まずは、作成したEC2インスタンスにSSHで接続しましょう!
自分のEC2インスタンスのIPアドレスを知っておく必要があるので、下記の画像を参考にして、IPアドレスをコピーしておきます。
それが完了したら、ターミナルを起動して下記のようなコマンドを打ちます。
# ダウンロードしたaws-tutorial.pemがあるディレクトリへ移動 $ cd ~/Downloads # pemファイルを~/.sshへ移動 $ mv aws-tutorial.pem ~/.ssh # pemファイルにアクセス権限を付与しておく $ chmod 400 ~/.ssh/aws-tutorial.pem # ssh接続 $ ssh -i "~/.ssh/aws-tutorial.pem" ec2-user@[自分のIPアドレス(ペースト)] Are you sure you want to continue connecting (yes/no)? yes # ←初回起動時には`yes`と入力ssh接続が完了すると、下記のようなコマンドプロンプトに切り替わります。
__| __|_ ) _| ( / Amazon Linux 2 AMI ___|\___|___| https://aws.amazon.com/amazon-linux-2/ 8 package(s) needed for security, out of 17 available Run "sudo yum update" to apply all updates. [ec2-user@ip-10-0-10-151 ~]$これでSSH接続は完了です!
ちなみに、SSH接続を切断する場合は、下記のコマンドを打ちます。$ exitGitとDockerを動かせるようにしよう!
SSH接続が完了したら、GitとDockerを使えるようにセットアップしていきましょう!
# yumをアップデート $ sudo yum update -y # Gitのインストール $ sudo yum install git -y # Dockerのインストール $ sudo yum install -y docker # Dockerの起動 $ sudo service docker start $ sudo systemctl enable docker.service $ sudo service docker status # 下記のような出力結果が得られればOK ● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled) Active: active (running) since 日 2019-12-22 03:26:02 UTC; 10s ago Docs: https://docs.docker.com Main PID: 12952 (dockerd) CGroup: /system.slice/docker.service └─12952 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.... # dockerコマンドを使用可能にする $ sudo usermod -a -G docker ec2-user # 一度exit $ exitここまででDockerとGitが動くようになりました。
SSHで再接続して、Docker-composeも入れていきましょう!# スーパーユーザに切り替える $ sudo -i # 必要なファイルをダウンロード curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # 権限を与える chmod +x /usr/local/bin/docker-compose # 通常ユーザに戻る exit # 確認 $ docker-compose --versionGitHubからプロジェクトをクローンしてこよう!
DockerとGitが使えるようになったので、GitHubからアプリケーションをダウンロードしてきましょう!
https://github.com/[あなたのGitHubID]/AWS_tutorial へ遷移しましょう。
「Clone with HTTPS」モードでクローンURLをコピーします。
コピーしたクローンURLを使ってEC2上にクローンしてきましょう!
$ git clone [ペースト] # AWS_tutorialが存在するか確認 $ ls AWS_tutorialアプリケーションを立ち上げよう!
ここまでこれば準備万端!あとは立ち上げるだけですね?
プロジェクトフォルダに移動して、アプリケーションを立ち上げましょう!
# プロジェクトフォルダへ移動 $ cd AWS_tutorial # アプリケーションを立ち上げる $ make start ... Creating aws_tutorial_api_server_1 ... done Creating aws_tutorial_client_1 ... done立ち上がりました!
Webブラウザからアクセスをして動作チェックをしてみましょう!動いているみたいですね!
バックエンドとの連携もできているでしょうか?http://EC2インスタンスのIPアドレス:3000/hello
できているみたいですね!!
バックエンドAPIに対してブラウザからはアクセスできないことも確認しておきましょう。
http://EC2インスタンスのIPアドレス:8080/ping
成功です!!お疲れ様でした
ハンズオンはここまでとなります。【発展】
バックエンド側(:8080)にアクセスできない理由は分かりますか?
余裕のある方は、発展としてなぜアクセスできないのか考えてみてください!(ヒントは、セキュリティグループです!)
いかがだったでしょうか?
かなり長いハンズオンになってしまいましたが、ここまで読んでいただきましてありがとうございました!今回はVPCとEC2を使って簡単なWebアプリケーションをデプロイしました。
次はRDSというサービスを使ってDB機能を持たせたり、Code Buildというサービスを使って自動デプロイを実現できるようなハンズオンを作りたいと思います!
明日のCA Tech Dojo/Challenge/JOB Advent Calendar 2019は@hmarfさんが担当です!引き続きお楽しみください!!
▼こちらも要チェック!!
?サイバーエージェント 2021エンジニア採用本選考開始?
— ゴッティ@サイバーエージェントエンジニア採用 (@masagotty) December 5, 2019
今年のテーマは『Make Your Chance』。
Chanceという単語には本来「偶然性」の意味合いもありますが、
それも含め自分から声を上げ動き、チャンスを作り出してほしいという思いを込めています。
挑戦、待ってます!https://t.co/zpnxztQOpl
- 投稿日:2019-12-22T09:34:49+09:00
Go の make の channel使ったときのデフォルト値
課題
make で channel を使ったときのデフォルト値が気になった。
quit := make(chan os.Signal)普段はキャパシティとかが後につくけど、これはどういうことだろう?デフォルト値は?
定義
こちらにこんな表があった。
Call Type T Result make(T, n) slice slice of type T with length n and capacity n make(T, n, m) slice slice of type T with length n and capacity m make(T) map map of type T make(T, n) map map of type T with initial space for approximately n elements make(T) channel unbuffered channel of type T make(T, n) channel buffered channel of type T, buffer size nmake は3つに大きくわかれているのですな。make(T) は、「バッファされない」がデフォルトのようです。
気になったのでソースコードを読んでいると、同じくコメントでありました。// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.すっきり。
- 投稿日:2019-12-22T08:36:13+09:00
Go言語の依存モジュール管理ツール Modules の使い方
Modules を使うには
Modules は、Go言語 1.11 以上のバージョンである必要があります。
1.11 以上をインストールすると、go mod
コマンドが使えるようになります。Modules を使う流れ
Modules を使う流れは以下のとおりです。
go mod init
で、初期化するgo build
などのビルドコマンドで、依存モジュールを自動インストールするgo list -m all
で、現在の依存モジュールを表示するgo get
で、依存モジュールの追加やバージョンアップを行うgo mod tidy
で、使われていない依存モジュールを削除する実は
go mod
を直接実行することは少なく、他の go サブコマンドを実行したときに、自動的に処理が行われることが多いです。以降は、コマンド実行例をご紹介します。
本記事では、バージョン1.12
で動作確認しています。1.
go mod init
で、初期化する新規プロジェクトを作成して、
go mod init
を実行します。
このコマンドには、引数としてモジュール名(example.com/go-mod-test
)を指定します。$ mkdir go-mod-test $ cd go-mod-test $ go mod init example.com/go-mod-test
go.mod
ファイルが作成されます。
以下のように、指定したモジュール名が記載されています。$ cat go.mod module example.com/go-mod-test go 1.122.
go build
などのビルドコマンドで、依存モジュールを自動インストールするまずビルド対象となるプログラムを作成しておきます。
以下のプログラムをmain.go
という名前のファイルで保存します。package main import ( "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { return events.APIGatewayProxyResponse{}, nil } func main() { lambda.Start(handler) }このプログラムでは、AWS Lambda のハンドラを実装しているので、
aws/aws-lambda-go
モジュールに依存している状態です。
ビルドコマンドを以下のように実行してみます。$ go build go: finding github.com/aws/aws-lambda-go/lambda latest go: finding github.com/aws/aws-lambda-go/events latest
依存モジュールがインストールされている様子が出力されると思います。
go.mod
の内容を見ると、依存モジュールの情報が記載されていることが確認できます。$ cat go.mod module example.com/go-mod-test go 1.12 require github.com/aws/aws-lambda-go v1.13.23.
go list -m all
で、現在の依存モジュールを表示する以下のように、現在の依存モジュールを確認してみます。
$ go list -m all example.com/go-mod-test github.com/BurntSushi/toml v0.3.1 github.com/aws/aws-lambda-go v1.13.2 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d github.com/davecgh/go-spew v1.1.0 github.com/pmezard/go-difflib v1.0.0 github.com/russross/blackfriday/v2 v2.0.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 github.com/stretchr/objx v0.1.0 github.com/stretchr/testify v1.4.0 github.com/urfave/cli v1.22.1 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 gopkg.in/yaml.v2 v2.2.2本プロジェクトで直接依存しているモジュールだけでなく、依存モジュールが依存しているモジュールもすべて出力されています。
4.
go get
で、依存モジュールの追加やバージョンアップを行う試しにログ出力のための
golang/glog
をインストールしてみます。$ go get github.com/golang/glog go: finding github.com/golang/glog latest
以下のように
go.mod
ファイルにインストールしたモジュールの情報が追記されています。$ cat go.mod module example.com/go-mod-test go 1.12 require ( github.com/aws/aws-lambda-go v1.13.2 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect )
// indirect
というコメントは、直接依存しているモジュールではないことを表しています。
先程作成したプログラムでは、まだgolang/glog
を使用していないので、このようなコメントが追記されています。5.
go mod tidy
で、使われていない依存モジュールを削除する以下のように、使われていない依存モジュールを削除してみます。
$ go mod tidy go: downloading gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 go: extracting gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go.mod
の内容を確認してみます。$ cat go.mod module example.com/go-mod-test go 1.12 require github.com/aws/aws-lambda-go v1.13.2先程インストールした
golang/glog
が使われていないので、その情報が削除されている状態になっています。以上、Modules を使う流れでした。
参考
Modules
https://github.com/golang/go/wiki/ModulesGo & Versioning
https://research.swtch.com/vgoUsing Go Modules
https://blog.golang.org/using-go-modules
- 投稿日:2019-12-22T07:08:05+09:00
Go で 無限ループをタイムアウトもしくは、Ctr+C で終了させたい
Go で、非同期リクエストを実施した後に、Pollingの処理を書いている。ふと気づいたのだが、その処理は無限ループの処理で、タイムアウトを設定している。しかし、タイムアウトまで長いので処理を中断させたいケースはままあるのだが、そういう時はどうするのだろう?
シグナルを受信する
最初に必要なのは、
Ctr+c
のシグナルを受信することだ。signal.Notify
の関数が使える。func Notify(c chan<- os.Signal, sig ...os.Signal)
os.Signal
のチャネルを作って、受信したいシグナルを列挙すればよさげ。ちなみに、シグナルを指定しないと、すべてのシグナルを受信するのがデフォルトになります。シグナルの種類
受信できるシグナルの種類ですが、Go の場合こちらに書いてあります。
Linuxのシグナルと同じになっていますが、そもそもLinux のシグナルにどんなものがあったのか覚えていません。
上記のマニュアルを見ると、SIGINT が該当しそうです。コマンドラインツールなので、SIGHUPも受信したほうがいいかなぁ、、、と思ったけど(例えば無限ループしている状態で、Terminal落としたとか)、その場合は、コマンドのプロセスごと死ぬからまあいいかと。
シグナル 値 動作 コメント
SIGHUP 1 Term 制御端末(controlling terminal)のハングアップ検出、
または制御しているプロセスの死
SIGINT 2 Term キーボードからの割り込み (Interrupt)
SIGQUIT 3 Core キーボードによる中止 (Quit)
SIGILL 4 Core 不正な命令
SIGABRT 6 Core abort(3) からの中断 (Abort) シグナル
SIGFPE 8 Core 浮動小数点例外
SIGKILL 9 Term Kill シグナル
SIGSEGV 11 Core 不正なメモリー参照
SIGPIPE 13 Term パイプ破壊:
読み手の無いパイプへの書き出し
SIGALRM 14 Term alarm(2) からのタイマーシグナル
SIGTERM 15 Term 終了 (termination) シグナル
SIGUSR1 30,10,16 Term ユーザー定義シグナル 1
SIGUSR2 31,12,17 Term ユーザー定義シグナル 2
SIGCHLD 20,17,18 Ign 子プロセスの一時停止 (stop) または終了
SIGCONT 19,18,25 Cont 一時停止 (stop) からの再開
SIGSTOP 17,19,23 Stop プロセスの一時停止 (stop)
SIGTSTP 18,20,24 Stop 端末より入力された一時停止 (stop)
SIGTTIN 21,21,26 Stop バックグランドプロセスの端末入力
SIGTTOU 22,22,27 Stop バックグランドプロセスの端末出力SIGINT 受信で終了する際の終了ステータスコード
SIGINTを受け付けて終了する場合のステータスコードって何になるのでしょう?下記のブログにとてもいい表がありました。
上記ブログより
終了コード 意味 例 コメント 1 一般的なエラーの catch-all let "var1 = 1/0" "divide by zero" や、他の許容されない操作を含む各種のエラー 2 シェル内蔵機能の誤使用 (Bash ドキュメントに従う) empty_function() {} キーワードが見つからないまたはコマンドが存在しない、またはアクセス許可の問題 (および、バイナリファイル比較の失敗時に diff が返すリターンコード) 126 起動されたコマンドが実行できない /dev/null アクセス許可の問題、またはコマンドが実行形式ファイルでない 127 "command not found" illegal_command $PATH の問題、またはタイプミス 128 exit への無効な引数 exit 3.14159 exit は 0 ~ 255 の範囲の整数値のみを引数として受け付けます (最初の脚注を参照) 128+n 致命的なエラー信号 "n" kill -9スクリプトの $PPID $?137 (128 + 9) を返す 130 スクリプトが Control-C により終了された Ctl-C Control-C は致命的なエラー信号 2 (130 = 128 + 2、上記参照) 255* 終了ステータスの範囲外 exit -1 exit は 0 ~ 255 の範囲の整数値のみを引数として受け付けます これでいくと今回は
130
でExit したらよさそうですが、まずい、今まで問題があったら1
で終了させていた。(CIで使うことを想定したツールだったので、終了させたかった)でも本当は終了ステータスはいろいろだから、ちょっとこの辺はライブラリ書いたほうがよさそう(もしくは、ライブラリを探すか、、、)サンプルコード
サンプルコードは次の感じです。
quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt) fmt.Println("step1") <-quit fmt.Println("step2")こちらを実行すると、step1 のみが最初に表示されて、
ctr+c
を受信したら step2が表示されて終了します。Notifyは、処理をブロックしませんのので、こんな感じで書けそうです。タイムアウトと、複数のチャネルを受信する
シグナルの受信の方法はわかったのですが次の課題は、複数のチャネルを扱う方法です。シグナルを受信したら、チャネル経由で受け取るわけですが、Pollingの無限ループは現在も、タイムアウトの処理が必要で、尚且つ、現在は無限ループを go routine でラップしていて、チャネルを受け取っているので、複数のチャネルの処理が必要です。
結論からいくと、go lang の
select
は相当強力で、複数のチャネルの受信も書くことができます。func main() { timeout := 30 quit := make(chan os.Signal) c := make(chan string, 1) signal.Notify(quit, os.Interrupt) go func() { result := loop() c <- result }() select { case result := <-c: fmt.Println(result) case <-quit: fmt.Println("Interrupt signal accepted.") case <-time.After(time.Duration(30) * time.Second): fmt.Printf("Timeout happened. exceed %d second.\n", timeout) } } func loop() string { counter := 0 for { time.Sleep(5 * time.Second) counter++ fmt.Println("Do my job:" + strconv.Itoa(counter)) if counter == 50 { return "done" } } }実行例
$ go run main.go Do my job:1 Do my job:2 Do my job:3 Do my job:4 Do my job:5 Timeout happened. exceed 30 second.
ctr+c
$ go run main.go Do my job:1 Do my job:2 Do my job:3 Interrupt signal accepted.
タイムアウト処理
タイムアウト処理を解説すると
time.After
関数を使っています。一定期間が過ぎると、チャネルに送信します。ちなみに、このマニュアルを読んでいると、効率のためには、NewTimer
を使えとあります。func After(d Duration) <-chan Time折角なのでこちらも試してみましょう。戻りがTimerなので、どうやってチャネルを待つのだろうと思ったのですが、Timerのストラクトに、Cというチャネルを持っているのでそれを使えば良さげです。
func NewTimer(d Duration) *Timer先ほどのプログラムを一部変更
// case <-time.After(time.Duration(30) * time.Second): case <-time.NewTimer(time.Duration(30) * time.Second).C: fmt.Printf("Timeout happened. exceed %d second.\n", timeout)実行結果
同じように動作しているようです。
$ go run main.go Do my job:1 Do my job:2 Do my job:3 Do my job:4 Do my job:5 Timeout happened. exceed 30 second.
リソース
ちなみに今回の調査で、go routine のパターンのブログを見つけたので貼っておきます。
go routine と select はもうちょい勉強せなあかんなぁ。
- 投稿日:2019-12-22T06:30:54+09:00
GithubActionsでプライベートリポジトリをsubmoduleとして取り込む
GithubActionsでプライベートリポジトリをsubmoduleとして取り込む方法はsubmodule addコマンドでhttps方式で追加したかssh方式で追加したかによって変わります。それぞれのメリットデメリットを説明しつつ、実現方法を記しておきます。
https方式 git submodule add https://github.com/u-nation/GITHUB_REPOSITORY.git ssh方式 git submodule add git@github.com:u-nation/GITHUB_REPOSITORY.githttps方式
手軽なのはhttps方式です。
以下の権限を持つパーソナルアクセストークンを作成し、secretsに登録してワークフロー内でwithオプションに渡してあげるだけで済みます。
※パーソナルアクセストークンはhttps方式しか使えません。1
.github/workflows/sample.yml- name: submodule uses: actions/checkout@v1 with: submodules: true token: ${{ secrets.PERSONAL_TOKEN }}
- メリット 手軽さ
- デメリット パーソナルアクセストークンは発行したアカウントがアクセスできるリポジトリ全てに権限が及ぶのでトークンが漏れた場合のセキリュティリスクが大きい。Bot用のアカウントを作成やトークン作成の依頼・共有など運用がしんどい。
ssh方式
- ssh-keygenコマンドで秘密鍵公開鍵を作成
- submodule先のリポジトリのDeploy Keysに公開鍵を登録(必要に応じてwrite権限を付与)
- 取り込む方のリポジトリのsecretsに秘密鍵を登録
- ワークフローでsshの設定とsubmodule update --initをする
.github/workflows/sample.yml- uses: actions/checkout@v2 - name: SSH Setting env: TOKEN: ${{ secrets.TOKEN }} run: | mkdir -p /home/runner/.ssh/ echo -e "$TOKEN" > /home/runner/.ssh/id_rsa chmod 600 /home/runner/.ssh/id_rsa # https://github.com/actions/checkout#checkout-submodules - name: Checkout submodules shell: bash run: | git submodule sync --recursive git submodule update --init --force --recursive
- メリット 鍵の権限はそのリポジトリに限定されるので漏れた時のセキュリティリスクが軽減される。必要に応じて各々が鍵の設定を追加できるので運用が楽。
- デメリット 鍵作ったり設定するのがちょっとだけめんどい。
所感
企業でGithubを使う場合は大抵プライベートなリポジトリだと思うので、誰かのお役に立てると嬉しいです。
ssh方式を実現するのに6時間位ハマって辛かったですが、運用が楽になるのでssh方式がオススメです。
注釈
注釈
GithubActionsに移行する前は、AWSのCodeBuildを使用していました(GithubAcitonsの方がめちゃくちゃ早いです)。その時は秘密鍵をSystem Managerで用意してssh方式をしていましたが、途中でhttps方式のsubmoduleオプションが登場しました。 ↩
- 投稿日:2019-12-22T02:41:53+09:00
一般的な方法で犬の年齢を人間換算したらそんな歳じゃないワン!!って怒られそうなので正しい年齢を算出してみた
今までワンコの年齢を人の年齢換算するときは、犬の年齢を7倍する方法を取られていたようですが、
DNAを調べて新たに計算式が作られたようです。
https://www.akc.org/expert-advice/health/how-to-calculate-dog-years-to-human-years/その計算機をGoで作ってみました。
ついでなので、今までの計算方法のものも作って比較してみました。つくったやつ
https://github.com/usk81/animal-age-to-human-year計算式の結果を表にまとめるとこんな感じです。
犬の年齢 人の年齢換算(数式) 人の年齢換算(一般的) 0 0 0 1 31 7 2 42 14 3 48 21 4 53 28 5 56 35 6 59 42 7 62 49 8 64 56 9 66 63 10 67 70 11 69 77 12 70 84 13 72 91 14 73 98 15 74 105 16 75 112 17 76 119 18 77 126 19 78 133 20 78 140 7倍してる方やばいですね、20歳になると人間超越しています。
新しい計算式の方は、1歳でもういいおっさんになってます。
31歳超えてない人は1歳以上のワンコを見たら、犬パイセンと呼ばないといけないかもしれません。
20歳はいい感じのご老人ですね。勉強がてら、go-chartを使って年齢をプロットしてみました。
青いほうが今回の計算式のものです
go-chart
https://github.com/wcharczuk/go-chartgo-chart使い方わかると便利です。
例見てもよくわからなかったので試行錯誤が大変でしたが...
- 投稿日:2019-12-22T02:24:30+09:00
go のプロジェクトでクラス図を生成する
概要
go のソースコードから、PlantUML で表示可能なクラス図を生成する。
試した環境
- macOS Mojave 10.14.5
- go 1.13.5
- PlantUML Version 1.2019.12
事前準備
goplantuml のインストール
$ go install github.com/jfeliu007/goplantuml/...
$GOPATH/bin
以下にgoplantuml
がインストールされる。PlantUML のインストール
以下は mac の場合。
$ brew install graphviz $ brew install plantuml試してみる
ここでは試しに、goplantuml 自身のクラス定義ファイルを出力してみる。
事前にソースを入手する。$ cd /tmp $ git clone https://github.com/jfeliu007/goplantuml.git $ cd goplantumlgoplantuml でクラス定義ファイルを出力
goplantuml
コマンドで、対象プロジェクトのディレクトリを指定する。
-recursive
をつけると、ディレクトリ以下を再帰的に解析する。$ goplantuml -recursive ./parser > /tmp/parser.pumlそのまま実行すると標準出力に出力されるので、ここでは
/tmp/parser.puml
というファイルに出力されるようにした。PlantUML でクラス図を生成する
$ plantuml /tmp/parser.puml出力した定義ファイルと同一ディレクトリに、拡張子が
.png
のクラス図画像ファイルが出力される。
(ここでは/tmp/parser.png
)メモ
画像が大きすぎて途中で切れてしまう場合
大きめのプロジェクトのクラス図全体を出力しようとすると、画像が入りきらず途中で切れてしまう場合がある。
https://github.com/qjebbs/vscode-plantuml/issues/136 でもこの問題について触れていて対策も紹介されているが、以下のような対応方法が考えられる。SVG 形式で出力する
PlantUML 実行時に
-tsvg
オプションをつけて実行する。
ただ、SVG形式の画像を開けるビューアは限られる(Safari など)。
また画像サイズが大きい場合は表示自体がとても重くなる。環境変数
PLANTUML_LIMIT_SIZE
を適切に設定するPlantUML が生成する画像の縦横サイズの上限値。デフォルトでは 4096 x 4096。
これを増やすことで、多少は余裕が生まれる。
ただ実用上、20000 くらいが上限と思われる。$ export PLANTUML_LIMIT_SIZE=20000対象ディレクトリを制限する
goplantuml
の-ignore
オプションで、除外するディレクトリを指定できる。(カンマ区切りで複数指定可能)
クラス定義ファイル、または画像を出力してみて、明らかに情報がいらないクラスがあった場合は-ignore
に順次追加していく。全体を表示することをあきらめる
goplantuml
実行時に指定するディレクトリの階層を下げてクラス数を減らす。
これが一番現実的な方法なのではないかと思う。
(仮に画像サイズを大きくしたところで、全体を見るのがかなり辛いのでw)
- 投稿日:2019-12-22T02:11:04+09:00
日本の緯度経度の情報を全取得するダウンローダを作った
Google Mapを頼らずに住所から位置情報を自力で取得できないか調べてみたら簡単にできそうだった。
国土交通省国土政策局が公開しているので、そこから取得したらよさそうです。GIS
http://nlftp.mlit.go.jp/cgi-bin/isj/dls/_choose_method.cgiこんな感じ
面倒そうなので、ダウンローダを作ってみた。
上記のスクリーンショットの版数(赤枠のところ)だけ覚えておいてください以下で、全国の位置情報をダウンロードできるようにしました。
go run main.go 版数 保存先コードは以下の通り。
ref. https://github.com/usk81/til/tree/master/go/download-japan-gis-datapackage main import ( "fmt" "io" "net/http" "os" "path/filepath" ) const urlTemplate = "http://nlftp.mlit.go.jp/isj/dls/data/%s/%02d000-%s.zip" // e.g. go run main.go 17.0a /home/xxx/gis func main() { args := os.Args[1:] if len(args) <= 1 { panic(fmt.Errorf("arguments are not enough")) } if err := bulkDownload(args[0], args[1]); err != nil { panic(err) } fmt.Println("download finished") } func bulkDownload(ver, p string) (err error) { if ver == "" { return fmt.Errorf("version is empty") } if p == "" { return fmt.Errorf("download path is empty") } for i := 1; i <= 47; i++ { u := getURL(ver, i) fp := filepath.Join(p, fmt.Sprintf("%02d000-%s.zip", i, ver)) if err = downloadFile(u, fp); err != nil { return fmt.Errorf("fail to request : %s", u) } } return nil } func downloadFile(u, p string) (err error) { // Create the file out, err := os.Create(p) if err != nil { return err } defer out.Close() // Get the data resp, err := http.Get(u) if err != nil { return err } defer resp.Body.Close() // Write the body to file _, err = io.Copy(out, resp.Body) if err != nil { return err } return nil } func getURL(ver string, no int) string { return fmt.Sprintf(urlTemplate, ver, no, ver) }
- 投稿日:2019-12-22T00:00:38+09:00
Go1.14で来るGo Modules関連の変更を見てみる
この記事は Go3 Advent Calendar 2019 の21日目です。
本日(12/21)時点でのGoの最新バージョンは1.13.5ですが、次バージョンであるGo1.14が2020年2月頃にリリース予定です。
ここ数バージョンで急激に発展してきたGo Modules周りにもいくつか変更が入る予定です。
そこでGo1.14ではどのような変更が入るか簡単に追ってみたいと思います。
Vendoringまわりの変更
vendorディレクトリが存在する場合の
-mod
フラグの挙動Go1.13時点では
go mod vendor
を行うとプロジェクト配下にvendorディレクトリが作成され、そこに全ての必要なパッケージが配置されます。Go1.14ではvendorディレクトリが存在し、
go.mod
ファイルによるGoのバージョン指定がgo1.14
以上である場合、go
コマンドがデフォルトで-mod=vendor
フラグを受け入れます。
-mod=mod
の追加新たに
-mod=mod
フラグが追加され、-mod=mod
を指定する場合は従来のモジュールキャッシュからモジュールをロードするようになります。また
-mod=vendor
が指定されている場合は、go
コマンドはvendor/modules.txt
ファイルとgo.mod
に一貫性があるか検証を行います。
go list -m
の暗黙的な挙動変更今まで
go list -m
はvendorディレクトリに存在しない暗黙的な依存関係を省略していましたが、それがなくなります。-mod=vendor
が指定されている場合は明示的に失敗となります。vendor周りはデプロイ時やコンテナイメージ作成でパッケージを配置する際によく使われることがあると思いますが、挙動の変化に注意したいですね。
go getの変更
go get
コマンドで-mod
フラグを受け入れなくなります。
Go1.13までは-mod
フラグが付与されている場合は無視されるか失敗となっていたので、明示的に付与しないようにする必要があります。フラグまわりの変更
-mod=readonly
のデフォルト適用追加今までもCIやTest用途で
go.mod
に変更が必要ないことを確認するために役立つ-mod=readonly
フラグですが、go.mod
ファイルが読み取り専用かつvendorディレクトリが存在しない場合、-mod=readonly
がデフォルトで設定されるようになります。
-modcacherw
フラグの追加これは
-mod=readonly
のように変更がある場合は失敗させるのではなく、モジュールキャッシュに新しく作成したディレクトリを残すように支持するフラグです。これによってCIやテストによって検証されていないファイルを誤って追加されてしまう可能性が高まりますが、go clean -modcache
ではなくrm -rf
で明示的にモジュールキャッシュを消すことが可能です。つまり全てのモジュールキャッシュを削除するのではなく、新たに追加された部分だけを明示的に
rm
コマンドで消せるようにするフラグだと思っています。
-modfile=file
フラグの追加
-modfile=file
はトップディレクトリにあるgo.mod
ではなく、明示的に読み込みたいgo.mod
ファイル(modfile)を読み取るフラグです。
本来modfileは暗黙的に読み込まれますが、このフラグを使うと指定したmodfileを読み込むようになります。
またmodfileを指定した場合、それに対応するgo.sum
ファイルも参照します。それはmodfileから.mod
拡張子を削除し、.sum
をつけて参照されます。モジュールの非互換バージョンの挙動変更
go.mod
ファイルに最新バージョンのモジュールが含まれている場合、go get
を行っても明示的にバージョンが指定されている場合を除いて、互換性のないメジャーバージョンにはアップデートをしなくなります。
go.mod
ファイルのメンテナンス
go mod tidy
以外のgo
コマンドは、変更が表面的なものである場合go.mod
ファイルを編集しなくなりました。明示的にメンテナンスするにはgo mod tidy
する必要があります。そして
-mod=readonly
を設定した場合、goディレクティブの欠如や誤ったindirect
コメントが原因でgo
コマンドが失敗しないようになります。その他
go
コマンドはモジュールモードでSubversionをサポートするようになります。また
go
コマンドはProxyや他のHTTPサーバからのプレーンテキストなエラーメッセージのスニペットを含むようになります。
エラーメッセージはUTF-8で、グラフィック文字(?)とスペースのみで構成されている場合に表示されます。終わりに
Go1.14も更にモジュール管理がカッチリしたものになりそうな片鱗が見て取れますね。
またリリース後に検証して行きたいと思います。