React 入門 (1) ~ 基本環境構築 ~
2018年度版、React、webpack、Babelのセットアップ手順をアップデートしてみた - Qiita
これをベースに入門してみる。 そんなに引っかかることがなく入門できた。
プロジェクトの初期化
$ npm init -y
必要パッケージのインストール
$ npm install --save react react-dom $ npm install --save webpack $ npm install --save webpack-cli
package.json に build タスク追加
{ "scripts": { "build": "webpack --mode=production" }, }
babel のインストール
$ npm install --save babel-loader @babel/core $ npm install --save @babel/preset-env @babel/preset-react
webpack.config.js の追加
module.exports = { module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } }
webpack-dev-server のインストール
$ npm i --save webpack-dev-server $ npm i --save html-webpack-plugin html-loader
webpack.config.js
の修正
const HtmlWebPackPlugin = require('html-webpack-plugin'); module.exports = { module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, { test: /\.html$/, use: [ { loader: "html-loader" } ] } ] }, plugins: [ new HtmlWebPackPlugin({ template: "./src/index.html", filename: "./index.html" }) ] }
package.json の修正
"scripts": { "build": "webpack --mode=production", "start": "webpack-dev-server --open --mode=production" },
.babelrc の作成
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
src/index.html の作成
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>aaa</title> </head> <body> <div id="root"></div> </body> </html>
src/index.js の作成
import React from 'react' import ReactDOM from 'react-dom' ReactDOM.render( <div>Hello React</div>, document.getElementById("root") )
成果はここにアップした GitHub - bamchoh/react-study
ここから拡張する場合は、上記リポジトリをコピーしたあと
> npm install > npm run build > npm start
で復元したあと、拡張していけばいいのかなと思っている。
NFCカードリーダー pasori を使って NFCカードのIDを golang で読み出す
Windows 対応も済んだので、一旦記事を書いてまとめておく。
概要
Sony の NFCカードリーダー (Pasori) で NFCのID情報を取得できるパッケージを書きました。
NFCカードリーダーは以下のものを使用しました。 www.amazon.co.jp
Github リポジトリは以下。 GitHub - bamchoh/pasori: pasori library for golang
参考にした記事/サイト一覧
Linux
Windows
Golangの他実装 (Windows)
こちらは 上記の felicalib の dll を呼び出して使用されているようです。
私の実装
Linux
github.com/google/gousb
を使用しているため、libusb
に依存しています。ご使用の際には libusb
をインストールしてからご使用ください。
Windows
私のパッケージは ID のみを取得するため、felicalib ほど大がかりなライブラリを必要としていませんでしたので、felicalibで実装されている一部を Go で書き起こしました。ですので、Sony が提供している felica.dll のみに依存しています。Sonyのサイトより、felicaライブラリをインストールしてからご使用ください。このパッケージはC:/Program Files/Common Files/Sony Shared/FeliCaLibrary
以下に felica.dll が存在していることを期待しています。
syscall.Call に 構造体のポインタを渡す方法
ひょんなことから golang で dll を動的に呼び出さなくてならなくて、引数として構造体のポインタを渡さなくちゃならなかったんだけど、調べてもよくわからんかったのでメモしておく。
dll 呼び出し側は以下のようなC言語の実装になっているとする。
#include "stdio.h" typedef struct struct1 { int x; int y; int ret; char name[8]; } struct1; int add0(int x, int y) { int ret; ret = x + y; return ret; } void add1(int x, int y, int *ret) { *ret = x + y; return; } void add2(struct1 *args) { args->ret = args->x + args->y; sprintf(args->name, "struct1"); return; }
golang から dll を呼び出すためにまず dll をLoadDLL
でロードする必要がある。そのあと、関数を FindProc
で golang の変数にバインドして Call
メソッドでその関数を呼び出すんだが、渡す引数をすべて uintptr
にしなければならない。Goの型を uintptr
にするには変数のポインタを unsafe.Pointer
で一度ラップしてからそれをさらに uintptr
でラップすることでできる。ただ、配列や構造体は変数のポインタを渡してもちゃんと渡されなくて、第一要素のポインタを渡す必要があるようだ。
例えば var a [10]int
のような場合は uintptr(unsafe.Pointer(&a))
ではなく uintptr(unsafe.Pointer(&a[0])
とする必要がある。構造体の場合の例では
var b struct { x int y int }
このような構造体があったとしたら、uintptr(unsafe.Pointer(&b.x))
とすることでポインタが渡る。(ただ、複雑な構造体の場合は単純には渡せない可能性があり、その場合はどうするのかはまだ解明不足)
上記を踏まえたうえで、先の dll 内の関数を呼び出す golang のコードは以下になる。
package main import ( "C" "fmt" "log" "syscall" ) import "unsafe" type struct1 struct { x int32 y int32 ret int32 name [8]uint8 } func main() { dll, err := syscall.LoadDLL("a.dll") if err != nil { log.Fatal("LoadDLL: ", err) } defer dll.Release() add0, err := dll.FindProc("add0") if err != nil { log.Fatal("FindProc(add2): ", err) } add1, err := dll.FindProc("add1") if err != nil { log.Fatal("FindProc(add2): ", err) } add2, err := dll.FindProc("add2") if err != nil { log.Fatal("FindProc(add2): ", err) } ret0, _, _ := add0.Call(1, 2) fmt.Println("add0:") fmt.Println(" ", ret0) var ret1 int add1.Call(1, 2, uintptr(unsafe.Pointer(&ret1))) fmt.Println("add1:") fmt.Println(" ", ret1) var ret2 struct1 ret2.x = 1 ret2.y = 2 add2.Call(uintptr(unsafe.Pointer(&ret2.x))) fmt.Println("add2:") fmt.Println(" ", ret2.x) fmt.Println(" ", ret2.y) fmt.Println(" ", ret2.ret) fmt.Println(" ", string(ret2.name[:])) }
以上
C#のライブラリ vs-streamjsonrpc とGoの標準添付パッケージ jsonrpc を通信させる方法
前回 Go と C# を名前付きパイプでつなぐ - bamch0h’s diary という記事を書いた。 その記事のまとめにも書いたように、RPCで通信できるのか興味があったので調べてみた 意外と大変だったのでブログに残しておくことにする。
TL;DR
- C#側のライブラリとして
Microsoft/vs-streamjsonrpc
を使ってみた - Go側は標準添付のJSONRPCパッケージだと通信しない。
- 理由は
vs-streamjsonrpc
が送受信にヘッダとしてContent-Length
を要求するから - なので、JSONRPCのコーデックに渡すnet.Connをラップしてあげるとよい
Go側のパッケージ
Go側は標準のパッケージを使用することにした。参考にしたのは以下の記事
Microsoft/go-winio
で Go同士をjsonrpcでつながることを確認して次の工程へ
C#側のライブラリ
C# jsonrpc
とかで検索すると JSON-RPC.NET
がトップに出てくるけど、あまり良い印象がないのと Microsoft 公式のリポジトリということで以下のライブラリを選択した。
サンプル実装は以下のリポジトリが紹介されていた。
ただ、サンプルをそのまま使ってもGo言語とは通信できない。理由はいくつかあり、以下の通り。
- このライブラリがjsonデータを送信する前にヘッダ情報として
Content-Length: <jsonデータ長>\r\n\r\n
を送信するから - Goの標準ライブラリは
params
を配列で来ているものとみなして1番目の要素のみを取得するが、送信されてきたデータはmap
なのでパースエラーとなってしまう - 1番目の内容とも絡む話だが、Go側からjsonデータを送信する前に
Content-Length
を送信してあげないとC#ライブラリ側でちゃんと受信してくれない
諸問題を解決する
の問題に対応するため、Go側でヘッダ情報をパースする。といっても、今回は
vs-streamjsonrpc
と通信する。という割り切りの元、Read待ちしているときの初めのパケットを捨てる。という対応で何とかしのぐことにする。の問題はGo側で対処するよりも、C#側で対処したほうがやりやすい。Go側の引数と同じフィールドを持つクラスを作成し、それを引数として送信してあげることにする。
の問題はGo側でjsonデータを送信する前に割り込んで
Content-Length
を送信する必要がある。だが、jsonデータの受信から送信までの流れをServeCodec()
が一手に引き受けているため外側からはどうにもできない。そこで、案が2つあり、一つはServeCodec()
に渡す引数のrpc.ServerCodec
を新たに作成し、jsonデータを送信する前にContent-Length
を送信するように修正する。というもの。もう一つは、jsonrpc.NewServerCodec()
に渡すio.ReadWriteCloser
をラップしてWrite()
が呼ばれたときに、実データを送る前にContent-Length
を送信する。というもの。前者は別の理由(JSONRPCのバージョン情報である、jsonrpc:
キーをjson内に含めたいという理由)で修正を行っていたが、標準ライブラリのほとんどをコピーしてこなければならず、あまり筋の良い方法だとは思えなかったので、後者に切り替えた。(こっちもWrite()する前に必ずContent-Length
を送信するので、それはそれでどうなんだろう? とも思ったりする)
Go側の実装
というわけで以下のように実装した
package main import ( "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "time" "github.com/Microsoft/go-winio" ) // --- ラッパー部分 ---- // Accept() で ラップした net.Conn を返したいのでwinioのListenerもラップする type localWin32PipeListener struct { l net.Listener } func (l *localWin32PipeListener) Accept() (net.Conn, error) { conn, err := l.l.Accept() if err != nil { return nil, err } // Accept()で取得したconnをラップして返す return &wrappedConn{conn}, nil } func (l *localWin32PipeListener) Close() error { return l.l.Close() } func (l *localWin32PipeListener) Addr() net.Addr { return l.l.Addr() } // net.Conn をラップした構造体 type wrappedConn struct { c net.Conn } func (c *wrappedConn) Read(b []byte) (n int, err error) { var m int tmp := make([]byte, 65535) m, err = c.c.Read(tmp) if err != nil { return 0, err } newline := 0 for i := 0; i < m; i++ { // Content-Lengthを受信している前提で読み飛ばす if tmp[i] == '\n' { newline++ continue } // body を buffer に詰め込む。bufferは512バイトなので // 受信データがそれ以上あるとコケる。 // ここら辺は後で実装を詰める必要あり if newline >= 2 { for j := 0; j < len(tmp[i:m]); j++ { b[j] = tmp[i+j] } return len(tmp[i:m]), nil } } return 0, nil } func (c *wrappedConn) Write(b []byte) (n int, err error) { // 標準パッケージでは jsonrpc 2.0 用のバージョンが付加されないので // 無理やりつける // これがないと、クライアント側で受け取ってくれない prefix := []byte(fmt.Sprintf("{\"jsonrpc\":\"2.0\",")) body := append(prefix, b[1:]...) // 実データを書き込む前に常にContent-Lengthを送信する header := []byte(fmt.Sprintf("Content-Length: %v\r\n\r\n", len(body))) fmt.Print(string(header)) fmt.Print(string(body)) return c.c.Write(append(header, body...)) } func (c *wrappedConn) Close() error { return c.c.Close() } func (c *wrappedConn) LocalAddr() net.Addr { return c.c.LocalAddr() } func (c *wrappedConn) RemoteAddr() net.Addr { return c.c.RemoteAddr() } func (c *wrappedConn) SetDeadline(t time.Time) error { return c.c.SetDeadline(t) } func (c *wrappedConn) SetReadDeadline(t time.Time) error { return c.c.SetReadDeadline(t) } func (c *wrappedConn) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } // ---- 本体 ---- var pipename = `\\.\\pipe\winiotestpipe` type ( TestRPC struct{} RPCArgs struct { A, B int } ) func (t *TestRPC) Add(args *RPCArgs, reply *int) error { fmt.Println("args.A", args.A) fmt.Println("args.B", args.B) *reply = args.A + args.B fmt.Println("Result", *reply) return nil } func main() { s := rpc.NewServer() t := &TestRPC{} s.Register(t) l, err := winio.ListenPipe(pipename, nil) if err != nil { log.Fatal(err) } defer l.Close() // Listener をラップする ll := &localWin32PipeListener{l} conn, err := ll.Accept() if err != nil { log.Fatal(err) } defer conn.Close() s.ServeCodec(jsonrpc.NewServerCodec(conn)) }
C#側の実装
rpc.InvokeAsync()
を実行するときの引数の指定に注意。それ以外はデフォルトで通信する。(Go側で頑張ってるからね)
using System; using System.Threading.Tasks; using System.IO.Pipes; using StreamJsonRpc; namespace StreamJsonRpcServer { // 引数指定用のクラスを定義する public class Add { public int A; public int B; } class Program { static async Task Main(string[] args) { await MainAsync(); } static async Task MainAsync() { using (var stream = new NamedPipeClientStream(".", "winiotestpipe", PipeDirection.InOut, PipeOptions.Asynchronous)) { await stream.ConnectAsync(); var rpc = JsonRpc.Attach(stream); // メソッド名は TestRPC.Add とすること // 引数をクラスで指定する int sum = await rpc.InvokeAsync<int>("TestRPC.Add", new Add { A = 3, B = 5 }); Console.WriteLine($"3+5={sum}"); } } } }
まとめ
C# の vs-streamjsonrpc
と Go の標準ライブラリ jsonrpc
をラップしてnet.Conn経由して通信するようにした。
vs-streamjsonrpc がなぜ Content-Length を要求するのかがわからないが、何か理由がるのだろう。ただ、その部分で汎用性が低くなっているのでは?という気がする。使用したvs-streamjsonrpc のバージョンが1.4
だったのだが、github上では2.0 betaがあったので、もしかしたらそのバージョンではより簡単に通信できるようになっているかもしれない。また、2.0 beta では 自由にハンドラを変更できるようになっているので、2.0がリリースされたら高速なハンドラを使用して通信できるようにするのも楽しいかもしれない。
Go と C# を名前付きパイプでつなぐ
昨日、UNIXドメインソケットをGoで使うサンプルを作成したがその際にパフォーマンス比較用に作成した名前付きパイプを使ってC#と通信する。
サーバーサイド(Go言語)
特筆するところはない。
package main import ( "log" winio "github.com/Microsoft/go-winio" ) var pipename = `\\.\pipe\winiotestpipe` var ok = []byte("ok") var bufSize = 65537 func main() { l, err := winio.ListenPipe(pipename, nil) if err != nil { return } defer l.Close() for { func() { conn, err := l.Accept() if err != nil { log.Fatal(err) } defer conn.Close() buf := make([]byte, bufSize) for { for { n, err := conn.Read(buf) if err != nil { return } if buf[n-1] == '\n' { break } } conn.Write(ok) } }() } }
クライアントサイド(C#)
注意するところは以下の通り
* 名前付きパイプのパスをGo言語側と同じにする必要があるので、\\.\pipe\
部分を削除しておく。
* UTF8でエンコードしてあげることで、GO側でも扱いやすくする。おそらくRead()したバッファは逆にUTF8からUTF16にエンコードする必要があるのではないかと思っているが、コード簡略化のため実施していない。
using System; using System.IO.Pipes; using System.Text; namespace ConsoleApp1 { class Program { static void PrintBuffer(byte[] buffer) { for (int i = 0; i < buffer.Length; i++) { Console.Write(Convert.ToChar(buffer[i])); } } static void PrintlnBuffer(byte[] buffer) { PrintBuffer(buffer); Console.WriteLine(); } static void Main(string[] args) { var client = new NamedPipeClientStream("winiotestpipe"); client.Connect(); var enc = new UTF8Encoding(); byte[] buffer = enc.GetBytes("a\n"); int len = buffer.Length; PrintBuffer(buffer); client.Write(buffer, 0, len); client.Flush(); byte[] rxbuf = new byte[2]; client.Read(rxbuf, 0, 2); PrintlnBuffer(rxbuf); Console.ReadLine(); } } }
まとめ
json
とかでやり取りしたらRPC通信的なことができてGoをバックエンドとしたデスクトップアプリとか書けるのかなぁ?と思っている。
go 1.12 では Windows にも Unix ドメインソケットが入るので簡単に使ってみた。
参考
go 1.12 beta1 のインストール
ここ(Google グループ) に書いてある通りにダウンロード。
> go get golang.org/dl/go1.12beta1 > go1.12beta1 download
簡単にインストールできるし、Windows でもコマンドが同じなのが便利。
ベンチマーク
単純にサーバーとクライアントを作るのもつまらないので、go test -bench
で通信のベンチマークをとってみる
サーバーサイド
net.Listen
に渡す引数を tcp
から unix
に変えて、IPアドレスとポートの代わりにUNIXドメインソケット用ファイルをアドレスとして渡すだけ。
package main import ( "log" "net" "os" "path/filepath" ) var ok = []byte("ok") var bufSize = 65537 func main() { proto := "unix" addr := filepath.Join(os.TempDir(), "unixdomainsocketsample") os.Remove(addr) listener, _ := net.Listen(proto, addr) defer listener.Close() for { func() { conn, err := listener.Accept() if err != nil { log.Fatal(err) } defer conn.Close() buf := make([]byte, bufSize) for { for { n, err := conn.Read(buf) if err != nil { return } // 終端をチェック if buf[n-1] == '\n' { break } } conn.Write(ok) } }() } }
クライアントサイド
ベンチマーク用関数を定義。送信サイズごとに計測
package main import ( "log" "net" "os" "path/filepath" "strings" "testing" ) func setup() net.Conn { proto := "unix" addr := filepath.Join(os.TempDir(), "unixdomainsocketsample") conn, err := net.Dial(proto, addr) if err != nil { panic(err) } return conn } func sendBase(conn net.Conn, times int, data []byte) { buf := make([]byte, 2) for i := 0; i < times; i++ { _, err := conn.Write(data) if err != nil { log.Println(err) return } conn.Read(buf) } } func BenchmarkSend1Char(b *testing.B) { conn := setup() defer conn.Close() benchmarks := []struct { name string data []byte }{ {"00001-char", []byte(strings.Repeat("a", 1))}, {"00127-char", []byte(strings.Repeat("a", 127))}, {"00256-char", []byte(strings.Repeat("a", 256))}, {"00512-char", []byte(strings.Repeat("a", 512))}, {"01024-char", []byte(strings.Repeat("a", 1024))}, {"02048-char", []byte(strings.Repeat("a", 2048))}, {"04096-char", []byte(strings.Repeat("a", 4096))}, {"08192-char", []byte(strings.Repeat("a", 8192))}, {"16384-char", []byte(strings.Repeat("a", 16384))}, {"32768-char", []byte(strings.Repeat("a", 32768))}, {"65536-char", []byte(strings.Repeat("a", 65536))}, {"131072-char", []byte(strings.Repeat("a", 131072))}, {"262144-char", []byte(strings.Repeat("a", 262144))}, } for _, bm := range benchmarks { var bmData []byte b.Run(bm.name, func(b *testing.B) { bmData = append(bm.data, '\n') // 終端追加 b.ResetTimer() sendBase(conn, b.N, bmData) }) } }
計測
計測方法は、サーバー側を起動した後、ベンチコマンドをクライアント側で実行する。
サーバー側実行
> go1.12beta1 build -o server.exe server.go & server.exe
クライアント側
> go1.12beta1 test -bench .
結果
TCP、名前付きパイプとの実行結果を合わせて表にしてみた。TCPは標準の net
パッケージを使用。名前付きパイプはMicroSoft/winio
(https://github.com/Microsoft/go-winio) の実装を使用
() の中はサーバー側のバッファサイズ
size は 送信データのサイズ
size | Unix(8192) | Unix(65537) | Tcp(8192) | Tcp(65537) | Pipe(8192) | Pipe(65537) |
---|---|---|---|---|---|---|
1 bytes | 19900 ns/op | 26439 ns/op | 74849 ns/op | 77649 ns/op | 60250 ns/op | 58899 ns/op |
127 bytes | 19230 ns/op | 26299 ns/op | 74449 ns/op | 77105 ns/op | 59733 ns/op | 58099 ns/op |
256 bytes | 19319 ns/op | 26499 ns/op | 75199 ns/op | 77009 ns/op | 61233 ns/op | 56000 ns/op |
512 bytes | 19790 ns/op | 26119 ns/op | 76249 ns/op | 77757 ns/op | 57700 ns/op | 58233 ns/op |
1024 bytes | 21549 ns/op | 27120 ns/op | 76349 ns/op | 77599 ns/op | 60200 ns/op | 57533 ns/op |
2048 bytes | 19020 ns/op | 33019 ns/op | 86399 ns/op | 87699 ns/op | 60050 ns/op | 57549 ns/op |
4096 bytes | 25670 ns/op | 31639 ns/op | 93450 ns/op | 96149 ns/op | 60600 ns/op | 59499 ns/op |
8192 bytes | 39780 ns/op | 34859 ns/op | 119399 ns/op | 119499 ns/op | 61300 ns/op | 60166 ns/op |
16384 bytes | 44699 ns/op | 33139 ns/op | 169699 ns/op | 167199 ns/op | 65550 ns/op | 63899 ns/op |
32768 bytes | 62300 ns/op | 40339 ns/op | 270600 ns/op | 259000 ns/op | 73649 ns/op | 64999 ns/op |
65536 bytes | 84750 ns/op | 44933 ns/op | 496664 ns/op | 493668 ns/op | 86299 ns/op | 75300 ns/op |
131072 bytes | 129299 ns/op | 80049 ns/op | 905476 ns/op | 908032 ns/op | 113600 ns/op | 80750 ns/op |
262144 bytes | 213400 ns/op | 119999 ns/op | 1828996 ns/op | 1816518 ns/op | 188400 ns/op | 117900 ns/op |
TCPとUNIXドメインソケットの差が大きいのでTcp(8192)
とTpc(65536)
を除外してグラフ化
Unix ドメインソケット同士の比較だと 4096と8192の間ぐらいで速度が反転する。大きいデータを送信する場合はそのあたりを基準にチューニングする必要があるのかな? パイプとUnix ドメインソケットの比較だと、基本的にはUnixドメインソケットのほうが早いようだ。ただ、サイズが大きくなると速度が同等になるので、大きいデータを一括で送信するようなケースの場合はパイプのほうがいいのかもしれない。
まとめ
今回はUNIXドメインソケットをGO言語で使ってみた。 使うだけだとつまらないのでパフォーマンスを測定し、TCP、名前付きパイプとの速度と比較してみた。 UNIXドメインソケットはTCPより速いみたいなので、内部的な通信ではUNIXドメインソケットを使うほうがいいのかもしれない。 また、Unixドメインソケットはパイプより速いが、大きなデータを一括で送信する場合はパイプでも速度的な違いはない。
今後の展開
Go言語とそれ以外の言語で通信できるようなら色々楽しそうなので そういうことにもチャレンジしていきたい。
コンテナに入るのに docker ps してから docker start して docker exec するのめんどくさいから一度にできるコマンド作った
結論
作った。Goで。 github.com
経緯
パソコン起動してから、dockerのコンテナに入るのにコマンドをいくつも叩かないといけないのがめんどくさかったから。
叩くコマンドは以下の3つ
* docker ps -a
* docker start <コンテナ名>
* docker exec -it -e <デフォルト環境変数> <コンテナ名> bash
コマンドの内容としては、単純で docker ps -a --format "{{.Name}}"
で出力されたコンテナ名を cho
で選択して、それを docker start
に渡して実行した後、docker exec
をコンテナ名とともに起動しているだけ。
cho に関しては、以下のリンクを参照。パイプで渡された文字列に対して行単位で選択ができるコマンド。今回作ったコマンドを実行するには必要なので、別途 go get https://github.com/mattn/cho
でインストールしておく必要がある。
mattn.kaoriya.net
知見
docker ps の書式指定
docker ps
に --format
というのがあり、表示するフォーマットをGoのtemplate書式で指定できる。
docker ps --format "{{.Name}}" とすれば、コンテナ名のみ表示され
docker ps --format "table {{.ID}}\t{{.Labels}}"とすれば、ヘッダ行を追加した状態で
IDと
Label`が表示されるので便利。
go-pipeline の Output で返される文字列
docker ps
と cho
を繋ぐために、今回は go-pipeline
(GitHub - mattn/go-pipeline) を使用したが、返される文字列をそのまま exec.Command() の引数として使うとエラーとなる。理由としては文字列の最後に改行文字が入っており、コンテナ名が存在しない。となるからだ。解決方法としては、最後の1バイトを取り除いた部分文字列を渡すことで解決した。
docker exec が動かない
上記問題を解決したうえで docker exec
が実行時にエラーとなっていた。原因は exec.Command で作成した Cmd 構造体の Stdin と Stdout にちゃんと標準入力と標準出力をセットしていなかったことが原因だった。nil のままだと NULLデバイス(os.DevNull)にアクセスするようだ。(https://golang.org/pkg/os/exec/#Cmd)