Yabu.log

ITなどの雑記

Golangで書かれたWebサーバーでHTTP/1.1のkeep-aliveを検証する

HTTP/1.1にはKeep-Aliveという通信方法があります。HTTP/1.0の頃は1セットの通信が終わるたびに接続・切断処理が入っていたので非効率だったため、複数のリクエストが来た場合にコネクションを使い回す機能になります。 今回はGolangで書かれたサーバーを実行してWiresharkで通信の様子をキャプチャしています

コード

keep-aliveの実装コードの一部です。Requestを終わりまで読み取り、コネクションを再利用します。

//Accept後のソケットでなんども応答を返すためにループ
for {
  //タイムアウトを設定
  conn.SetReadDeadline(time.Now().Add(5 * time.Second))
  //リクエストを読み込む
  request, err := http.ReadRequest(bufio.NewReader(conn))
  if err != nil {
    //タイムアウトもしくはソケットクローズ時は終了
    //それ以外はエラーにする
    neterr, ok := err.(net.Error) //ダウンキャスト
    if ok && neterr.Timeout() {
      fmt.Println("timeout")
      break
    } else if err == io.EOF {
      break
    }
    panic(err)
  }
  //リクエストを表示
  dump, err := httputil.DumpRequest(request, true)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(dump))
  content := "Hello World\n"

  //レスポンスを書き込む
  //HTTP/1.1かつ,ContentLengthの設定が必要
  response := http.Response{
    StatusCode:    200,
    ProtoMajor:    1,
    ProtoMinor:    1,
    ContentLength: int64(len(content)),
    Body: ioutil.NopCloser(
      strings.NewReader(content)),
  }
  response.Write(conn)
}

通信内容

curlで2回リクエストを投げています。成功すると2回hello worldが帰ってきます

$ curl localhost:8888 localhost:8888
hello world
hello world

キャプチャ結果

続いてWiresharkのキャプチャ結果を貼りますちょっと分かり辛いかもしれませんが、

  • 「接」がTCPの接続処理(3Way HandShake)です
  • 「通」がデータの通信処理です
  • 「断」がTCPの切断処理です(FIN)

f:id:yuyubu:20180915230426p:plain
図1.keep aliveなしの場合

f:id:yuyubu:20180915230429p:plain
図2.keep aliveありの場合

keep aliveが無い方は2回のリクエストに対してそれぞれ接続、切断処理を行なっていますが、keep aliveがある方はコネクションを使いまわして全部で1度しか接続、切断処理をしていないことがわかります。

Golangで作成したWEBサーバーのTCP通信(HTTP GET)をWiresharkで検証

ソースコード

サーバー側のソースコードです8888ポートでクライアントからの送信を待ち受けます

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httputil"
    "strings"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:8888")
    if err != nil {
        panic(err)
    }
    fmt.Println("Server is running at localhost:8888")
    for {
        conn, err := listener.Accept()
        if err != nil {
            panic(err)
        }
        go func() {
            //fmt.Printf("Accept %v\n",conn.RemoteAddr())
            //リクエストを読みこむ
            request, err := http.ReadRequest(
                bufio.NewReader(conn))
            if err != nil {
                panic(err)
            }
            dump, err := httputil.DumpRequest(request, true)
            if err != nil {
                panic(err)
            }
            fmt.Println(string(dump))

            //レスポンスを書き込む
            response := http.Response{
                StatusCode: 200,
                ProtoMajor: 1,
                ProtoMinor: 0,
                Body: ioutil.NopCloser(
                    strings.NewReader("hello world\n")),
            }
            response.Write(conn)
            conn.Close()
        }()
    }
}

クライアントの方は後述のcurlを利用します

通信方法

サーバーの方で8888のポートで受信を受け付けているのでこちらにcurlで通信を試みています

$ curl localhost:8888
hello world

サーバーの方でリクエストを受け取るとhello worldという文字列を返すようにしているのでコマンドを実行するとこちらの文字列が帰ります

localhostに対する送受信をキャプチャする準備

yuyubu.hatenablog.com

こちらの記事を参考にLoopback:Io0を選択しています

netstatコマンドを利用したTCPコネクションの状態

以下のタイミングでnetstatコマンドを実行してコネクションの状態を確認しました

