20191222のGoに関する記事は26件です。

【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_profile
export 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.0beta11

Version情報が表示されれば、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.0

Goの環境設定

最後に使用する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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 test

src ディレクトリは以下のようになっています。
そこそこ 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 vendor

Go で 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.go
package 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

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

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.go
type 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.go
status.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.go
func 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.go
runtime.HTTPError = HTTPError

まとめ

【前編】【後編】に続き、gRPC-Gatewayに関する実装で、カスタムエラーを扱う方法を紹介しました。
WithDetailsメソッドを使えば、紹介したフィールド以外でも返せるので、より詳細なエラーをクライアントに伝えることが可能になります。

また、カスタムエラーの記事はいくつかありますが、metadataでを使う方法が多く、おそらく古い方法なのでお勧めしません。
もし、他にいい実装方法を知っている方がいたら、コメント等でご紹介いただければ幸いです。

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

docker pullをちょっとだけ楽にするCLIつくりました

つくったもの

dockerのimageからtagを検索&選択してpullできるCLI

※動画の圧縮になれておらず、、画質が悪いです。。

バージョン等

  • 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-buster

cobraも、promptuiも色々できることがありそうなので、今後拡張していきたいです。

まとめ

Docker アドベントカレンダー担当日の1日前に思いたったのですが、ライブラリが非常に強力で1日かからずにものをつくることができました!

今回作ったものは足りないところがたくさんありますが、実際つくってみると個人的には欲しい機能がどんどん思いついていきたので、今後継続的に開発していきたいと思います。

実は今年最初やりたいことのひとつに「CLIツールをつくる」がありまして、ぎりぎり達成することができたのでよかったです!!
(この記事を書いている最終に思い出しました笑)

参考

Examples using the Docker Engine SDKs and Docker API
How do I authenticate with the V2 API?

どうでもいいからささっとGoでCLIつくりたいとき
Go初心者がGoでコマンドラインツールの作成に挑戦した話

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

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.go
package 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.go
package main
import "fmt"

func init(){
    fmt.Println("Initializing...")
}
func main(){
    fmt.Println("Hello, World!")
}
//Initializing...
//Hello, World!

Import

Goでは、様々なパッケージをインポートしつつプログラムを書いていきます。
以下は、現在の時刻を出力するプログラムです。

PrintTime.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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スタンプ買っちゃいました。
Gopher

以上です。

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

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
作成にあたっては、クライアントライブラリの examplenode_exporter参考にしました。

node_exporter では、取得する情報を引数で指定してフィルタしたりしてましたが、今回取得情報に関する引数は取らないので、こんな感じになりました。

sample-exporter/main.go
func 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.go
var (
...
    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 golang

sample-exporter/collector/memory.go
const (
    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 で長期安定化試験のときに見られると便利ですしね。

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

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.yml
version: '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/ を開く

Screen Shot 2019-12-22 at 20.31.13.png

Go で Shared Library を 作成する。

VSCode->File(Menu)->/app/wgo に移動する。

hello.go
package 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.hlibhello.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.dart
import '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

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

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.yml
version: '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/ を開く

Screen Shot 2019-12-22 at 20.31.13.png

Go で Shared Library を 作成する。

VSCode->File(Menu)->/app/wgo に移動する。

hello.go
package 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.hlibhello.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.dart
import '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

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

go langでslackと連携 api

main.go
package 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
}

ほぼ自分用

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

Goを真面目に勉強する〜3.関数と型〜

はじめに

Goをはじめて1年半。アウトプットが進まない私が、専門家の@tenntennさんから受けたマンツーマンレッスンの内容をまとめて、Goのスキルアップを目指します。Goの基礎から丁寧に学んでいきます。
記事のまとめは以下の通りで順次作成していきます。
今回は「3.関数と型」になります。

シリーズの一覧

  1. Goについて知っておく事
  2. 基本構文
  3. 関数と型(今回)
  4. パッケージとスコープ(次回予定)

本記事の内容

今回学ぶ内容は以下の通りです。

  • 組込み型
  • コンポジット型
    • 構造体
    • 配列
    • スライス
    • マップ
  • ユーザ定義型
  • 関数
    • 組込み関数
    • 関数定義
    • 無名関数
  • メソッド
    • メソッド値
    • メソッド式

組込み型

(本項目は前回記事に記載のものと同様です)
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,
}

構造体のフィールドが構造体の場合

フィールドが構造体の場合は少し注意が必要です。フィールド値は、型が明示されているか、型推論できる必要があります。intstringのような組込み型の場合は値リテラルを指定すれば型推論してくれますが、コンポジットの場合はコンポジットリテラルを与える必要があります。
次の例では、フィールドを構造体で宣言している場合の初期化をおこなっていますが、これを念頭に見れば理解できると思います。

// 変数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

スライス

配列の一部を切り出したデータ型で、型情報に要素数は含めません。その特性から、スライスの背後には配列が存在することが前提になります。

スライス.png

スライスを構成している配列、要素数、容量がどのように管理されているのか、以下のスライスの内部構造を見れば分かります。
https://golang.org/search?q=slice#Global_pkg/runtime

type 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()で使われている変数inは、fs[i]に代入したタイミングではなく、f()を実行する時点でのinの値で計算されます。

メソッド

参考: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
メソッド値はメソッドを値として表したもので、レシーバは束縛された状態になります。

書式
レシーバ "." メソッド名

以下の例では、fgにメソッド値を値として保存しているので、保存した時点の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
}

