- 投稿日:2020-01-03T23:06:28+09:00
【Go】データベース操作
Goでデータベースを操作(CREATE・SELECT・UPDATE・DELETE・SELECT)する記述をメモします。
CREATE文
func main() { //DB接続 DbConnection, _ := sql.Open("sqlite3", "./example.sql") defer DbConnection.Close() //CREATE文 createCmd := `CREATE TABLE IF NOT EXISTS person( name STRING, age INT )` _, err := DbConnection.Exec(createCmd) if err != nil { log.Fatalln(err) } }INSERT文
insertCmd := "INSERT INTO person (name, age) VALUES (?, ?)" _, err = DbConnection.Exec(insertCmd, "John", 20) if err != nil { log.Fatalln(err) }UPDATE文
updateCmd := "UPDATE person SET age = ? WHERE name = ?" _, err = DbConnection.Exec(updateCmd, 30, "John") if err != nil { log.Fatalln(err) }DELETE文
deleteCmd := "DELETE FROM person WHERE name = ?" _, err = DbConnection.Exec(deleteCmd, "John") if err != nil { log.Fatalln(err) }SELECT文(複数行)
var DbConnection *sql.DB type Person struct { Name string Age string } func main() { //DB接続 DbConnection, _ := sql.Open("sqlite3", "./example.sql") defer DbConnection.Close() //この場合はテーブル名が固定の場合のSELECT文 selectCmd := "SELECT * FROM person" //この場合はテーブル名を指定する場合のSELECT文 tableName := "person" selectCmd := fmt.Sprintf("SELECT * FROM %s", tableName) //DBのデータを取得してStructに入れる rows, _ := DbConnection.Query(selectCmd) defer rows.Close() var people []Person for rows.Next() { var p Person err := rows.Scan(&p.Name, &p.Age) if err != nil { log.Fatalln(err) } people = append(people, p) } //Structから取り出して表示 for _, p := range people { fmt.Println(p.Name, p.Age) } }SELECT文(一行のみ)
var DbConnection *sql.DB type Person struct { Name string Age string } func main() { //DB接続 DbConnection, _ := sql.Open("sqlite3", "./example.sql") defer DbConnection.Close() //SELECT文 oneSelectCmd := "SELECT * FROM person WHERE name = ?" row := DbConnection.QueryRow(oneSelectCmd, "John") var p Person err = row.Scan(&p.Name, &p.Age) if err != nil { if err == sql.ErrNoRows { log.Fatalln("No row") } else { log.Fatalln(err) } log.Fatalln(err) } //1行表示 fmt.Println(p.Name, p.Age) }
- 投稿日:2020-01-03T21:58:52+09:00
Go で ipc package を書いてみた
あけましておめでとうございます。
昨年の末ごろから、なんとなーくプロセス間のソケット受け渡しを Go でやってみたいなーと思って少しずつコードを書いていたんですが、年末年始の時間を使って書き上げたので、ここで報告します。
軽い気持ちで書き始めたものの、なかなかハマることが多くて結構な時間がかかってしまいました。。。
成果物
https://github.com/navel3/go-ipc
https://godoc.org/github.com/navel3/go-ipcMotivation
- TCP接続をプロセス間で授受することで、コネクションレスなリバースプロキシ的なものができるのではないかと思った
- Go だけで低レイヤーのコードを書いてみたかった
- テストからドキュメントまで Go Way に従って書く題材が欲しかった
実装
以下、実装について説明していきます。
プロセス間通信
Linux, Windows 共に名前付きパイプで通信してます。
Linux
普通に
net.UnixConnを使ってます。特筆すべきこともないので詳細は割愛。Windows
Microsoft が公開している microsoft/go-winioを使いました。
natefinch/npipe というのもありますが、2016年から更新が停止しています。microsoft/go-winio の README によれば、これから発想を得て実装したとのこと。プロセス間のTCP接続の授受
パイプ経由でソケット情報の受け渡しをしています。ここで付加情報として
peeked []byteを渡すことで、通信を先読みした情報を、ソケット受領側で再現できるようにしています。当初、recv(2)にはMSG_PEEKがあるので、この機能は不要かと思っていましたが、go ではシステムコールを直に叩かない限り peek できないため実装しました。
また、Microsoft サポートによれば peek は色々問題を引き起こすので実行するなとの記事も。(ページ消えちゃってますが)[INFO] Winsock ではデータのピークは実行しない
Linux
net.TCPConn.SyscallConn()でファイルディスクリプタを取り出し、システムコールsendmsg(2)を叩きます。net.TCPConn.File().Fd()で取り出すことも可能ですが、これはdup(2)により実装されており、ディスクリプタが複製されてしまうので SyscallConn を使います。rawConn, err := conn.(*net.UnixConn).SyscallConn() if err != nil { return } gw.wbuf.Reset() writeWithLength(&gw.wbuf, func(b *bytes.Buffer) error { return s.serialize(b) }) var n int rawConn.Control(func(connFd uintptr) { rights := unix.UnixRights(fd) for { n, err = unix.SendmsgN(int(connFd), gw.wbuf.Bytes(), rights, nil, 0) if err == nil || err != unix.EAGAIN { break } // TODO: There is no way to get write deadline of conn if ok, _ := waitIOEvent(waitWrite, fd, waitForever); !ok { break } }Go で作られた Socket は non-blocking に設定されているため、EAGAIN が発生する可能性があります。
とりあえずselect(2)で wait 実装しました。しかし、これでは待っている間 goroutine がスレッドを占有してしまうという問題があります。runtime では定期的に epoll で監視する実装になっているようなので、参考にして今後改善したいポイントです。
- 参考にさせていただいた記事: Golangのスケジューラあたりの話
- runtime/proc.go
- runtime/netpoll_epoll.go
func waitIOEvent(mode, fd int, timeout *unix.Timeval) (bool, error) { fds := &unix.FdSet{} fds.Set(fd) var n int var err error if mode == waitWrite { n, err = unix.Select(fd+1, nil, fds, nil, timeout) } else { n, err = unix.Select(fd+1, fds, nil, nil, timeout) } if err != nil { return false, err } return n == 1, nil }受信側でも同様にディスクリプタを取り出して
recvmsg(2)で受領します。rawConn, err := conn.(*net.UnixConn).SyscallConn() if err != nil { return } rights := unix.UnixRights(0) var dlen uint32 rawConn.Control(func(connFd uintptr) { var buf [4]byte for { n, _, _, _, err := syscall.Recvmsg(int(connFd), buf[:], rights, 0) if err == nil { if n != 4 { panic(fmt.Sprintf("n must be 4 but was %v", n)) } break } if err != unix.EAGAIN { break } // TODO: There is no way to get read deadline of conn if ok, _ := waitIOEvent(waitRead, fd, waitForever); !ok { break } } dlen = binary.BigEndian.Uint32(buf[:]) })Windows
Linux と同じく
net.TCPConn.SyscallConn()でファイルディスクリプタを取り出します。これをシステムコールWSADuplicateSocketで通信相手のプロセスにハンドルを複製します。WSADuplicateSocketは標準packageでは提供されていないため、自前で定義しました。(Go の標準パッケージにないシステムコールを使う)Pipe への書き込みは microsoft/go-winio のレイヤーを使っているので、Linux のようなタイムアウトの考慮は不要です。
rawSock, err := sock.SyscallConn() if err != nil { return } rawSock.Control(func(fd uintptr) { sd := socketData{ laddr: *sock.LocalAddr().(*net.TCPAddr), raddr: *sock.RemoteAddr().(*net.TCPAddr), peeked: peeked, withData: len(msg) > 0, } err = gw.sendImpl(conn, msg, func() (s serializer, err error) { err = winsys.WSADuplicateSocket(windows.Handle(fd), uint32(gw.pid), &sd.ProtocolInfo) if err != nil { return } return &sd, nil }) })受信側では、受け取った情報から
WSASocket(これも標準にないので自前定義) でソケットを作ります。ソケットはWSAIoctlで non-blocking にしておきます。後述しますが、ここも要改善ポイント。
err = gw.receiveImpl(conn, &sd) if err != nil { return } fd, err := winsys.WSASocket(winsys.FROM_PROTOCOL_INFO, winsys.FROM_PROTOCOL_INFO, winsys.FROM_PROTOCOL_INFO, &sd.ProtocolInfo, 0, 0) if err != nil { return } // set non-blocking mode to enable deadline const finbio = uint32(0x8004667e) on := uint32(1) var retsize uint32 err = windows.WSAIoctl(fd, finbio, (*byte)(unsafe.Pointer(&on)), 4, nil, 0, &retsize, nil, 0) if err != nil { return }独自 TCPConn 実装
受領したディスクリプタを wrap して
net.Connとして扱えるようにしてます。
送信側で先読みしたデータをpeeked []byteとして保持しておき、初回読み込み時はその内容を返すようにしました。peeked を全て読んだ後は、ソケットにデータがあるならば読み込みます。ここに問題があります。続く。
func (c *tcpConn) Read(b []byte) (n int, err error) { if len(c.peeked) > 0 { n = copy(b, c.peeked) c.peeked = c.peeked[n:] if yes, _ := c.sysSocket.isReadableState(); !yes { return } } b = b[n:] if len(b) == 0 { return } nn, err := c.sysSocket.read(b) runtime.KeepAlive(c) n += nn return }前項で触れた通り、ソケットの write or read が EAGAIN or EWOULDBLOCK を返す(=ソケットバッファが一杯or空)ケースへの対応として、システムコールで wait するよう実装したのですが、これだと、そのシステムコールを実行した goroutine がスレッドを占有してしまいうという問題があります。試していないですが、runtime.GOMAXPROCS の TCPConn がIO待ちになると、プログラムがハングすると思います。
microsoft/go-winio は Overlapped I/O を使ってこの辺をうまく制御しているようなので、今後改善していきたいなと思います。
他プロセスとのファイルハンドルの授受
TCPConn の実装したついでにファイルハンドルの授受も実装してみました。
こちらはos.NewFile()でファイルディスクリプタからos.Fileを作れるので簡単。Linux は socket の実装とほぼ同じ。Windows は
OpenProcess()でプロセスハンドルを取得して、DuplicateHandle()で複製。Benchmark
次のような単純な通信を、PIPEを介して接続を授受するパターンとしないパターンで性能測定をしてみました。
- TCP接続
- HTTP GET を書き出す
- 200 OK を応答する
- 接続を閉じる
測定環境
- CPU: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
- MEM: 16GB
- OS
Linux: Archlinux(Linux x1carbon 4.19.91-1-lts #1 SMP Sat, 21 Dec 2019 16:34:46 +0000 x86_64 GNU/Linux)
Windows: Windows 10 Home実測
まず Linux。
$ go test -bench 'TCP' goos: linux goarch: amd64 pkg: github.com/navel3/go-ipc BenchmarkTCPDirect-8 18794 60392 ns/op BenchmarkTCPIPC-8 10000 111099 ns/op PASS ok github.com/navel3/go-ipc 2.949s次に Windows。
$ go test -bench 'TCP' goos: windows goarch: amd64 pkg: github.com/navel3/go-ipc BenchmarkTCPDirect-8 5229 214000 ns/op BenchmarkTCPIPC-8 3250 368860 ns/op PASS ok github.com/navel3/go-ipc 2.657s圧倒的に Linux が速いですが、どちらも直使用に対して半分弱の性能落ち込み。想定していた程は遅くなりまりませんでした。
ただ、このベンチマークでは同一プロセス内で実行しているため、異プロセス間で通信する場合、異なる結果になるかもしれません。最後に
SyscallConn を使えばC言語感覚でシステムコール使えるかなーと簡単に考えていたんですが、syscall 系の package のドキュメントがまるで書かれていなかったり、非同期IOの実装の難しさなど、なかなか骨が折れる題材でした。
runtime の I/O polling 周りの仕組みをユーザが使えたり、
os.Fileと同じようにnet.TCPConnもファイルディスクリプタから作ることができれば、もう少し楽に書けるのになーと思います。
正直、車輪の再発明をしている感がまだまだ課題が残っていますが、いろいろ勉強にはなるので、今後も時間をみて修正してきたいと思います。
- 投稿日:2020-01-03T20:57:50+09:00
GoにおけるSqlite3の利用方法
Sqlite3のインストール
Windowsならば下記サイトよりSqliteをダウンロードします。
https://www.sqlite.org/download.html
「sqlite-tools-win32-x86-3300100.zip」をダウンロードしてインストールします。Macならば、下記コマンドを実行してください。
brew install sqlitegccのインストール方法
sqliteはC言語で開発されているため、gccが必要です。
Windowsの場合、下記よりTDM-gccをダウンロードしてインストールします。
http://tdm-gcc.tdragon.net/downloadMacならば、Xcodeをダウンロードしてインストール後、下記コマンドを実行してください。
xcode-select --installこれでgccが利用できるようになります。
go-sqlite3を取得する
コマンドラインで次を実行します。
go get github.com/mattn/go-sqlite3
インストールするとGoでSqliteが利用できるようになります。Goでsqliteを利用する
package main import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) var DbConnection *sql.DB type Person struct { Name string Age string } func main() { DbConnection, _ := sql.Open("sqlite3", "./example.sql") //接続開始(example.sqlに保存する) defer DbConnection.Close() //最後は確実にクローズする。 //この下に、CREATE文 SELECT文 INSERT文 UPDATE文 DELETE文を記載する }
- 投稿日:2020-01-03T20:57:50+09:00
【Go】Sqlite3の利用方法
1. Sqlite3のインストール
Windowsならば下記サイトよりSqliteをダウンロードします。
https://www.sqlite.org/download.html
「sqlite-tools-win32-x86-3300100.zip」をダウンロードしてインストールします。Macならば、下記コマンドを実行してください。
brew install sqlite2. gccのインストール方法
sqliteはC言語で開発されているため、gccが必要です。
Windowsの場合、下記よりTDM-gccをダウンロードしてインストールします。
http://tdm-gcc.tdragon.net/downloadMacならば、Xcodeをダウンロードしてインストール後、下記コマンドを実行してください。
xcode-select --installこれでgccが利用できるようになります。
3. go-sqlite3を取得する
コマンドラインで次を実行します。
go get github.com/mattn/go-sqlite3
インストールするとGoでSqliteが利用できるようになります。4. Goでsqliteを利用する
package main import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) var DbConnection *sql.DB func main() { DbConnection, _ := sql.Open("sqlite3", "./example.sql") //接続開始(example.sqlに保存する) defer DbConnection.Close() //最後は確実にクローズする。 //この下に、CREATE文 SELECT文 INSERT文 UPDATE文 DELETE文を記載する }
- 投稿日:2020-01-03T18:01:20+09:00
[Go]ポインタをゼロから理解する
対象読者
Go言語の文法を学んでる段階の方から、*とか&を使うケースは覚えたけど何となく使ってしまってる方まで(ポインタ渡しについては触れてません)
はじめに
ポインタ変数を理解するに当たって、そもそも変数が定義された時どのようなことが行われているのかを理解する必要があります。そのために必要なメモリというものを冒頭で説明したいと思います。
メモリとは
PCのCPUが処理を行う際に読み書きを行う値(データ)を格納する場所のことです。メモリの最小単位は1バイトであり、1バイトごとにアドレスと呼ばれる識別番号が16進数で割振られています。
ここからはイメージを掴むために画像を入れながら説明します。16進数の適当なアドレスを用いて、メモリを表すとこのような形になります。
変数定義の流れ
では実際に変数が定義された際の流れを見ていきましょう。
Int型のaという変数を定義したとします。このaという変数が定義された時メモリは以下のようになります。このように変数の型やサイズに合わせてメモリの中のアドレスが確保されるのです。
* アドレスと変数のイメージを掴むことが目的なので、変数に対応した厳密な16進数ではありませんがご理解ください。
ポインタとは
ここまで変数定義された時の一連の流れを見てきました。
では本題のポインタの説明をします。ポインタを端的に言うと、「その変数の型情報や大きさを指し示したもの」です。
より具体的には、その変数が格納されてるメモリ領域の先頭アドレスを指します。この先頭アドレスに変数の型に関する情報や領域(大きさ)に関する情報が入っているのです。この図において変数aのポインタは75bd5b9ということになり、ここには型や領域に関する情報が入っています。
Goでの実装例
ではこれらのポインタをgoにおいてどのように扱うか見ていきましょう。
変数からポインタを出力する
簡単な実装例として、変数のポインタを出力してみます
①変数から直接取得する場合
main.gopackage main import "fmt" func main() { var a int fmt.Println(&a) // 出力結果 // 0xc000094000 }これは&で(アンパサド演算子)変数のポインタを出力したものです。
②ポインタ型を指定して参照する場合
ポインタ型変数には変数のアドレス値が入ります。
よって、先ほどの変数aのアドレス値をポインタ型変数bに代入すれ変数のポインタを出力できますmain.gopackage main import "fmt" func main() { var a int var b *int b = &a fmt.Println(b) // 出力結果 // 0xc000094000 }こちらは予め定義したポインタ型変数に①で定義した変数aのアドレスを代入したものになります。出力方法は違いますが、ここでの値は等しくなります。
ここで使った特殊記号は以下の2つです。
main.go* ポインタ型変数の定義 (例 var a *int var b *string & 変数のアドレスを取得③ポインタから変数の値を取得する場合
ポインタ型の指定と同じなので紛らわしいですが、以下のようにします。
main.gopackage main import "fmt" func main() { var a int var b *int b = &a c := *b fmt.Println(c) // 出力結果 // 0 }出力した変数cには何も値が入っていないので0が出力されました。
goの文法を理解する上で避けては通れないポインタですが、基本概念から落とし込み手を動かすことで徐々に慣れていきましょう
- 投稿日:2020-01-03T16:52:28+09:00
Ubuntu18.04にVSCodeのGo開発環境を作る
環境はUbuntu18.04
Extensions
.zshrc
go envで調べてPATH追加export GOPATH=$HOME/go export GOROOT=/usr/local/go export GOBIN=$GOROOT/bin export PATH=$PATH:$GOPATH export PATH=$PATH:$GOBINsetting.json
LanguageServerを有効にした
setting.json{ "go.useLanguageServer": true, "go.languageServerExperimentalFeatures": { "format": true, "autoComplete": true, "rename": true, "goToDefinition": true, "hover": true, "signatureHelp": true, "goToTypeDefinition": true, "goToImplementation": true, "documentSymbols": true, "workspaceSymbols": true, "findReferences": true, "diagnostics": true, "documentLink": true } }設定についてはREADMEにある
Document
gp1.13以降だと
godocは入ってないgo get golang.org/x/tools/cmd/godocこれで
godoc -http:8000が使えるようになる
- 投稿日:2020-01-03T16:35:45+09:00
tail -fをカスタマイズするツールを作ってみた
tail -fを実行すると、ログが延々と出力されますが、出力しつつ結果をカスタマイズするツールを作った。
- 時刻表示
ログの時刻表示形式がマチマチだったり、場合によっては時刻が出ない場合もある。1行ずつ時刻を表示させるようにしてみた。
追加でエポック時刻も表示させ、処理時間を算出しやすくしてみた。
- キーワードの色づけ
tailすると表示が流れてしまうので、色がつくようにしてみた。
実装
tailの実装は https://github.com/hpcloud/tail をインポートした。引数なしで実行するとこんな感じ。
NAME: decotail - decorated tail USAGE: decotail [global options] command [command options] [arguments...] VERSION: 1.0 COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: -t (default: false) -k value add color to text --help, -h show help (default: false) --version, -v print the version (default: false)
-tで時刻表示、-kで色づけするキーワードを指定する。("A B C")のようにスペース区切りで6キーワードまで色をつける。7キーワード目以降は色をつけない。
decotail -t -k "apple syslogd" /var/log/system.logを実行してみた。(色はわかりませんが...)2020-01-03 16:32:49.312 1578036769 Jan 3 00:03:46 kuritayunoiMac syslogd[124]: ASL Sender Statistics 2020-01-03 16:32:49.312 1578036769 Jan 3 00:04:21 kuritayunoiMac GoogleSoftwareUpdateDaemon[5585]: objc[5585]: Class KSPaths is implemented in both /Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Frameworks/DeviceManagement.framework/Versions/A/DeviceManagement (0x106578b48) and /Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon (0x106365e50). One of the two will be used. Which one is undefined. 2020-01-03 16:32:49.313 1578036769 Jan 3 00:04:21 kuritayunoiMac GoogleSoftwareUpdateDaemon[5585]: objc[5585]: Class KSBundle is implemented in both /Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Frameworks/DeviceManagement.framework/Versions/A/DeviceManagement (0x106578aa8) and /Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon (0x106366210). One of the two will be used. Which one is undefined. 2020-01-03 16:32:49.313 1578036769 Jan 3 00:04:22 kuritayunoiMac com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.pid.mdmclient.5592): Failed to bootstrap path: path = /usr/libexec/mdmclient, error = 108: Invalid path 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:20 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: Entered:_AMMuxedDeviceDisconnected, mux-device:8 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:20 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:8 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:20 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: tid:9e1b - Mux ID not found in mapping dictionary 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:20 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: tid:9e1b - Can't handle disconnect with invalid ecid 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:40 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: Entered:_AMMuxedDeviceDisconnected, mux-device:9 2020-01-03 16:32:49.313 1578036769 Jan 3 00:13:40 kuritayunoiMac AMPDeviceDiscoveryAgent[3028]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:9https://github.com/kuritayu/infra-tools にツールをおいてます。
- 投稿日:2020-01-03T15:58:37+09:00
【2020年1月】令和だし本格的にVSCodeのRemote Containerで、爆速の"開発コンテナ"始めよう
VSCode の Remote Conainer で"開発環境+プロジェクト全部入りのコンテナ"から開発をスタートダッシュをキメませんかッ!?
開発でVS Code の Remote Conainer使っていますか?単に既存のコンテナに入るだけなら Remote SSH でも構いませんが、"ローカル開発環境の一部"として、いやむしろローカルの開発環境=Remote Containerとして、ビンビンにRemote Container使っていきましょう。令和だし!
(すでに2年だけどね・・・?)特にMacを使っていると最初からPythonやらPHPやらRubyやらが入ってしまっているので開発環境があるのですが、これらは割とmacOSのエコシステムに組み込まれているので不要にパッケージの追加削除、できないのですよ。
brewとか意外とあっさり壊れますしね・・・。特にバージョンアップなんてもってのほかです。全然、余裕でおかしくなります。
そんなわけでMacに入っているPythonやRubyでプログラミングをバリバリしていると・・・ふと、後戻りできない状況になったりするわけです。
そんなことにならないためにも、"Remote Containerでの開発"に入門しましょう~!"Dev Container" 機能のご紹介
Remote Container の本家サイトと本家Githubで"Try a dev container"と言う項目とリポジトリがあるの、ご存知でしょうか?
"dev container"は開発環境入りコンテナが付属したプロジェクトのサンプルで、以下の各言語向けにdev-containerのサンプルが用意されています。
- Node.js, Javascript
- Python
- Go
- Java
- .Net Core
- PHP
- Rust
- C++
例えば、node.js、Javascript用のサンプルは以下のようなツリーになっております。
% git clone https://github.com/Microsoft/vscode-remote-try-node nodejs-dev-sample % cd nodejs-dev-sample % tree -a -I ".git" . ├── .devcontainer │ ├── Dockerfile │ └── devcontainer.json ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .vscode │ └── launch.json ├── LICENSE ├── README.md ├── package.json ├── server.js └── yarn.lock
treeコマンドで.gitだけ除外して全て表示すると上記のようになります。
ここで気になるのが・・・.devcontainerですよね?!
中身のDockerfileとdevcontainer.jsonは以下のようになっております。#------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- FROM node:10 # The node image includes a non-root user with sudo access. Use the "remoteUser" # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs # will be updated to match your local UID/GID (when using the dockerFile property). # See https://aka.ms/vscode-remote/containers/non-root-user for details. ARG USERNAME=node ARG USER_UID=1000 ARG USER_GID=$USER_UID # Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive # Configure apt and install packages RUN apt-get update \ && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ # # Verify git and needed tools are installed && apt-get -y install git iproute2 procps \ # # Remove outdated yarn from /opt and install via package # so it can be easily updated via apt-get upgrade yarn && rm -rf /opt/yarn-* \ && rm -f /usr/local/bin/yarn \ && rm -f /usr/local/bin/yarnpkg \ && apt-get install -y curl apt-transport-https lsb-release \ && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update \ && apt-get -y install --no-install-recommends yarn \ # # Install eslint globally && npm install -g eslint \ # # [Optional] Update a non-root user to UID/GID if needed. && if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ groupmod --gid $USER_GID $USERNAME \ && usermod --uid $USER_UID --gid $USER_GID $USERNAME \ && chown -R $USER_UID:$USER_GID /home/$USERNAME; \ fi \ # [Optional] Add add sudo support for non-root user && apt-get install -y sudo \ && echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ && chmod 0440 /etc/sudoers.d/$USERNAME \ # # Clean up && apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* # Switch back to dialog for any ad-hoc use of apt-get ENV DEBIAN_FRONTEND=dialog上記の解説は後に回して、続いて
devcontainer.jsonの中身は・・・devcontainer.json{ "name": "Node.js Sample", "dockerFile": "Dockerfile", // Use 'appPort' to create a container with published ports. If the port isn't working, be sure // your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost. "appPort": [3000], // Comment out the next line to run as root instead. "remoteUser": "node", // Use 'settings' to set *default* container specific settings.json values on container create. // You can edit these settings after create using File > Preferences > Settings > Remote. "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, // Specifies a command that should be run after the container has been created. "postCreateCommand": "yarn install", // Add the IDs of extensions you want installed when the container is created in the array below. "extensions": [ "dbaeumer.vscode-eslint" ] }と、コンテナの設定とvscodeのsettings.jsonの混ざったような形式のファイルとなっております。
この設定がなんなのかはだいたい、想像がつくかと思いますが、このフォルダをVSCodeで開くと何が起こるのでしょうか…試してみましょう!
その前に、Dockerデーモンが立ち上がってない方は事前にDockerデーモンを立ち上げてください。WindowsやMacの方はDocker for Desktopを起動しておいてください。Dockerが起動したらVSCodeを立ち上げて
nodejs-dev-sampleを開いてみます。
すると・・・
dev container configurationが見つかったからコンテナで開くか?と言う問い合わせが出ました!
そしてReopen in Containerをクリックすると・・・
しばらく時間が経って…開きました!左下のステータスバーがグリーンに変わってRemote接続中であることと、"Dev Container: Node.js Sample"の文字が眩しいですね!
そうなんです。.devcontainerフォルダとdevcontainer.jsonの設定と然るべきDockerfileが揃っていればプロジェクトフォルダ内のファイルを丸ごとマウントしたコンテナの自動生成とプロジェクトをVSCodeで開くのを勝手にやってくれるのです!また、ここでのDockerfileの特筆するべき点はdockerに問題の"root"ユーザー問題を宜しく解決してくれている点です。
"コンテナの中で開発する"のは聞こえはいいですが、大抵のイメージはそのまま実行するとユーザーが"root"になってしまうのが多いので、コンテナの中で更新されるファイルのオーナーが"root"になってしまってウザい問題がありました。自前でDockerfile書けばなんとでもなるのですがいちいち書いてられないし、いちいちDockerfile書くくらいなら開発環境汚れても別にいいじゃん会社のPCだし、みたいなことになってますよね!?
このDockerfileでは丁寧にそこのところをサポートしてくれているので、rootユーザー問題が無事に解決されています。ついでにコンテナの管理も VSCode からできるので…
このようにどのプロジェクトでどのコンテナ使っているかは、VSCodeから管理できます。
特にDockerではどのフォルダのDockerfileで立ち上げたコンテナかがわからない(と言うかイメージを作ってコンテナ起動するのでどこのフォルダのDockerfileで作成したイメージかどうかは本来は関係ないハズ)のでこれは便利です。どうやって使うのか?
まずは本家リポジトリのご紹介をしておきましょう。
リポジトリ名からどの言語向けのサンプルプロジェクトかわかると思います。
- microsoft/vscode-remote-try-python
- microsoft/vscode-remote-try-node
- microsoft/vscode-remote-try-go
- microsoft/vscode-remote-try-java
- microsoft/vscode-remote-try-cpp
- microsoft/vscode-remote-try-dotnetcore
- microsoft/vscode-remote-try-rust
- microsoft/vscode-remote-try-php
実際には
.devcontainerフォルダとdevcontainer.jsonの設定と使用されるDockerfileの3つが揃っていれば自動的にやってくれますので、これらのファイルのみを本家リポジトリからコピペで作成するのもアリです。
とは言え毎度コピペするのも面倒なので、以下のように手順を整えてしまいましょう。1. 雛形としてクローン
上記のリポジトリを以下のように"プロジェクトの名前"でクローンしてきます。
$ git clone https://github.com/microsoft/vscode-remote-try-php my-first-phpリポジトリ名の後ろの引数が作成されるフォルダ名で、別名でクローンするのが第1のミソです。
2. 履歴の削除と再作成
当然、cloneしたばかりでは本家リポジトリのコミット履歴を全て含んでおります。
このまま再利用しても全然、良いのですが・・・大抵の場合は気になるでしょう。
そこで.git以下をざっくり削除します。$ rm -rf .gitこれで過去のコミット履歴は綺麗さっぱり忘れました。
ついでに不要なファイルは削除しておきましょう。
なんなら.devcontainer以外は不要です。.vscodeはお好みにお任せいたします。で、お掃除が完了したところで新規のリポジトリとして初期化します。
$ git init ...これで新たな開発コンテナプロジェクトとして第一歩が始まりました!
3. Dockerfile のカスタマイズ
実際の案件ではDBやAngular、Aws、Firebase、AzureなどのCLIなどなどなどを入れる必要があったりするのでDockerfileがそのまま使えることはほぼないです。
よってDockerfileに予め必要なツールをインストールするコマンドを入れておきます。Dockerfileの記述に関してはここでは割愛させていただきます。。。
4. devcontainer.json のカスタマイズ
devcontainer.jsonではプロジェクト名や転送するポート、必要なVSCodeのプラグインなどの設定ができ、とても重要なファイルです。
設定値のリストは以下の箇所にあります。サンプルリポジトリの中で使われている設定をいくつか抜き出してみますと・・・
- settings … これはコンテナの中で使用される VSCodeのsetting.jsonの設定値となります。
- appPort … 転送するポートです
- postCreateCommand … コンテナ作成後に実行されるコマンド
- extensions … リモート側にインストールされるVSCodeプラグイン
あたりが重要なカスタマイズするポイントでしょうか。
また、リファレンスによると
docker-compose.ymlも利用可能な模様です。これらを設定後に、VSCodeからフォルダを開けば、即座に開発環境込みのプロジェクトのスタートです!
まとめ
VScode Remoteコンテナの機能の入門編を書いてみるにあたって、公式ドキュメントを見直しましたが…ボリュームが半端ないですね。
これをどこまで深堀りするべきか悩みましたが、入門編ということでほとんど触れないようにいたしました(笑)
本文中に飛び飛びでリンクを貼っていますが、Remote Containerの公式ヘルプは驚愕の1ページです。これだけで本書けそうなボリュームですよ・・・
というところで、今回はここまでといたします!
- 投稿日:2020-01-03T15:49:51+09:00
GoのS3 ListObjectsはPythonより20秒遅いのか?問題
はじめに
S3から大量Objectをダウンロードする場合、Objectサイズに関わらず中々速度が出ないですよね。
Pythonで書いている時も、concurrent.futuresなどで頑張ってたのですが、もしかしてGoroutineで出来るのでは?と思い、Golangデビューしてみました。やろうと思ったこと
- ListObjectV2を用いて、S3の特定Prefix配下のKeyをすべて取得
- 取得したKeyをGoroutineでいい感じにダウンロード
実際起こったこと
- ListObject部分を試しに書いてみたが、はっきり言って遅い。
- なんならPythonで書いた方が早い気が?
ん?API叩くだけなので同じ速度。だったらまだ納得は出来るが、スクリプト言語よりGoの方が遅いというのはちょっと気になる。
予定を急遽変更して、本件を少し検証してみました。ソースコード
Go版
main.gopackage main import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "os" ) func main() { bucket := os.Getenv("BUCKET") prefix := os.Getenv("PREFIX") region := os.Getenv("REGION") sess := session.Must(session.NewSession()) svc := s3.New(sess, &aws.Config{ Region: ®ion, }) params := &s3.ListObjectsV2Input{ Bucket: &bucket, Prefix: &prefix, } fmt.Println("Start:") err := svc.ListObjectsV2Pages(params, func(p *s3.ListObjectsV2Output, last bool) (shouldContinue bool) { for _, obj := range p.Contents { fmt.Println(*obj.Key) } return true }) fmt.Println("End:") if err != nil { fmt.Println(err.Error()) return } }Python版
こちらもサクッと書いてみる。
Goと条件を合わせる為、低レベルクライアントで。main.py#!/usr/bin/env python # -*- coding: utf-8 -*- import os import boto3 bucket = os.environ["BUCKET"] prefix = os.environ["PREFIX"] region = os.environ["REGION"] # r = boto3.resource('s3').Bucket(bucket).objects.filter(Prefix=prefix) # [print(r.key) for r in r] # 普段は上記の様に取得するが、Golangへ寄せるため下記のコードにて測定 s3_client = boto3.client('s3', region) contents = [] next_token = '' while True: if next_token == '': response = s3_client.list_objects_v2(Bucket=bucket, Prefix=prefix) else: response = s3_client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=next_token) contents.extend(response['Contents']) if 'NextContinuationToken' in response: next_token = response['NextContinuationToken'] else: break [print(r["Key"]) for r in contents]環境
サーバ等
- 基本的にCloud9 on EC2(t2.micro)で実行。
ビルド・デプロイ等
- 環境を汚したくない&面倒なので、全部Dockerで構築。
$ docker-compose up -d --build
- ちなみに構築資材は下記を参照。
DockerfileFROM golang:1.13.5-stretch as build RUN go get \ github.com/aws/aws-sdk-go/aws \ github.com/aws/aws-sdk-go/aws/session \ github.com/aws/aws-sdk-go/service/s3 COPY . /work WORKDIR /work RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main main.go FROM python:3.7.6-stretch as release RUN pip install boto3 COPY --from=build /work/main /usr/local/bin/main COPY --from=build /work/main.py /usr/local/bin/main.py WORKDIR /usr/local/bin/docker-compose.ymlversion: '3' services: app: build: context: . container_name: "app" tty: True environment: BUCKET: <Bucket> PREFIX: test/ REGION: ap-northeast-1S3バケット
東京リージョンにバケットを一つ作って、以下のツールで1000件程度作成。
#/bin/bash Bucket=<Bucket> Prefix="test" # テストファイル作成 dd if=/dev/zero of=testobj bs=1 count=30 # マスタファイルのコピー aws s3 cp testobj s3://${Bucket}/${Prefix}/testobj # マスタファイルを複製 for i in $(seq 0 9); do for k in $(seq 0 99); do aws s3 cp s3://${Bucket}/${Prefix}/testobj s3://${Bucket}/${Prefix}/${i}/${k}/${i}_${k}.obj done done測定
測定結果(1000 Object)
- Go
$ time docker-compose exec app ./main ~略~ real 0m21.888s user 0m0.580s sys 0m0.107s
- Python
$ time docker-compose exec app ./main.py ~略~ real 0m2.671s user 0m0.577s sys 0m0.104sGoがPythonより10倍遅い。なんでや!
Object数を増やしてみる
- もうすこしobjectを増やしてみましょう。とりあえず10000件あたりで。
#差分のみ for i in $(seq 0 99); do for k in $(seq 0 99); do
- ちなみにアップロード完了までに3、4時間かかりました。ツールはちゃんと作っとけばよかったね…
再測定結果(10000 Object)
- Go
$ time docker-compose exec app ./main ~略~ real 0m23.276s user 0m0.617s sys 0m0.128s
- Python
$ time docker-compose exec app ./main.py ~略~ real 0m5.973s user 0m0.576s sys 0m0.114s今回は4倍程度の差。
というよりObject数によらず18秒ほど差が出ている様子。うーむ。終わりに
- ライブラリの設定起因か、言語仕様の理解不足な気もしているので、もう少し情報を漁ってみたい。
- そもそもの目的であるGoroutineでの並列ダウンロード処理の効率が良ければ20秒程度は誤差な気もするので、残りも実装してみます。
気になるところ
- よく見るとuser, sysは同じ程度なのでS3でI/O周りが怪しい。
- goのコードを雑にprintデバッグしたところ("Start:", "End:")、list objectが処理時間の大半を占めている様子。もしやboto3とはS3設定のデフォルト値が異なるのだろうか。
- 同じコンテナで動かしてるので、T系インスタンスのCPUクレジット問題やNW帯域の差も関係ないと思うが……
- 前者はm5.largeに代えても解決しなかったので関係なさげ。
- 投稿日:2020-01-03T11:46:01+09:00
macOSにGoの開発環境をインストール(備忘録)
Goのインストール
Homebrewでgoをインストール
ターミナル$ homebrew install go $ go version go version go1.13.4 darwin/amd64ちゃんとhomegrew(自家醸造)して、Cellar(貯蔵庫)に入ったgoが使われていることを確認。
ターミナル$ which go /usr/local/bin/go $ ls -al /usr/local/bin/go lrwxr-xr-x 1 shizuku admin 26 11 17 19:23 /usr/local/bin/go -> ../Cellar/go/1.13.4/bin/goGOPATHとPATHの設定
- ホームディレクトリにgoディレクトリを作成してGOPATHに設定。
- PATHにGOPATH/binを追加。
下記の2行を.bash_profileに追加しておく。
.bash_profileexport GOPATH=$HOME/go export PATH=$PATH:$GOPATH/binVisual Studio Code (vscode) のインストール
vscodeのアンインストール
vscodeの環境をクリーンにしたかったので、すでにインストールされているvscodeと設定ファイルをアンインストール。
- vscodeの本体はFinderのApplicationフォルダからゴミ箱に移動
- vscodeの設定ファイルはターミナルで削除
ターミナル$ rm -rf ~/Library/Application\ Support/Code $ rm -rf ~/.vscodevscodeのインストール
vscodeの最新版をダウンロードしてインストール
https://code.visualstudio.com/Goプラグインのインストール
vscodeで"go"キーワードでプラグインを検索。Microsoft製のms-vscode.goを選択してインストール。
vscodeのコマンドパレットで"> Go: Install/Update Toolsを実行して、Goの各種ツールを全て選んでインストール。
Goのツールは、GOPATHに設定したディレクトリにインストールされる。
ターミナルls ~/go/bin/ bolt go-symbols goimports gore impl dlv gocode gomodifytags gorename kuzusi fillstruct godoc gopkgs gotests go-outline godoctor goplay guruここまでの設定でvscode上でGoの実行、デバッグが可能。
vscodeのキーマップ設定
キーマップ設置はデフォルのまま。
Macの場合は、デフォルトでControlキーを使ってEmacsライクなカーソル移動ができるので。
- 投稿日:2020-01-03T11:15:35+09:00
Go言語で画像(png, jpg, gif)処理 [ドット絵、リサイズ]
初めに
「Go言語で画像処理を書いたことないなー」と思い、ドット絵を作成する gpixart と画像をリサイズする grimg を作成したときに苦労したことを書きます。
gpixartは内応が薄いので時間がない方は読み飛ばしてください。gpixart
github.com/nfnt/resizeを使ってリサイズするだけでドット絵になります。
例えば、180x200の画像を18x20の画像にリサイズするとドット絵になります。(ドット絵というより画質の悪い画像ですけどね)
正直これで終わってしまうんです。なので、ドット絵ナニカ のように減色処理を加えました。手法としては、k-meansを使って減色します。pixart.gofunc kmeans(img *image.RGBA, cluster int, size int) *image.RGBA { vcolor := rgbaToArray(img) npixels := len(vcolor) vcluster := make([]color.RGBA, cluster) residual := float32(npixels) rand.Seed(time.Now().UnixNano()) vtype := make([]int, npixels) for i := 0; i < len(vtype); i++ { vtype[i] = rand.Intn(cluster) } niter := 0 for residual > 0 && niter < 30 { residual = 0 for i := 0; i < cluster; i++ { clusterInt := make([]int, 0) for index, typeCluster := range vtype { if typeCluster == i { clusterInt = append(clusterInt, index) } } if len(clusterInt) == 0 { continue } nclusterInt := float64(len(clusterInt)) rS, gS, bS, aS := 0.0, 0.0, 0.0, 0.0 for _, typeCluster := range clusterInt { color_ := vcolor[typeCluster] rS += float64(color_.R) / nclusterInt gS += float64(color_.G) / nclusterInt bS += float64(color_.B) / nclusterInt aS += float64(color_.A) / nclusterInt } vcluster[i] = color.RGBA{uint8(rS), uint8(gS), uint8(bS), uint8(aS)} } for vTypeIndex, color_ := range vcolor { clusterIndexMin := vtype[vTypeIndex] distanceMin := 1000.0 for clusterIndex, cluster := range vcluster { distance := distance(color_, cluster) if distance < distanceMin { distanceMin = distance clusterIndexMin = clusterIndex } } if clusterIndexMin != vtype[vTypeIndex] { residual++ } vtype[vTypeIndex] = clusterIndexMin } niter++ } for index := 0; index < npixels; index++ { vcolor[index] = vcluster[vtype[index]] } return upQualityImage(img, vcolor, size) }grimg
github.com/nfnt/resizeを使ってリサイズするだけです。
ただ辛かったのが、Gif画像のリサイズです。以下は全てgifのリサイズでのお話です。unknown format
// ファイルを開く file, _ := os.Open(o.InputFile) // formatがpng, jpeg, gifの判定のために Decode() _, format, _ := image.Decode(file) // gifのとき DecodeAll() if format == "gif"{ gifimg, err := gif.DecodeAll(file) }上記のコードだと
unknown formatが出現します。一度Decodeしたデータを再度Decodeしようとしたからみたいです。
解決策として、
1. ファイルをもう一度Openする
2. bytes.BufferにCopyしておく
3. decodeした後に、encodeし再びdecode
4. file.Seek(0, io.SeekStart)
が思いつきました。今回は4個目のseekメソッドを採用しました。// ファイルを開く file, _ := os.Open(o.InputFile) // formatがpng, jpeg, gifの判定のために Decode() _, format, _ := image.Decode(file) // gifのとき DecodeAll() if format == "gif"{ file.Seek(0, io.SeekStart) // add gifimg, err := gif.DecodeAll(file) }gifの扱える色
gifは256色(8ビット)までの色を扱うことのできる画像形式です。jpegは1670万色(24ビット)まで扱うことができる画像形式です。gifで使えるカラーがいかに少ないか分かりますね。
gif画像であっても"github.com/nfnt/resize"でリサイズすると256色を超えます。なのでgif画像のカラーを保存しておいて、drawするときにリサイズ済み画像の色を置き換えました。func keys(m map[color.Color]bool) color.Palette { var p color.Palette for k := range m { p = append(p, k) } return p } func getGifColor(rec image.Rectangle, img image.Image, w int, h int) color.Palette { cUsedM := make(map[color.Color]bool) // The color used in the original image for x := 1; x <= w; x++ { for y := 1; y <= h; y++ { if _, ok := cUsedM[img.At(x, y)]; !ok { cUsedM[img.At(x, y)] = true } } } return keys(cUsedM) }gif の disposal method
一番苦労しました。
最初にgif画像の1フレームずつリサイズして保存すれば、gif画像のリサイズ完成!と思い、実装しました。
結果、用意した10個のGifのうち4つが綺麗にリサイズできたのですが、6個はノイズが発生しました。最初訳がわからなかったのですが、CGファイル概説 第4章 第2節 その1 - 我楽多頓陳館 を読んだら、disposal methodによって処理を変えなきゃいけないっぽいことに気付きました。
Animated GIFs がdisposal methodを理解する上で参考になったので記載しておきます。
gif - The Go Programming Language によるとconst ( DisposalNone = 0x01 DisposalBackground = 0x02 DisposalPrevious = 0x03 )その時の私のコードは
DisposalPreviousだけに対応し、他のdisposal methodに対応していませんでした。これが原因でうまくリサイズできていませんでした。
ちなみに、grimgはDisposalBackgroundに対応していません。理由としては、DisposalBackgroundのGifが見つからなかったからです。。。
[補足]DisposalPreviousのときは差分画像しか保存されていないので、前フレームに上書きする形で画像を作成し、リサイズしています。リサイズ後に合成するとノイズが発生しました。まとめ & お願い
画像のリサイズだけで時間がかかってしまいました。
gifはDisposal Method,Delay Timeに気をつけて扱いましょう。
disposal methodDisposalBackgroundのGifが見つかれば僕に送ってほしいです!
- 投稿日:2020-01-03T09:38:56+09:00
ユーモアあふれる、ステータスコード 418 と 420 って何?
Status Code 418 by Google
https://www.google.com/teapot にアクセスすると ステータスコード
418が返ってきます。
開発者ツールからももちろん確認できます。400 番台の HTTP レスポンスエラーは、クライアントエラー ですね。
MDN のページで確認すると、サーバーが、自身がティーポットであることを理由としてコーヒーを入れることを拒否することを示します
とのこと。
1998 年からはじめた Google によるエイプリルフールのジョークのようです。
しっかりと Go のドキュメント (net/http) にも書かれていました。
StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 StatusExpectationFailed = 417 // RFC 7231, 6.5.14 StatusTeapot = 418 // RFC 7168, 2.3.3 StatusMisdirectedRequest = 421 // RFC 7540, 9.1.2 StatusUnprocessableEntity = 422 // RFC 4918, 11.2ジョークのステータスコードを入れていいのかよ。と思ったら Go 言語は Google が作った言語でしたね。。
Status Code 420 by Twitter
ツイッターでは、420 という ステータスコード を下記のように定義しています。
420 Enhance Your Calm Returned when an app is being rate limited for making too many requests.たくさんのリクエストをサーバーに送って制限値を超えた際に API のレスポンスで返されるステータスコードのようです。
420 は、マリファナとの関連して良く使われる言葉で、訳すと「落ち着いていこーぜ」という感じです。
ツイッター本社のあるカリフォルニアは、マリファナが合法なのでそれと何か関係しているのかもしれませんね。
注意
ステータスコード 418 & 420 は、正式な HTTP のステータスコードではない ので、HTTP のステータスコードとして実装する場合は、それを認識したうえで、ユーモアたっぷりに実装しましょ!