サーバー起動時

サーバーは起動したタイミングではnetstatコマンドの出力に8888portの情報は含まれていません。

listner.Accept()メソッドに置いたブレイクポイントで処理を止めたタイミング

tcp4      78      0  localhost.ddi-tcp-1    localhost.53315        ESTABLISHED
tcp4       0      0  localhost.53315        localhost.ddi-tcp-1    ESTABLISHED

クライアント側のポート番号として53315が選ばれていることがわかります

なおこの状態でパケットをキャプチャすると以下の情報が得られました。 サーバーがGETを受け取ってOKを返す直前という感じです。

f:id:yuyubu:20180915165701p:plain

接続終了時

netstatコマンドにport8888の出力は含まれませんでした

次の記事でHTTP/1.1のKeep-Aliveを検証するのでこのnetstatの情報の差分は興味深い結果になると思います

キャプチャ結果

Wiresharkを起動しフィルタをtcp.port==8888に設定して通信内容を記録しました。すると下のような通信内容が得られました。

f:id:yuyubu:20180915165058p:plain

解説

  • 色枠1のところでTCPの3way HandShakeをしています
  • 色枠2のところでデータを送受信しています
  • 色枠3のところで切断処理をしています

3Way HandShake

TCPの接続開始を示すこの3つのパケットのやりとりを3 Way HandShakeと言います。

f:id:yuyubu:20180915165237p:plain

データ送受信

f:id:yuyubu:20180915165250p:plain

  • データ送信直後に、受信側が送信するパケットのACKがデータを受け取った分だけ増えていることが確認できます。
  • データ受信確認の後、送信側から送ったもので受信が確認できたオクテット数だけSEQが増えていることが確認できます

切断

f:id:yuyubu:20180915165307p:plain

サーバーから送ったFINのパケットに対してクライアントもFINのパケットを送り返しています。 こちらは確認応答のたびにACKを+1しています

感想

キャプチャ結果冒頭*1の[SYN]パケットを送った後の[RST,ACK]が送り返されて再度SYNパケットを送信している理由がわかりませんでした。 一般的な挙動には見えないのですが。。。これはGoのListnerの実装などを読めばわかるのでしょうか。次回はKeep-Aliveを実験してみたいと思います

参考

  • Goならわかるシステムプログラミング第6章TCPソケットとHTTPの実装
  • コンピュータネットワーク第5版
  • マスタリングTCP/IP入門

*1:①の上に位置する2つのパケット

Wiresharkでlocalhost(127.0.0.1)宛のパケット送受信をキャプチャする方法

「Goならわかるシステムプログラミング」のネットワークプログラミングの章でプログラムの検証をやりたかったのでLoopbackアドレスに対するパケット送受信ができないのか調べてみたところ、日本語ではあまりズバリな解決策*1が出なかったため英語で調べると解決した。

wireshark how to capture localhost」でググったところ以下の投稿を参考にwiresharkwikiにたどり着いた

https://stackoverflow.com/questions/5847168/wireshark-localhost-traffic-capture

https://wiki.wireshark.org/CaptureSetup/Loopback

Summary: you can capture on the loopback interface on Linux, on various BSDs including Mac OS X, and on Digital/Tru64 UNIX, and you might be able to do it on Irix and AIX, but you definitely cannot do so on Solaris, HP-UX, or Windows.

上記引用にある通り、WiresharkNIC選択一覧に出ているものの中には、loopbackのパケットをキャプチャできるものがある。

f:id:yuyubu:20180913133347p:plain
Loopback:Io0を利用した

今回はこちらを選択することで無事localhost宛のパケットをキャプチャすることができた

f:id:yuyubu:20180913133516p:plain
TCPハンドシェイクを含むHTTP通信をキャプチャした様子

ただしmacOSではこの方法が使えたが

では使えないので該当OSユーザーの方は別の方法を模索してください

*1:ルーティングテーブルを書き換えろとか、別のツールを入れろとか

Effective SQL 読書会(5)に参加

参加者が少なかったので木村さんの話がいつもより多く聞けました。 サポートやコンサル、DBMSの制作などをされている方なのでその膨大な知見に毎度驚かされます。