次の例のようにレシーバをポインタにした場合、束縛されるレシーバ値が変数へのポインタなので、fgにメソッド値を値として保存するのもポインタになりますので、上の結果とは異なります。

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

最後に

無名関数はほぼ使ったことが無かったため理解するのに少し時間がかかりました。メソッド値、メソッド式については触れたことが無いので、これを書いている時点でも不安がありました。
前回までよりも量が多くて、書き終えるのに時間がかかってしまったので、次に書くパッケージとスコープについては、もう少しスピーディに進めようと思います。

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

Go言語でつくるインタプリタを Haskell で書く

この記事は?

  • Haskell の基本機能だけでプログラミング言語作成したのでご紹介します
    • 既存のライブラリやパーサジェネレータ等を使用せずにプログラミング言語を作成
  • 元ネタは Go 言語で書かれた「Go言語でつくるインタプリタ」という書籍
  • 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 World

Haskey における関数は値(ファーストクラス)として扱えます

>> 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 作成の概要

作成の大まかな手順は以下の通りです。これらをテストファーストで実装していきました。

  1. 字句解析実装
  2. 構文解析実装
  3. 評価器実装
  4. 配列、ビルトイン関数などのその他機能を実装

ひとつひとつをもう少し掘り下げて説明します。

字句解析

  • 対象モジュール
    • 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 型が処理したい内容は主に以下です。

  1. Token から抽象構文木を生成する
  2. Token 配列を入力に受け取る
  3. 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
        <*  goAheadIfNextSemicolon

if 式のパーサ

-- | 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 型を定義し、処理させたい内容として以下を実装しました。

  1. 環境を入力に評価した結果を返す
  2. 環境の状態を扱う
  3. 評価が失敗した際のエラー処理を行う

ここに現れる「環境」というのは定義した識別子とデータを紐付けた情報です。
評価時に出現した識別子の情報を環境に持たせて併せて評価します。
これもまた 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 との違いはこちらも構文解析と同じよう評価コンビネータ?を定義しエラー処理や環境の引き回しを自動で行うよう設計しました。

ここまでで評価の実装も完了プログラム言語として最低限機能するようになりました。

その他の実装

その後以下の機能を実装しました。

  1. 配列の実装
  2. ビルトイン関数の実装
  3. 標準出力の実装

これらはもはやは消化試合と考えていたんですが標準出力関数puts()の実装でつまずきました。
恐らくちゃんとした Haskeller なら事前に気づくと思うようなことなのですが。

Go 版は puts() 評価時に Print するのですが Haskell は言語使用上副作用や IO の伴う処理は原則専用の型の関数でしか扱えません。
これまでの設計で評価関数は IO を処理できる関数として設計していなかったため、そのままでは標準出力できません。
色々悩みましたが解決策としては評価時に入力として環境と共に Buffer を引数にとり、標準出力が生じた際にはバッファリングし main 関数内で評価の合間にフラッシュする設計としてお茶を濁しました。

まとめ

Haskell の基本機能のみでプログラミング言語が書けるのかは不安でしたが、実際は「Go言語でつくるインタプリタ」の内容を写経 + αくらいの感覚で書けました。
もともとの Go 版の実装がシンプルであり、他の言語に簡単に置き換え可能な Go 自体のポテンシャルにも改めて気きづきました。

Haskell としての良さはやはりエラー処理、状態の引き回しなど本質的な部分以外を
型システムに落とし込むことにより、本来やりたい作業だけに集中できるというのが非常に気持ちよかったです。

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

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

処理の流れ・プロジェクト構成

処理の流れは次の通りです。

  1. HackerNews APIからHTMLを取得する(fetch)
  2. HTMLからタグを取り除きTextだけにする(extract)
  3. TextをToken化・Taggingする(tokenize)
  4. 単語の集計を行う(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.go

HackerNews APIからHTMLを取得する

実はHackerNewsはAPIが公開されており、自由に記事を取得することができます。
こちらのGithubにAPIの仕様が載っています。

HackerNews/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.go
package 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を用いた、スクレイピングの際に非常に便利です。

PuerkitoBio/goquery

extractor.goの処理は非常にシンプルです。
scriptタグやstyleタグなど、HTMLタグ中で単語と集計して困るテキストをすべて除去し
最後にbodyタグ内のテキストを抽出しています。

extractor.go
package 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というライブラリですべて行うことができます。

jdkato/prose

tokenizer.go
func 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.go
package 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.go
package 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.txt
Text: , 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の内容によって内容をフィルタリングしていないため、ピリオドやカンマが最頻出単語になってしまってます。(当たり前)
面白い点としては、FacebookGoogleなどが上位に食い込んでいること。やはりFANGはHackerNewsでも話題の種みたいですね。

実装をUPDATE次第、こちらの単語帳も更新していきたいと思います。

今後の改善ポイント

英語全体で頻出な単語が集計されている

isとかaなどの英語全体での頻出単語が出力されている状態です。
そのため、英語全体での頻出英単語は取り除く処理を入れる必要があります。

幸いにも、Googleをスクレイピングした頻出英単語集はあるので、それをもとに、集計から除外する処理を入れればよいです。

Goルーチンで並行処理化を行う

現状、一切、Goルーチンを使ってません。
TopStoriesが取れれば、それ以降は処理を並列化できます。そのため、処理を並列化してより処理の高速化を図りたいと思います。

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

ポケモン名だけで作った「いろは歌」を高速に生成する(ビット演算+DFS+Go+GCP)

みなさんポケットモンスター ソード&シールドやってますか?私は開会式に参加したところで中断しており、まだバッジ0個です。

さて、ポケモンを愛する皆さんであれば、一度はポケモン名でいろは歌を作りたいと思ったことがあるはずです。つまりどういうことかというと、ア〜ンまでのカタカナ45音を重複なく一度ずつ利用したポケモン名の列挙をしたいということです。例えば、以下の12体のポケモンの組み合わせは上記条件を満たしているので、いろは歌として成立しています。

iroha.001.jpeg

ではこのような組み合わせが、初代〜ソード&シールドまでの890匹の全ポケモンで一体何パターン存在するのでしょうか。私は気になって夜も眠れません。そこでこの記事では、高速にポケモンいろは歌の全列挙を行うためのアルゴリズムと実装の考察を行います。

先行研究として、このようなポケモン名によるいろは歌生成は9-8. vcoptでポケモン「いろは歌」できるかな(世界初) | Vignette & Clarity(ビネット&クラリティ)で既に取り組まれています。こちらでは遺伝的アルゴリズムを用いた探索を行われているようです。このアプローチでは汎用的なライブラリで問題を解くことができる一方で、特定の文字を持つポケモンのスロットを決め打ちするなど、問題設定の工夫が必要になっていそうです。

問題設定

  • カナ45音を一度ずつ全て使ったポケモン名の組み合わせが何パターン存在するかを計算する
    • 本来いろは歌は「ン」を含めて48音ですが、今回は「ゐ」「ゑ]「を」を省いた45音とします。
  • 記号を含むポケモン名は、読みのカタカナに変換する
    • 例) ポリゴンZポリゴンゼット
  • 長音や読みが無い記号は無視
    • タイプ:ヌルのコロンは無視します
  • メガシンカやアローラの姿などは除外する
  • 濁音、半濁音は取り除く

  • 小文字は大文字に直す

