20200103のGoに関する記事は12件です。

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

Go で ipc package を書いてみた

あけましておめでとうございます。

昨年の末ごろから、なんとなーくプロセス間のソケット受け渡しを Go でやってみたいなーと思って少しずつコードを書いていたんですが、年末年始の時間を使って書き上げたので、ここで報告します。

軽い気持ちで書き始めたものの、なかなかハマることが多くて結構な時間がかかってしまいました。。。

成果物

https://github.com/navel3/go-ipc
https://godoc.org/github.com/navel3/go-ipc

Motivation

  • 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 を使います。

src

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 で監視する実装になっているようなので、参考にして今後改善したいポイントです。

src

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) で受領します。

src

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 のようなタイムアウトの考慮は不要です。

src

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 にしておきます。

後述しますが、ここも要改善ポイント。

src

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 を全て読んだ後は、ソケットにデータがあるならば読み込みます。ここに問題があります。

続く。

src

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を介して接続を授受するパターンとしないパターンで性能測定をしてみました。

  1. TCP接続
  2. HTTP GET を書き出す
  3. 200 OK を応答する
  4. 接続を閉じる

測定環境

  • 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 もファイルディスクリプタから作ることができれば、もう少し楽に書けるのになーと思います。
正直、車輪の再発明をしている感が :disappointed_relieved:

まだまだ課題が残っていますが、いろいろ勉強にはなるので、今後も時間をみて修正してきたいと思います。

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

GoにおけるSqlite3の利用方法

Sqlite3のインストール

Windowsならば下記サイトよりSqliteをダウンロードします。
https://www.sqlite.org/download.html
「sqlite-tools-win32-x86-3300100.zip」をダウンロードしてインストールします。

Macならば、下記コマンドを実行してください。
brew install sqlite

gccのインストール方法

sqliteはC言語で開発されているため、gccが必要です。

Windowsの場合、下記よりTDM-gccをダウンロードしてインストールします。
http://tdm-gcc.tdragon.net/download

Macならば、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文を記載する

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

【Go】Sqlite3の利用方法

1. Sqlite3のインストール

Windowsならば下記サイトよりSqliteをダウンロードします。
https://www.sqlite.org/download.html
「sqlite-tools-win32-x86-3300100.zip」をダウンロードしてインストールします。

Macならば、下記コマンドを実行してください。
brew install sqlite

2. gccのインストール方法

sqliteはC言語で開発されているため、gccが必要です。

Windowsの場合、下記よりTDM-gccをダウンロードしてインストールします。
http://tdm-gcc.tdragon.net/download

Macならば、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文を記載する

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

[Go]ポインタをゼロから理解する

対象読者

Go言語の文法を学んでる段階の方から、*とか&を使うケースは覚えたけど何となく使ってしまってる方まで(ポインタ渡しについては触れてません)

はじめに

ポインタ変数を理解するに当たって、そもそも変数が定義された時どのようなことが行われているのかを理解する必要があります。そのために必要なメモリというものを冒頭で説明したいと思います。

メモリとは

PCのCPUが処理を行う際に読み書きを行う値(データ)を格納する場所のことです。メモリの最小単位は1バイトであり、1バイトごとにアドレスと呼ばれる識別番号が16進数で割振られています。

ここからはイメージを掴むために画像を入れながら説明します。16進数の適当なアドレスを用いて、メモリを表すとこのような形になります。

メモリとアドレス.png

変数定義の流れ

では実際に変数が定義された際の流れを見ていきましょう。
Int型のaという変数を定義したとします。このaという変数が定義された時メモリは以下のようになります。

変数格納.png

このように変数の型やサイズに合わせてメモリの中のアドレスが確保されるのです。

アドレスと変数のイメージを掴むことが目的なので、変数に対応した厳密な16進数ではありませんがご理解ください。

ポインタとは

ここまで変数定義された時の一連の流れを見てきました。
では本題のポインタの説明をします。

ポインタを端的に言うと、「その変数の型情報や大きさを指し示したもの」です。
より具体的には、その変数が格納されてるメモリ領域の先頭アドレスを指します。この先頭アドレスに変数の型に関する情報や領域(大きさ)に関する情報が入っているのです。

ポインタ.png

この図において変数aのポインタは75bd5b9ということになり、ここには型や領域に関する情報が入っています。

Goでの実装例

ではこれらのポインタをgoにおいてどのように扱うか見ていきましょう。

変数からポインタを出力する

簡単な実装例として、変数のポインタを出力してみます

①変数から直接取得する場合

main.go
package main

import "fmt"

func main() {
    var a int
    fmt.Println(&a)

    // 出力結果
    // 0xc000094000
}

これは&で(アンパサド演算子)変数のポインタを出力したものです。

②ポインタ型を指定して参照する場合

ポインタ型変数には変数のアドレス値が入ります。
よって、先ほどの変数aのアドレス値をポインタ型変数bに代入すれ変数のポインタを出力できます

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

import "fmt"

func main() {
    var a int
    var b *int
    b = &a
    c := *b
    fmt.Println(c)

    // 出力結果
    // 0
}

出力した変数cには何も値が入っていないので0が出力されました。

goの文法を理解する上で避けては通れないポインタですが、基本概念から落とし込み手を動かすことで徐々に慣れていきましょう

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

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:$GOBIN