今回は

  • (続き)第2章 プログラム可能性とインデックスの設計
    • 項目17 計算値をインデックスで使用する状況を理解する
  • 第3章 設計を変更できないときはどうするか
    • 項目18 変更できないものはビューを使って単純化する
    • 項目19 ETLを使って非リレーショナルデータを情報に変える
    • 項目20 サマリテーブルを作成して管理する

を読みました

Effective SQL

Effective SQL

  • SQLServerの計算処理はC言語のライブラリを使っている

    • ビルド(32bit or 64bit,win/linux/mac)によって微妙に計算結果が違っていたらしする
    • 最近はOS間の違い(Cのライブラリ?)が少なくなってきている
  • SQLServerANSI_NULLS

過去のSQLServerではNULLが値として扱われていた。 そのためNULL is NULLじゃなくてNULL = NULLでもTrueとなる ANSI_NULLSオプションをオンにするとこのようなおかしな挙動を防げる 最近だとデフォルトで有効化されているので安心。

  • MySqlのTIMESTANP型はタイムゾーンを考慮する
  • 関数ベースのインデックスは決定関数でしか使えない
  • 関数か決定的かどうかは関数定義時に特殊なキーワードを使う
    • DETERMINISTIC句(Oracle)
    • IMMUTABLE句(PostgreSQL)
    • その関数が決定的かどうかはコンパイル時にわからない。
      • インデックスに利用する関数が決定的であることはプログラマが責任を持たなければならない。
  • SQLliteの専門的な知見を持っている日本人はかなり少ない
    • らしいです
  • ETLツール
    • 静的なデータファイルをDBにインポートするツール
    • それらしいものは何度か触ったことがあるが、ETL(Extract, Transform, Load)という名称は知らなかった
    • 商用のものだとData Spiderなんかも有名
    • PostgreSQLではTALEND(OSS)なんかが利用可能
  • 計算量の大きいデータをキャッシュする仕組み
    • マテリアライズドビュー
    • サマリーテーブル
      • サマリーテーブルは有用だがあまり活用されないらしい
  • MySQLのイベントスケジューラー

  • Raspberry Pi

    • 最近は64bitのOSもある
      • 最近までARMは32bitのOSが殆どだったらしい
    • 並列計算にも利用されている

ラズパイは正直、あまり大規模ではない組み込み用途か教育用でしか利用できないと思っていたが、 分散システムのようなものを作ってパフォーマンスがシビアな状況でも利用しているらしい

次回は「項目21 UNIONを使って非正規化データをアンピボットする」から

プログラマのためのSQL 読書会(26)に参加

今回もやたら難解なクエリが多く、輪読がはかどらず、もくもく会になってしまう場面が多かったです*1

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

検証環境:

  • MySQL:Ver 14.14 Distrib 5.7.22, for osx10.13 (x86_64) using EditLine wrapper
  • PostgreSQL:PostgreSQL 10.1 on x86_64-apple-darwin13.4.0

FETCH FIRST ROWS

fetch first 3 rowsを使って結果から「N行までのデータ」という取り方をすることができます。 それぞれのDBMS実装依存でTOPやLIMITなども使えますが、一応SQL標準(ANSI 2008)の行数制限指定はこちらですので、 標準SQLにこだわる方はこちらの存在を知っておいた方が良いです。

list_seq  | cat_string
----------+------------
        1 | abc
        2 | bcd
        3 | cde
        4 | def
        5 | efg
        6 | fgh
        7 | ghi
SELECT * FROM make_string FETCH FIRST 3 ROWS ONLY;
 list_seq | cat_string
----------+------------
        1 | abc
        2 | bcd
        3 | cde
(3 rows)

limit句とfetch first句の対応状況です。

SQL-対応DB Oracle MySQL PostgreSQL DB2 SQLServer
limit × ×
fetch first

SQLServerでは標準化される前はTOPという構文を使っていたようです。

p.547のクエリが動くのはPostgresだけ

らしいです

--▼P.547


CREATE TABLE Make_String
(list_seq INTEGER NOT NULL PRIMARY KEY,
 cat_string VARCHAR NOT NULL);--MySQLで試す場合はVARCHARのサイズ指定が必要
---