下2つのルールにより、例えばピカチュウヒカチユウとなります。
ちなみにイーブイが2回含まれているので絶対にいろは歌で登場することはありません。

ポケモン名の一覧はWikipediaの全国ポケモン図鑑順のポケモン一覧ページを元データとします。
ChromeのDevToolを開き,コンソールで以下のコマンドを実行することで、1行1ポケモン名の文字列を得ることができます。コピーしてあらかじめ適当なファイル名とヘッダを付けて、csv形式として保存しておきます。(後ろの方は関係ない文字列が含まれてしまうので、890行目のムゲンダイナ以降は省いてください)

スクリーンショット 2019-12-21 11.03.33.png

pokemon_list.csv
Name
フシギダネ
フシギソウ
フシギバナ
...

アプローチ

さて、いろは歌への意気込みはあるものの、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,ポカブ,ハトーボー,ホイーガ,シンボラー,ボルトロス,ハリボーグ,ホルビー,ホルード,アブリボン,ホシガリス,ポットデス,マホイップ,コオリッポ,
ヒ: キャタピー,ビードル,スピアー,ピクシー,ヒトデマン,カビゴン,ピィ,ブビィ,バルビート,ブーピッグ,ビブ ラーバ,ヒンバス,ビッパ,ビーダル,ビークイン,ピンプク,スコルピ,ドラピオン,ヒードラン,ヒヤップ,ヒヤッキ ー,コアルヒー,シビルドン,ゴビット,ビリジオン,ヒトツキ,ヒドイデ,ラビフット,
チ: チコリータ,オタチ,クチート,パッチール,チルット,チルタリス,チリーン,ジラーチ,パチリス,チャオブー, フタチマル,マラカッチ,バルチャイ,チゴラス,ガチゴラス,カチコール,バチンキー,パッチラゴン,パッチルドン,ドロンチ,
テ: ディグダ,ガーディ,フーディン,イシツブテ,プテラ,デリバード,ダーテング,ハリテヤマ,ハンテール,ラティアス,ラティオス,デオキシス,タテトプス,トリデプス,ディアルガ,ハーデリア,フシデ,デスカーン,テッシード, シャンデラ,テラキオン,ジガルデ,ディアンシー,ドデカバシ,デカグース,キテルグマ,テッカグヤ,ヤクデ,マルヤクデ,デスバーン,
ヤ: ギャロップ,ヤドン,ヤドラン,バリヤード,ギャラドス,ファイヤー,ヤドキング,ヤジロン,リーシャン,ツタージャ,ジャローダ,ヤブクロン,オシャマリ,ヤングース,ジャラコ,ジャランゴ,マーシャドー,
オ: ダグトリオ,オーダイル,オクタン,ルクシオ,リオル,ルカリオ,グライオン,バオップ,バオッキー,オタマロ, バスラオ,フリージオ,コバルオン,フォッコ,マフォクシー,オーロット,オンバット,アオガラス,イオルブ,フォクスライ,バリコオル,
ロ: ロコン,ゴローン,カイロス,クロバット,コロトック,ダンゴロ,ローブシン,プロトーガ,バッフロン,ランドロス,ハリマロン,ブリガロン,ゴロンダ,ブロスター,フクスロー,ドロバンコ,トロッゴン,
ア: ルギア,キルリア,アーマルド,ガブリアス,リーフィア,パルキア,ギガイアス,アバゴーラ,ギアル,アギルダー,アマルス,アシマリ,アブリー,アマカジ,
キ: マンキー,ゴーリキー,カイリキー,キングラー,ラッキー,コイキング,ブラッキー,キングドラ,バルキー,バンギラス,マスキッパ,キバゴ,
マ: マタドガス,イトマル,マリル,リングマ,マグカルゴ,フカマル,ダルマッカ,マーイーカ,マッシブーン,
タ: バタフリー,コラッタ,ラッタ,コダック,ゴルダック,ブースター,バクーダ,コータス,ダンバル,スカタンク, ドータクン,ダークライ,ダブラン,クイタラン,ゴリランダー,ストリンダー,
リ: プリン,プクリン,スリープ,スリーパー,ゴクリン,コリンク,
カ: ドガース,ガルーラ,カブト,グライガー,ラブカス,ドンカラス,スカンプー,ガバイト,カバルドン,ガントル, ズガドーン,
コ: コクーン,ゴルバット,コイル,ゴース,ゴースト,コドラ,フライゴン,ジバコイル,ドッコラー,ゴルーグ,コフ ーライ,
ハ: ズバット,パラス,ブーバー,バクフーン,ブーバーン,フーパ,
イ: ストライク,スイクン,
ツ: ズルッグ,
フ: クラブ,ブルー,グランブル,ドーブル,プラスル,
ラ: シードラ,ラルトス,ジーランス,グラードン,ランクルス,
シ:
ク:
ス:
ト:
ル:
ン:

このグループで以下のように木構造を作ります。

pokemon_dfs.png

まずヌのグループからポケモンをひとつ取り出します。次にセからまたひとつ取り出します。文字がかぶったらそれ以降の探索を打ち切ります。例えば上の図では、ヌメルゴンとトランセルを選択した場合、「ル」が重複しているため、これ以降どうやってもいろは歌は完成しません。これを繰り返して、いろは歌が成立するポケモン集合を列挙します。

ビット演算によるカナ重複検出

前節で、カナが重複した場合は探索を打ち切ると述べました。ではこのカナ重複をどうやって実現すれば良いでしょうか。
Goで素直に実装する場合、以下のようなコードになると思います。

func hasDuplicatedRune(word, otherWord string) bool {
    for _, r := range word {
        if strings.ContainsRune(otherWord, r) {
            return true
        }
    }
    return false
}

しかし、ビット演算を利用することでより高速な重複検出が実現できます。
まず、かな45文字それぞれについて1ビットを割り当てます。

スクリーンショット 2019-12-22 13.17.04.png

こうすると、各ポケモン名がどのカナを含んでいるかも(順序性は失われますが)45ビット列で表現できます。
例えば、ブーバーを表現するビット列は以下のようになります。

iroha.004.jpeg

このように表現されたポケモン名では、カナが重複しているかどうかは、両方で同じ位置のビットが立っているかどうかで判定できます。
例えばアが重複しているかどうかは、1ビット目が両方1になっているかどうかを見ればよいです。
そうすると、これを45ビット全体で判定するには、2つのビット列の論理積でビットが立っているかどうかを見ればよさそうです。

iroha.005.jpeg

これで高速にかな重複が検出できます。ではGoで実装してみましょう。
あるカタカナ一文字やポケモン名を表すのに45ビット必要なので、64ビットの符号なし整数型であるuint64を使うことにします。

type WordBits uint64 // あるポケモン名を表すビット列

カナの重複があるかどうかを返す関数は以下のようになります。

// 与えられたカナ集合のいずれかと重複するカナを持っているかどうかを返す
func (w WordBits) HasDuplicatedKatakana(otherWordBits WordBits) bool {
    return w&otherWordBits != 0
}

また、重複がなかった場合に、利用済みカナ一覧へ新たなポケモン名を追加するのも、論理和を計算するだけです。

iroha.006.jpeg

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=1max-p-depth=5とすると、深さ1(上の図では「セ」グループ)から深さ5までのグループは並列に、深さ0(上の図では「ヌ」グループ)と深さ6以降のグループでは直列に計算を行います。
これにより、ノード数に応じてgoroutineの数をある程度制御できます。

min-p-depth-0.png
min-p-depth-1.png

ちなみに、goroutineスケジューラには実行の順序保証がないので、このような並行処理を実装すると深さ/幅優先探索は行えなくなります。今回は途中で計算を打ち切りつつ全探索を行うので問題になりませんが、いろは歌が一つでも見つかればいいケースでは、並列化しない方が早く探索が終わる可能性もあります。

ノードごとの計算結果キャッシュ

前節でコアスケールできるようになったので、コアを積めば積むほど高速化できるはずです。
手元のMacBook Proは4コアですが、例えばGCPでは最大96コアのマシンを利用できます。が、やはりそれなりのお値段になります。
ところで、GCPには通常のVMの他にプリエンプティブルVMというものがあります。これは、通常VMに比べて料金が大幅に安い代わりに、突然終了させられる可能性があるマシンタイプです。
そこで、計算が途中で中断されても続きから再開できる仕組みを実装して、プリエンプティブルVM上で実行できるようにします。

基本的な戦略としては、選択したポケモンの組み合わせごとにいろは歌が何個完成したかをキャッシュしておきます。
例えば、ロコン+スリープを選んだ際に2個、ロコン+スリーパーで3個のいろは歌が生成できることが計算により分かったとします。
この結果を保存しておけば、次回以降このノードにたどり着いた時点でキャッシュをチェックし、存在すればそちらから結果を返すことにより計算を省略できます。
(実際には、最後に成立したいろは歌の内容を表示したいので、もう少し色々な情報をキャッシュしていますが、説明の簡略化のためにここでは割愛します)