setting.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 が使えるようになる

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

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:9

https://github.com/kuritayu/infra-tools にツールをおいてます。

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

【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ですよね?!
中身のDockerfiledevcontainer.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 を開いてみます。
すると・・・
スクリーンショット 2020-01-03 14.10.20.png

dev container configurationが見つかったからコンテナで開くか?と言う問い合わせが出ました!
そして Reopen in Containerをクリックすると・・・
スクリーンショット 2020-01-03 14.22.25.png
しばらく時間が経って…開きました!左下のステータスバーがグリーンに変わってRemote接続中であることと、"Dev Container: Node.js Sample"の文字が眩しいですね!
そうなんです。.devcontainerフォルダとdevcontainer.jsonの設定と然るべきDockerfileが揃っていればプロジェクトフォルダ内のファイルを丸ごとマウントしたコンテナの自動生成とプロジェクトをVSCodeで開くのを勝手にやってくれるのです!

また、ここでのDockerfileの特筆するべき点はdockerに問題の"root"ユーザー問題を宜しく解決してくれている点です。
"コンテナの中で開発する"のは聞こえはいいですが、大抵のイメージはそのまま実行するとユーザーが"root"になってしまうのが多いので、コンテナの中で更新されるファイルのオーナーが"root"になってしまってウザい問題がありました。自前でDockerfile書けばなんとでもなるのですがいちいち書いてられないし、いちいちDockerfile書くくらいなら開発環境汚れても別にいいじゃん会社のPCだし、みたいなことになってますよね!?
このDockerfileでは丁寧にそこのところをサポートしてくれているので、rootユーザー問題が無事に解決されています。

ついでにコンテナの管理も VSCode からできるので…
スクリーンショット 2020-01-03 14.23.29.png
このようにどのプロジェクトでどのコンテナ使っているかは、VSCodeから管理できます。
特にDockerではどのフォルダのDockerfileで立ち上げたコンテナかがわからない(と言うかイメージを作ってコンテナ起動するのでどこのフォルダのDockerfileで作成したイメージかどうかは本来は関係ないハズ)のでこれは便利です。

どうやって使うのか?

まずは本家リポジトリのご紹介をしておきましょう。
リポジトリ名からどの言語向けのサンプルプロジェクトかわかると思います。

実際には.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ページです。

これだけで本書けそうなボリュームですよ・・・

というところで、今回はここまでといたします!

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

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.go
package 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: &region,
    })
    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
  • ちなみに構築資材は下記を参照。
Dockerfile
FROM 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.yml
version: '3'
services:
  app:
    build:
      context: .
    container_name: "app"
    tty: True
    environment:
      BUCKET: <Bucket>
      PREFIX: test/
      REGION: ap-northeast-1

S3バケット

東京リージョンにバケットを一つ作って、以下のツールで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.104s

Goが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に代えても解決しなかったので関係なさげ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/go

GOPATHとPATHの設定

  • ホームディレクトリにgoディレクトリを作成してGOPATHに設定。
  • PATHにGOPATH/binを追加。

下記の2行を.bash_profileに追加しておく。

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

Visual Studio Code (vscode) のインストール

vscodeのアンインストール

vscodeの環境をクリーンにしたかったので、すでにインストールされているvscodeと設定ファイルをアンインストール。

  • vscodeの本体はFinderのApplicationフォルダからゴミ箱に移動
  • vscodeの設定ファイルはターミナルで削除
ターミナル
$ rm -rf  ~/Library/Application\ Support/Code
$ rm -rf ~/.vscode

vscodeのインストール

vscodeの最新版をダウンロードしてインストール
https://code.visualstudio.com/

Goプラグインのインストール

vscodeで"go"キーワードでプラグインを検索。Microsoft製のms-vscode.goを選択してインストール。
go_plugin.png

vscodeのコマンドパレットで"> Go: Install/Update Toolsを実行して、Goの各種ツールを全て選んでインストール。
go_tools.png

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ライクなカーソル移動ができるので。

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

Go言語で画像(png, jpg, gif)処理 [ドット絵、リサイズ]

初めに

「Go言語で画像処理を書いたことないなー」と思い、ドット絵を作成する gpixart と画像をリサイズする grimg を作成したときに苦労したことを書きます。
gpixartは内応が薄いので時間がない方は読み飛ばしてください。

gpixart

github.com/nfnt/resize を使ってリサイズするだけでドット絵になります。
例えば、180x200の画像を18x20の画像にリサイズするとドット絵になります。(ドット絵というより画質の悪い画像ですけどね)
正直これで終わってしまうんです。なので、ドット絵ナニカ のように減色処理を加えました。手法としては、k-meansを使って減色します。

pixart.go
func 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個はノイズが発生しました。

リサイズに失敗した(ノイズあり)Gif 
output_d.gif

最初訳がわからなかったのですが、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 method DisposalBackground のGifが見つかれば僕に送ってほしいです!

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

ユーモアあふれる、ステータスコード 418 と 420 って何?

Status Code 418 by Google

teapot.PNG

https://www.google.com/teapot にアクセスすると ステータスコード 418 が返ってきます。
開発者ツールからももちろん確認できます。

devtools.PNG

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 のステータスコードとして実装する場合は、それを認識したうえで、ユーモアたっぷりに実装しましょ!:relaxed:

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