INSERT INTO Make_String
VALUES (1, 'abc'), (2, 'bcd'), (3, 'cde'),
       (4, 'def'), (5, 'efg'), (6, 'fgh'), (7, 'ghi');

---

WITH RECURSIVE
String_Tail(list_seq, cat_string)
AS
(SELECT list_seq, cat_string
   FROM Make_String
  WHERE list_seq > 1),
String_Head(list, max_list_seq, list_seq)
AS
(SELECT cat_string, 1, list_seq
   FROM Make_String
UNION ALL
SELECT String_Head.list || ', ' || String_Tail.cat_string,
       String_Head.max_list_seq + 1,
       String_Head.list_seq
  FROM String_Tail, String_Head
 WHERE String_Tail.list_seq = String_Head.max_list_seq + 1)
SELECT list
  FROM String_Head
 WHERE list_seq = 1
   AND max_list_seq = (SELECT MAX(list_seq)
                         FROM Make_String);

Postgresで試した結果

list                
-----------------------------------
abc, bcd, cde, def, efg, fgh, ghi
(1 row)

確かにMySQLではエラーになった。

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RECURSIVE
String_Tail(list_seq, cat_string)
AS
(SELECT list_seq, cat_string
   F' at line 1

p551のクエリに関して

CREATE TABLE Performance
(portfolio_id CHAR(7) NOT NULL,
 execute_date DATE NOT NULL,
 rate_of_return DECIMAL(13,7) NOT NULL);

---

INSERT INTO Performance
VALUES ('001', '2006-01-06', 0.5),
       ('001', '2006-01-07', 0.3),
       ('001', '2006-01-08', 0.6),
       ('001', '2006-01-09', 0.8),
       ('001', '2006-01-10', 0.1);


---


CREATE TABLE BigPi
(execute_date DATE NOT NULL,
 day_1 INTEGER NOT NULL,
 day_2 INTEGER NOT NULL,
 day_3 INTEGER NOT NULL,
 day_4 INTEGER NOT NULL,
 day_5 INTEGER NOT NULL);

---

INSERT INTO BigPi
VALUES ('2006-01-06', 1, 0, 0, 0, 0),
       ('2006-01-07', 0, 1, 0, 0, 0),
       ('2006-01-08', 0, 0, 1, 0, 0),
       ('2006-01-09', 0, 0, 0, 1, 0),
       ('2006-01-10', 0, 0, 0, 0, 1);
SELECT portfolio_id,
       (SUM((1.00 + P1.rate_of_return) * M1.day_1) *
        SUM((1.00 + P1.rate_of_return) * M1.day_2) *
        SUM((1.00 + P1.rate_of_return) * M1.day_3) *
        SUM((1.00 + P1.rate_of_return) * M1.day_4) *
        SUM((1.00 + P1.rate_of_return) * M1.day_5)) AS product
  FROM Performance AS P1, BigPi AS M1
 WHERE M1.execute_date = P1.execute_date
  AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10'
 GROUP BY portfolio_id;

途中結果がsumを経由しているので値として0になっているのか、nullが含まれているから0担っているのかわからない、という問題があり対策のCASE式として以下のものが書籍中に紹介されていた

CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_N)=0.00
  THEN CAST(NULL AS DECIMAL(13,7))
  ELSE SUM((1.00 + P1.rate_of_return) * M1.day_N)
END

この対策は結局P1.rate_of_returnに-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?ということを主張しました(が検証しきれなかった)

上記を適用したクエリと全体は以下のようになる

SELECT portfolio_id,
       (       
       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_1)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_1)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_2)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_2)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_3)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_3)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_4)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_4)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_5)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_5)
       END
        ) AS product
  FROM Performance AS P1, BigPi AS M1
 WHERE M1.execute_date = P1.execute_date
  AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10'
 GROUP BY portfolio_id;

とりあえず実行してみたところ結果は変わらない

portfolio_id |                product                
-------------+---------------------------------------
001          | 6.17760000000000000000000000000000000
(1 row)

適当にPerformanceテーブルからレコードを抜くとnullになった

portfolio_id | product
-------------+---------
001          |        
(1 row)

もちろん対策前のクエリでは結果は0になります