iroha-cache (1).png

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を使い切っていて気持ちが良いですね。
このマシンもまさか全力でいろは歌を計算させられることになるとは思っていなかったでしょう。
スクリーンショット 2019-12-22 14.40.49.png

スクリーンショット 2019-12-22 14.42.38.png

約3分半で、5000枚のいらすとや画像タイトルから2687パターンのいろは歌を生成できることが確認できました。
これでポケモン次回作で登場ポケモン数が爆発的に増えても安心ですね。

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

GoでランダムなGif画像を生成する

この記事はtomowarkar ひとりAdvent Calendar 2019の22日目の記事です。

はじめに

GoでのGIF画像生成を調べてもあまり出てこなかったので書いてみます。

?実際の生成物
スクリーンショット 2019-12-22 16.04.44.png

本記事には関係ないですが、image.Imageからimage.Palettedに変換する方法って何かいい方法ありませんかね...

考えかた

  • Intのスライスを生成する
  • 数字に対応する色をマッピングする
  • マッピングした画像を複数作り、GIFにする

Int スライス

スクリーンショット 2019-12-22 16.04.44.png

マッピング

スクリーンショット 2019-12-22 16.04.44.png

コード

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

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

令和元年冬 犬は駆け廻り 猫は円くなり 私はCIに目を回す

0. 序文

本稿は TECOTEC Advent Calendar 2019 の21日目の記事です。

:raising_hand_tone1:「エンジニアがすなる技術ブログといふものを、私もしてみむとしてするなり。はてなブログ、君に決めた!」
:smirk_cat:「そういえば、小説の原稿をGit管理すると、編集点や更新履歴が共有できて、校正・編集が捗るって聞いたことがあるよ」
:angel:「Gitを使うなら……投稿まで自動化しては……どうでしょう……捗りマックスですよ……」
:thumbsup_tone1:「いいね!それ採用」

というわけでGitHub初心者が見果てぬ夢を追いかけて自動化の塔を目指すまでの物語です。

X. 本稿の内容を3行で

  • ブログ記事をGit管理して校正・改稿を可視化したく
  • Git使うなら投稿まで自動化できたらいいなとCircleCIに手を出し
  • 必要なパッケージがインストールできずに頓挫した

今のところ先生にもなれないしくじり話です、ご容赦ください。
同じような躓き方をしてしまった時に、少しでもご参考になれば幸いです。

1. 訓練

1-1. 調査

:speaking_head:「まずは冒険に必要な道具を確認しましょう」
:relieved:「『はてなブログ Git 管理』でググります」

このブログの内容を GitHub で管理するようにした - えいのうにっき

:speaking_head:「あなたは【blogsync】という道具が有用であることを知りました。はてなブログの投稿記事を取得する、新しい記事を作成する、既にある記事を更新する――以上のことができます。Go言語で書かれた、AtomPubのラッパーのようです」
:smirk:「グッド(Go触ったことないけど)。関連記事にもっと詳しい情報がないか調べます」

コード化したはてなブログリポジトリの更新を CircleCI 2.0 で自動化した - えいのうにっき

:speaking_head:「あなたは【CircleCI】でblogsyncを動かすという方策を見つけました。」
:innocent:「【blogsync】と【CircleCI】で自動化ね、完全に理解した(CircleCI使ったことないけど)」

1-2. 地図

:speaking_head:「それでは、冒険の地図を作りましょう」
:confused:「ブログを管理するGitHubリポジトリを作って、それをCircleCIに登録して、CircleCIの中ではblogsyncを動かして……」
:speaking_head:「blogsyncで《記事作成》と《記事更新》を行う場合、ファイル名を指定する必要があるようです」
:relaxed:「じゃあ、pull requestでのmasterブランチとの差分を取得して、それぞれのファイルについてpostpushをする感じで……できた!」

blogsync.png

1-3. 準備

:sunglasses:「まずは自分の家(ローカル)で【blogsync】を入手し、試してみたいと思います」
:speaking_head:「よいでしょう。構築環境を決めるので、1d6を振ってください……はい、あなたの環境はWindowsなので、【Chocolatey】を導入するところからです」

Windows10でChocolateyを使ってみた - Qiita

:yum:「チョコ美味しいなり。で、次はGoのインストール……$ choco install golangを唱えます」
:speaking_head:「では、2d6を振ってください」
:unamused:「えっ、成功判定? こんなところで失敗することなんてあるの……?」コロコロ……

2→ 9-1.に進む
3以上→ 1-4.に進む

1-4. 調達

:speaking_head:「Go環境を導入して、【blogsync】を入手する準備が整いました」
:smile:「唱えます。$ go get github.com/motemen/blogsync
:speaking_head:「2d6 + 《Go知識》ボーナスで12以上で成功です」
:sweat_smile:「高っ!? ボーナス0なんですけど。インストールするだけですよね……?」

11以下→ 9-2.に進む
12→ 1-5.に進む

1-5. 実践

:speaking_head:「【blogsync】を手に入れました」
:kissing_closed_eyes:「やったね!ではGitリポジトリを作成してルートを決め、configファイルを作ります」
:speaking_head:「成功です」
:relieved:「ここはあっさりなんですね」

motemen/blogsync - GitHub

.config/blogsync/config.yaml
[はてなブログのURL]:
    username: [はてなアカウントのID]
    password: [はてなブログの"詳細設定"の"AtomPub"にあるAPIキー]
    local_root: [Gitリポジトリをcloneしたディレクトリ]
    omit_domain: [local_rootの階層を深くしたくなければtrue]

:wink:「早速サンプルで用意したはてなブログを引っ張ってみます。$ 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

:speaking_head:「成功です。12/20に作成していた下書き状態の記事を.md形式で取得することができました」

2. 円環

2-1. 導入

:speaking_head:「では、自動化の塔に挑みましょう」
:sweat_smile:「展開が早い……いや実際にはここまでだいぶ死に戻りを繰り返した気がするけれども……。まぁローカルで成功したことですし、同じようにすればできるでしょ。【CircleCI】をGitHubアカウントで利用開始、リポジトリを登録します」
:speaking_head:「成功です」
:expressionless:「さすがにそれはね……。では、調査で獲得した資料を参照して、.circleci/config.yamlを作成し、masterブランチにpull requestを送ります」
:speaking_head:.yamlファイルの作成でよいですね?」

YES→ 9-4.に進む
NO→ 2-2.に進む

2-2. 調整

:fearful:「間違えました、説明書をよく読むと.ymlと書いてあるので、.circleci/config.ymlを作成します。記述も先の資料をお手本にしますが、ブログ名称などもCircleCIの環境変数で定義するようにします。また、blogsyncを入れた後は、とりあえず$ blogsync pullが動くか試してみようと思います」

.circleci/config.yml
version: 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. 死闘

:cold_sweat:「……importしているパッケージをすべて$ go getすれば……いいんですかね……?」

.circleci/config.yml
version: 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を使ったインストールに失敗する

:speaking_head::skull:ファンブル:skull:です」

【現象】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を入れるところから始めましょう。

Powershellでvimを使う - Qiita

解決したら1-3.に戻ります。

9-2. blogsyncのインストールに失敗する(cli)

:speaking_head:「失敗です」

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する

Cannot get package - GitHub

Windowsの場合は、$ go build -o blogsync.exeと、実行ファイルであることを明示して作成する必要があります。
blogsyncのパッケージ化に成功したら、1-5.に進みます。

9-3. blogsyncのインストールに失敗する(パッケージ不足)

:speaking_head:「失敗です」
:worried:「わけがわからないよ……」

$ #!/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ファイルが読み込めない

:speaking_head:「失敗です」

$ #!/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)

:speaking_head:「失敗です」
:hugging:「んに"ゃあああああああ! お手上げです _(: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. おお ○○○○よ

:speaking_head:しんでしまうとは なさけない…。
:head_bandage:「GoとCircleCIの知識を経験点として、次のキャンペーンに進んでいいですか……」

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

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 のアドレスを代入しているので、同じアドレスとなります。

The Go Playground

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 つ追加したため、len は 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)
    }()
}

The Go Playground

最後に

毎月開催されている Women Who Go Tokyo ですが、色んな人に Go を楽しんでもらいたいと思っています。
こちらの記事 Women Who Go Tokyo という取り組みでわたしたちは。|micchie|note でも紹介しているので、興味のある方はぜひ、覗いてみてください。

※ 本当はレシーバの話も書きたかった…間に合わなかった…

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

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-101

Docker上でアプリケーションをビルド・実行してみよう

まず始めに、作成したアプリケーションを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.4MB

21.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-101

BioRxivGo
Code Chrysalisの多言語週間中に作成したフルスタックアプリケーション(Vue, Golang, PostgreSQLなど)になります。こちらのアプリケーションではマルチステージビルドに加えて、Docker composeでアプリケーションを起動しています。
https://github.com/Imamachi-n/BioRxivGo

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

【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を使用している皆さんにつきましては適時読み替えて頂きますようにお願いします。

目次

本ハンズオンでは下記のような流れでWebアプリケーションの公開〜自動デプロイの構築までを行っていきます。

0. 事前準備
1. VPCを構築しよう!
2. EC2インスタンスを起動しよう!
3. Webアプリをデプロイしよう!

0. 事前準備

本ハンズオンでは私が事前に作っておいたアプリケーションを使用します。
下記の手順に従ってアプリケーションをダウンロード&動作確認してみましょう!

GitHubリポジトリをforkしてこよう!

下記のGitHubリポジトリから自分のリポジトリへforkしてきましょう!

▼まずはアクセス▼
https://github.com/Takumaron/AWS_tutorial

スクリーンショット_2019-12-21_14_28_09.png

フォーク先を指定して、きちんとフォークできていれば下記のような画面になります。(Qiita-test-aws-tutorialの部分は皆さんのGitHubアカウント名になります。)

スクリーンショット 2019-12-21 14.26.38.png

続いて、Forkしてきたリポジトリを自分のPCにクローンしてきます。

スクリーンショット_2019-12-21_14_31_25.png

ターミナルを開いて下記のコマンドを入力してください。

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     ... done

Dockerコンテナが正常に立ち上がったら、お好みのWebブラウザを開いて http://localhost:3000/hello へアクセスしてください。
下記のような画面が表示されていれば、OKです!お疲れ様でした。

スクリーンショット 2019-12-22 9.01.53.png

1. VPCを構築しよう!

さっそく、先ほどローカル上で動作確認したアプリケーションをAWSに乗せていきます!

まずはVPCというサービスを使って、グローバルなWebの世界に自分の領域を作っていきます。下記の手順に従って一緒に設定していきましょう!

まずは、AWSマネジメントコンソールへサインインします。

https://console.aws.amazon.com/console/home?region=us-east-1

サインインが完了したら、左上の「サービス」をクリックして下記の画像のようにVPCを検索します。

スクリーンショット_2019-12-22_9_06_50.png