portfolio_id |                product                
-------------+---------------------------------------
001          | 0.00000000000000000000000000000000000
(1 row)

結局P1.rate_of_returnに-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?

上記の自分の疑問が検証できそうなデータを作成しました

DELETE FROM Performance;
INSERT INTO Performance
VALUES ('001', '2006-01-06', 0.5),
       ('001', '2006-01-07', 0.3),
       ('001', '2006-01-08', 0.6),
       ('001', '2006-01-09', -1),--これ!
       ('001', '2006-01-10', 0.1);

するとやはり結果はNULLになりました

 portfolio_id | product
--------------+---------
 001          |        
(1 row)

ただし利率が-1になるような金融というか口座は存在し得ないと思うのでこちらの不具合は多分起こらないかも。 というか普通はこの辺の処理はホスト側の言語でやると思います。

感想

特に本書中で触れられていませんが、BigPiの中身のデータのような感じで 単位行列のようなデータを利用した変態なクエリが多かった印象です。

セルコの他の本でグラフ理論?を扱っているものがありましたが、 線形代数(行列)を使ったクエリ、テーブル設計、RDBMSの実装などもあればさらに変な使い方ができて面白いかも知れません。

*1:おそらく自分が出たことのある会で一番進捗が悪かったと思います

第20回横浜Go読書会に参加

初参加です。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

Goを学び始めた動機などは

yuyubu.hatenablog.com

にも書いていますが、インタプリタの本でGoのコードをちょっとみたのが大きかったかなと思います。*1

結局先日からは章末の連取問題を解くのに必死で全然読み進められていません。(p146で止まっている) 会場に向かう電車の中で軽く流し読みをして挑みました。

今回は13章のGo言語と並列処理の240ページからでした

Go1.11

Go1.11が近日発表された、ということで、リリースノートを軽く読みました。

https://golang.org/doc/go1.11

Webassemblyサポート

サンプルを試したところ、ChromeのコンソールにHello Worldが出せたらしい

Goのデバッガ

最適化したコードでもデバッグ情報がかなり出るようになった

Go fmtが少々変わる

古いバージョンのGoで静的解析が通っていたソースが 今回のバージョンでフォーマットの違いで警告が出る恐れがある

  • CIのバージョンが違うと怒られる可能性がある

Go2

まだ仕様は出ていないが、軽くディスカッションされている

  • generics
  • エラー処理に関して重要な変更が2件

Go 1.12は半年後くらい。 いつ出るかわからない

mutexはgorutin間で通信する時に重要

書き込みが1つだけであっても、 他のスレッド(gorutin)から「書いた」値を読めるように書き込みの完了までをロックする必要がある。 *2*3

メモリモデルは

https://golang.org/ref/mem#tmp_5

が詳しい

重い処理をマルチスレッドで実行する場合はCPUの物理的な個数分のプロセスに分解するとパフォーマンスが良い

Goではruntime.NumCPU()でCPUの個数を取得できるが、こちらで確認できるのは物理CPUの数ではなく、 論理CPU の数であるため注いが必要。 本書に記載されている通り、SMT(Simultaneous Multi-Threading)機能などにより、CPUのコア数が多く評価されることがある。 自分の環境で試したところ、デュアルコアであるにも関わらず、4が帰ってきた。

Goでは並列を保証しない。

平行だけ。これは各CPUに割り当てるプロセス「プロセス」は作れても 空いてる新規にプロセスを割り当てるCPUを増やすなどの動的な制御をGo内だけで処理できない。

実行じにGOMAXPROCの環境変数で実行時に指定するか、OSのスケジューラによる割り当てに任せる。

p242で配列が1000万件あったら,という話題になった

package main

import (
    "fmt"
    "time"
)

func main() {
    tasks := []string{
        "cmake ..",
        "cmake. --build Release",
        "cpack",
    }
    for _, task := range tasks {
        go func() {
            fmt.Println(task)
        }()
    }
    time.Sleep(time.Second)
}
  • 上記のコードは動機をとっていないので最初のgoroutineの実行時にfor文が周りきった状態になり最後の要素が3回出力される、という悪い見本コードです.
    • ちなみに動機をSleepを使って取るのもあまり良い手本ではありません
  • 流石に1万もあれば最後ではなく、最初の要素が出るでしょう、という話になった
  • 結局はスケジューラーによるのでは?