このような画面は開けましたか?

スクリーンショット_2019-12-22_9_12_41.png

今回構築するVPCの全体像

Untitled Diagram.png

出た!訳のわからない図!!って言って諦めないでくださいw
システム構成図も理解できればそんなに大したものではありません。

まず押さえていただきたいポイントは、VPCのなかにパブリックサブネットっていう奴があって、その中にEC2っていうものが動いているというざっくりしたイメージです。

分かりやすい例えがないかなと一生懸命考えて思いついた「家の例え」でこの構成を説明していきます。

まずはこの対応表を頭にいれてください。

サービス名 家に例えると?
VPC ? 家
インターネットゲートウェイ ? 玄関
サブネット ? 部屋
EC2 ?‍? 住人

イメージとしては、

家(VPC)に外部から人がやってきたときには、玄関(インターネットゲートウェイ)を通して家に入ります。そして、部屋(サブネット)へ案内をして、住人(EC2)がおもてなしします。

という一連の流れで理解するとそれぞれの役割が明確になるのではないでしょうか?

それぞれのサービスの役割が理解できたところでハンズオンに戻っていきましょう!

VPCを作成する

スクリーンショット_2019-12-22_9_54_49.png

スクリーンショット_2019-12-22_10_03_58.png

サブネットを作成する

スクリーンショット_2019-12-22_10_10_46.png

スクリーンショット_2019-12-22_10_14_24.png

インターネットゲートウェイを作成する

スクリーンショット_2019-12-22_10_22_04.png

スクリーンショット_2019-12-22_10_24_51.png

スクリーンショット_2019-12-22_10_26_43.png

スクリーンショット_2019-12-22_10_31_47.png

ルートテーブルを編集する

サブネット(部屋)やインターネットゲートウェイ(玄関)がバラバラに存在している状態なので、それぞれの対応づけをしていきます。
スクリーンショット_2019-12-22_10_38_45.png

スクリーンショット_2019-12-22_10_45_37.png

スクリーンショット_2019-12-22_10_48_08.png

スクリーンショット_2019-12-22_10_50_49.png

スクリーンショット_2019-12-22_10_53_16.png

VPCの構築はここまでで完了となります!お疲れ様でした!
次は、VPCの中で動作するEC2インスタンスを作成していきます。

2. EC2インスタンスを起動しよう!

Webアプリケーションを動かすための場所作りは1. VPCを構築しよう!にて完了したので、実際に作ったWebアプリケーションを動かしてくれるマシーンを構築していきましょう!

【補足】
EC2インスタンスとは、仮想サーバーのようなものです。
AWSが事前に用意しているOSや独自で作成したOSイメージを使ってサーバを立てることができます。
今回は無料枠で利用できるAmazon Linux 2 AMIを利用します。

スクリーンショット_2019-12-22_11_08_31.png

スクリーンショット_2019-12-22_11_12_01.png

スクリーンショット_2019-12-22_11_14_05.png

スクリーンショット_2019-12-22_11_15_48.png

【変更点】のみ入力内容を修正して「次のステップ」へ

スクリーンショット_2019-12-22_11_21_08.png

スクリーンショット_2019-12-22_11_23_53.png

スクリーンショット_2019-12-22_11_25_25.png

スクリーンショット_2019-12-22_11_29_29.png

スクリーンショット_2019-12-22_11_34_29.png

スクリーンショット_2019-12-22_11_38_37.png

キーペアは後ほどEC2にローカルからつなぎに行くときに必要になるファイルですので、ダウンロード後も無くさないようにしてくださいね!

ダウンロードが完了したら、「インスタンスの作成」をクリックすればEC2インスタンスの作成が始まります。(約2分ほど待てば下記のようにインスタンスの状態が「running」に切り替わると思います。)

スクリーンショット_2019-12-22_11_48_13.png

お疲れ様でした!
ここまででVPCの構築と、EC2インスタンスの起動まで終わりました!

あとは、EC2インスタンス上でWebアプリケーションを動かすだけです。もうちょっとなので一緒に頑張りましょう?

3. Webアプリをデプロイしよう!

さぁ、あとは自作したWebアプリケーションをEC2上で動かすだけですね!

Webアプリのデプロイは下記のような手順で行なっていきます。

  1. EC2インスタンスにssh接続する
  2. GitコマンドとDocker・Docker-composeコマンドが使用できるようにパッケージをインストール
  3. GitHubからプロジェクトをクローンしてくる
  4. アプリケーションを立ち上げる

それでは、さっそく始めましょう〜!

EC2インスタンスにSSH接続しよう!

まずは、作成したEC2インスタンスにSSHで接続しましょう!

自分のEC2インスタンスのIPアドレスを知っておく必要があるので、下記の画像を参考にして、IPアドレスをコピーしておきます。

スクリーンショット_2019-12-22_11_48_13.png

それが完了したら、ターミナルを起動して下記のようなコマンドを打ちます。

# ダウンロードした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接続を切断する場合は、下記のコマンドを打ちます。

$ exit

Gitと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 --version

GitHubからプロジェクトをクローンしてこよう!

DockerとGitが使えるようになったので、GitHubからアプリケーションをダウンロードしてきましょう!

https://github.com/[あなたのGitHubID]/AWS_tutorial へ遷移しましょう。

「Clone with HTTPS」モードでクローンURLをコピーします。

スクリーンショット_2019-12-22_12_37_15.png

コピーしたクローン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/
スクリーンショット 2019-12-22 12.56.23.png