実験してみました。まずはサンプルコードのまま3件でそのまま動かしました。

cpack
cpack
cpack

本書の記載通り、最後の要素であるcpackが帰ってきました

ではsliceの中身を100個に増やしてみましょう

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(100)

    tasks := []string{

        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
    ...(略)
        "99",
        "100",
    }
    for _, task := range tasks {
        go func() {
            fmt.Println(task)
            wg.Done()
        }()
    }
    wg.Wait()
}

このコードで実験してみました

注:前のコードでmain終了を待たせる同期を取っている箇所、time.Sleep(time.Second)の期限で処理が完了しなかったのでWaitGroupを使っています*4

結果

13
6
30
31
41
10
54
54
54
54
54
54
54
54
46
54
54
54
54
62
68
68
68
68
68
68
68
68
78
78
60
62
68
68
68
54
68
70
78
68
78
78
78
78
78
62
78
78
78
78
78
100
100
100
100
78
100
100
78
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
73
100

という出力が得られました。

一番最初/最後の要素が必ず出るという訳ではなさそうです。 結局は内部でどのようにスケジューリングしているか、という話なのでしょうか。

sync/atomicパッケージはgoで書かれていない

このあたりのコードは全てアセンブリで描かれている。

CPUごとに処理をさせたいときはCounting semaphoreを使う

Wikipediaによると、

任意個の資源を扱うセマフォをカウンティングセマフォ、値が0と1に制限されている(ロック/アンロック、使用可能/使用不可の意味がある)セマフォをバイナリセマフォと呼ぶ。後者はミューテックスと同等の機能を持つ。

とのことです

https://ja.wikipedia.org/wiki/セマフォ

セマフォは大学の時にOSの単元で軽く学習しましたが、 あまり覚えておらず、他の分散系の読書会でも出てきたので、どこかで復習しておきたいと思います。

interface{}はVariant型

読んでいるコードのところでわからない箇所があって助け舟を出してもらったりしました。

var count int
pool := sync.Pool{
  New: func() interface{} {
    count++
    return fmt.Sprintf("created:%d", count)
  },
}
pool.Put("manualy added: 1")
pool.Put("manualy added: 2")
fmt.Println(pool.Get())//manualy added: 1
fmt.Println(pool.Get())//manualy added: 2
fmt.Println(pool.Get())//created:1

poolに中身がないとNew:~以降が実行されてcountがインクリメントされた後string型が返されます。

interface{}の部分をStringに変えたり色々試してみましたが、まだこの部分の文法が何をやっているのかよくわかりませんでした。

この辺は時間をかけてGoを学んで吸収したいと思います

感想

Javaの有名な本を沢山訳されていたり、大きな講演会の基調講演をされている柴田さんがやられている勉強会なので、 前から気になっていましたが、敷居が高いかなと思い参加せずにいましたが、この読書会はそこまでハードコアではないと耳にしたので*5、思い切って参加してみました。

今回の範囲ですが、GoはランタイムがOSのような資源管理をしているのが面白いと思いました。

柴田さんが懇親会で「若い人はまともなAPI設計ができない、経験が積めていない」という趣旨のことをなんども仰られていました。

柴田さんが訳されたAPI設計の極意、10%ほど読んで放置になっていたので、こちらも早いうちに読まないといけないと思います。

パタヘネとコンピュータネットワーク(セスぺの試験までには読み終わりたいがこちらもペース的に若干きつい)も平行して読みたいので、勉強することが多くて大変だなと思います。

*1:言語処理系に興味があったので直近で発売された本を片っ端から眺めています。

*2:この本には出てこないらしい。

*3:Goのメモリモデルに関係している

*4:今回の読書会で得た知見を早速使ってみました

*5:柴田さんのJava/Go研修はかなり厳しいようです

golangを触って見た雑感

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

C言語はいろいろ大変だからgoでシステムプログラミングを学ぼうぜ。 という趣旨の本を読んでいる。ちょうど半分くらい(146/338)読めたのでgoの雑感でも適当に箇条書きで書いてみる。*1

goはtcfmで何度か触れられていたり、インタプリタ作る本が出ていたり

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

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