動いているみたいですね!
バックエンドとの連携もできているでしょうか?

http://EC2インスタンスのIPアドレス:3000/hello
スクリーンショット 2019-12-22 12.56.31.png

できているみたいですね!!

バックエンドAPIに対してブラウザからはアクセスできないことも確認しておきましょう。

http://EC2インスタンスのIPアドレス:8080/ping
スクリーンショット 2019-12-22 12.59.46.png

成功です!!お疲れ様でした:tada:
ハンズオンはここまでとなります。

【発展】
バックエンド側(:8080)にアクセスできない理由は分かりますか?
余裕のある方は、発展としてなぜアクセスできないのか考えてみてください!(ヒントは、セキュリティグループです!)


いかがだったでしょうか?
かなり長いハンズオンになってしまいましたが、ここまで読んでいただきましてありがとうございました!

今回はVPCとEC2を使って簡単なWebアプリケーションをデプロイしました。

次はRDSというサービスを使ってDB機能を持たせたり、Code Buildというサービスを使って自動デプロイを実現できるようなハンズオンを作りたいと思います!

明日のCA Tech Dojo/Challenge/JOB Advent Calendar 2019@hmarfさんが担当です!引き続きお楽しみください!!

▼こちらも要チェック!!

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

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 n

make は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.

すっきり。

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

Go言語の依存モジュール管理ツール Modules の使い方

Modules を使うには

Modules は、Go言語 1.11 以上のバージョンである必要があります。
1.11 以上をインストールすると、go mod コマンドが使えるようになります。

Modules を使う流れ

Modules を使う流れは以下のとおりです。

  1. go mod init で、初期化する
  2. go buildなどのビルドコマンドで、依存モジュールを自動インストールする
  3. go list -m all で、現在の依存モジュールを表示する
  4. go get で、依存モジュールの追加やバージョンアップを行う
  5. 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.12

2. 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.2

3. 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/Modules

Go & Versioning
https://research.swtch.com/vgo

Using Go Modules
https://blog.golang.org/using-go-modules

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

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 はもうちょい勉強せなあかんなぁ。

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

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.git

https方式

手軽なのはhttps方式です。
以下の権限を持つパーソナルアクセストークンを作成し、secretsに登録してワークフロー内でwithオプションに渡してあげるだけで済みます。
※パーソナルアクセストークンはhttps方式しか使えません1

.github/workflows/sample.yml
      - name: submodule
        uses: actions/checkout@v1
        with:
          submodules: true
          token: ${{ secrets.PERSONAL_TOKEN }}
  • メリット 手軽さ
  • デメリット パーソナルアクセストークンは発行したアカウントがアクセスできるリポジトリ全てに権限が及ぶのでトークンが漏れた場合のセキリュティリスクが大きい。Bot用のアカウントを作成やトークン作成の依頼・共有など運用がしんどい。

ssh方式

  1. ssh-keygenコマンドで秘密鍵公開鍵を作成
  2. submodule先のリポジトリのDeploy Keysに公開鍵を登録(必要に応じてwrite権限を付与)
  3. 取り込む方のリポジトリのsecretsに秘密鍵を登録
  4. ワークフローで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方式がオススメです。

注釈

注釈


  1. GithubActionsに移行する前は、AWSのCodeBuildを使用していました(GithubAcitonsの方がめちゃくちゃ早いです)。その時は秘密鍵をSystem Managerで用意してssh方式をしていましたが、途中でhttps方式のsubmoduleオプションが登場しました。 

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

一般的な方法で犬の年齢を人間換算したらそんな歳じゃないワン!!って怒られそうなので正しい年齢を算出してみた

今までワンコの年齢を人の年齢換算するときは、犬の年齢を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を使って年齢をプロットしてみました。
青いほうが今回の計算式のものです
output.png

go-chart
https://github.com/wcharczuk/go-chart

go-chart使い方わかると便利です。
例見てもよくわからなかったので試行錯誤が大変でしたが...

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

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 goplantuml

goplantuml でクラス定義ファイルを出力

goplantuml コマンドで、対象プロジェクトのディレクトリを指定する。
-recursive をつけると、ディレクトリ以下を再帰的に解析する。

$ goplantuml -recursive ./parser > /tmp/parser.puml

そのまま実行すると標準出力に出力されるので、ここでは /tmp/parser.puml というファイルに出力されるようにした。

PlantUML でクラス図を生成する

$ plantuml /tmp/parser.puml

出力した定義ファイルと同一ディレクトリに、拡張子が .png のクラス図画像ファイルが出力される。
(ここでは /tmp/parser.png)

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)

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

日本の緯度経度の情報を全取得するダウンローダを作った

Google Mapを頼らずに住所から位置情報を自力で取得できないか調べてみたら簡単にできそうだった。
国土交通省国土政策局が公開しているので、そこから取得したらよさそうです。

GIS
http://nlftp.mlit.go.jp/cgi-bin/isj/dls/_choose_method.cgi

こんな感じ

スクリーンショット 2019-12-22 2.05.44.png

面倒そうなので、ダウンローダを作ってみた。
上記のスクリーンショットの版数(赤枠のところ)だけ覚えておいてください

以下で、全国の位置情報をダウンロードできるようにしました。

go run main.go 版数 保存先

コードは以下の通り。
ref. https://github.com/usk81/til/tree/master/go/download-japan-gis-data

package 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)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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も更にモジュール管理がカッチリしたものになりそうな片鱗が見て取れますね。
またリリース後に検証して行きたいと思います。

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