で結構興味がある。直近でOSの仕組みを結構学んだのでそれに近いことも書かれているし、横浜で楽しそうな読書会をやっている

yokohama-go-reading.connpass.com

などなど、色んなタイミングが重なってちょうどいいかなと思って手に取ってみた次第です

  • go run <.gofile>で動かすので、一見インタプリタっぽいと思ったが、コンパイルしてネイティブの実行バイナリを作成できる
$ go helloworld.go
helloworld

$ go build helloworld.go

$ ls
helloworld       helloworld.go

$ file helloworld
helloworld: Mach-O 64-bit executable x86_64

$ ./helloworld
helloworld
  • 試していないがクロスコンパイルとかもできるらしい

https://qiita.com/Jxck_/items/02185f51162e92759ebe

  • アセンブルしてみようと思ったがバイナリサイズができすぎてよくわからなかった
$ ls -alt helloc helloworld
-rwxr-xr-x  1  staff     8432  9  3 02:14 helloc
-rwxr-xr-x  1  staff  2011816  9  3 02:13 helloworld
  • hellocはC言語のhelloworldだけど、goはその200倍近いサイズがある。

    • ランタイムや何かでかいライブラリをリンクしているのかな。
  • goはgoとアセンブラで書かれている

  • インターフェースの説明がわかりやすい
    • interfaceを実装したい型の宣言時にinterfaceの情報を含めなくて良い
    • 確かrubyもこんな感じの仕様だった気がする(publicメンバが同一ならポリモーフィックな関係がある)
      • ちょっと調べましたがダックタイピングと言われるものですね

ダック・タイピング - Wikipedia

type Talker interface{
  Talk()
}

//構造体GreeterにはTalkerを明示的に実装しているという情報は含めない
//Javaのimplements的なやつがない
type Greeter struct{
  name String
}

//この宣言の時の(g Greeter)はレシーバ。どの構造体にインスタンスメソッドを実装するか、的なやつ
func(g Greeter) Talk(){
  fmt.Printf("hello,"g.name)
}
func main(){
  var talker Talker
  talkder = &Greeter{"wozozo"}
  talkder.Talk()
}
  • メソッド名は大文字で書くとpublic,小文字で書くとprivateの可視性をもつ
  • goのbufio.Writerのデフォルトバッファサイズは4096
    • いろんなバッファ、4096のものが多い気がする
  • goのprint文は書いた分だけシステムコールが発行される

  • p44より

    (goには)残念ながらErlangの文法に組み込まれているバイナリパターンマッチのような強力なバイナリ解析はありません

    • バイナリパターンマッチ、よくわからないけどErlang詳しい人に聞いたとところ、ヘッダの解析とかにめちゃくちゃ使えるらしい。
  • goは関数呼び出し時に関数名に"go "をつけることでgoroutine(軽量スレッド)と呼ばれる並列処理が使える
    • goのランタイムがOSっぽくこのスレッドを管理する
      • CPUからOSからはプロセスが増えたようには見えないらしい
      • Cだとfolkするとtopコマンドとかで表示されるプロセスが増える
  • goにはジェネリクス今の所ない
    • go2で入るらしい
  • goには例外がない
    • channelというものを使って処理の動機を取ることができる
    • FIFOっぽく使える
  • rustみたいに特定のポスト特定の言語みたいなのを狙っている感じはない
    • rustはC++の代替を目標?にしている空気がある
  • goはML系言語をやっている人たちからあまり好ましく思われていない?らしい

本書はgoそのものの情報よりもシステムプログラミングのトピックが多い。HTTP2やファイルディスクリプタ,RESTやunix socketなどなど。

聞きかじりの知識と適当にググった情報でやってるけど大丈夫かな。*2

これはgoに限った話ではないけど、後発の言語は、以前の言語の問題点とかハマりポイントをうまく回避したり、 いろんな言語で成功している概念などを取り入れている印象があるので、特に使わなくても新しい言語を学んでおくメリットはあると思し、実際学んでみると結構楽しい。

*1:最初の環境構築をサボったので大急ぎで演習問題を解いて追い付こうとしているところです

*2:それでもある程度挫折せずに進めているのはgoの凄いところなんじゃないかなと思う