初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

スポンサーリンク

いまからのGo(1)



サトツ「行け!ゴーファー 100万ループだ!」

本を読んでも実際に何か作ってみないと全く理解が進まないのでサンプルを組みながら理解していく。今更な内容だけど備忘録ってことで。

環境

OS Windwos 10 Pro
Go 1.6.2
IntelliJ IDEA 2016.2
Goプラグイン 0.12.1724
GB 0.4.x

環境構築は以下の記事のままだが、IDEAとGoプラグインのバージョンが更新されている。

osa030.hatenablog.com

osa030.hatenablog.com

なお、コマンドラインはGitBashである。

作成するもの

せっかくならGoルーチンを使うものがいいので、ネットワークプログラムなんてどうだろうか。ということで

漢は黙ってTCP/IP。

なんだけども、クライアント側から文字列を送るとサーバ側が大文字変換して応答してくれるシンプルなechoサーバ、

ビッグECHOサーバを作りましょう。

プロジェクト構成

「pachimongo」がプロジェクトのルートフォルダ。

$ cd pachimongo/
$ mkdir src

こんな感じの構成。

$ tree src/ -F
src/
`-- cmd/
    |-- client1/
    |   `-- main.go
    `-- server1/
        `-- main.go

ソースファイル

ビッグECHOサーバ

処理はこんな感じ

  • TCPで5555番で接続を待ち受け
  • 接続があると以下ループ
    • クライアントからの要求を受信
    • 大文字変換
    • クライアントへ応答を送信
package main

import (
	"bufio"
	"log"
	"net"
	"strings"
)

func main() {

	service := ":5555"

	log.Println("start server", service)
	defer log.Println("stop server")

	listener, err := net.Listen("tcp", service)
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	log.Println("waiting ....")
	conn, err := listener.Accept()
	if err != nil {
		log.Println("ERROR:", err)
		return
	}

	for {
		msg, err := bufio.NewReader(conn).ReadString('\n')
		if err != nil {
			log.Println("ERROR:", err)
			break
		}
		msg = strings.Trim(msg, "\n")
		log.Printf("RECV[%s]\n", msg)

		resp := strings.ToUpper(msg)

		_, err = conn.Write([]byte(resp + "\n"))
		if err != nil {
			log.Println("ERROR:", err)
			break
		}
		log.Printf("SEND[%s]\n", resp)
	}
}

ECHOクライアント

処理はこんな感じ

  • TCPでローカルホスト(127.0.0.1)の5555番に接続
  • 接続成功後は以下ループ
    • 標準入力から文字列を取得
    • サーバへ文字列を送信
    • サーバからの応答を受信
package main

import (
	"bufio"
	"log"
	"net"
	"os"
	"strings"
)

const EOL = '\r' // for WINDOWS

func getInput() (string, error) {
	msg, err := bufio.NewReader(os.Stdin).ReadString(EOL)
	if err != nil {
		return "", err
	}
	return strings.Trim(msg, string(EOL)), nil
}

func main() {
	service := "127.0.0.1:5555"

	log.Println("start client", service)
	defer log.Println("stop client")

	conn, err := net.Dial("tcp", service)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	for {
		msg, err := getInput()
		if err != nil {
			log.Println("ERROR:", err)
			break
		}

		_, err = conn.Write([]byte(msg + "\n"))
		if err != nil {
			log.Println("ERROR:", err)
			break
		}
		log.Printf("SEND[%s]\n", msg)

		resp, err := bufio.NewReader(conn).ReadString('\n')
		if err != nil {
			log.Println("ERROR:", err)
			break
		}
		resp = strings.Trim(resp, "\n")

		log.Printf("RECV[%s]\n", resp)

	}
}

注意点として、今回標準入力やTCPコネクションからの文字列取得時にbufioパッケージのReadString()メソッドを用いている。

bufio - The Go Programming Language

このメソッドは引数に指定したデリミタまで文字列を読み込み続けてくれるのだが、今回のソースコードでは以下のようにデリミタを使い分けている。

  • 標準入力からの読み込み時は「\r」
  • TCPコネクション(ネットワーク)からの読み込み時「\n」

その理由は、WINDOWSの標準入力からの読み込みは改行コードが「\r\n」となる為である。改行コードレベルはGoランタイムで吸収してくれるものだと思っていたので最初ハマった。あと、ReadString()メソッドの結果にデリミタも含まれるのでそれをstrings.Trim()で除去。

strings - The Go Programming Language

ビルド&実行

gbコマンドでビルド。

$ gb build all
cmd/server1
cmd/client1

サーバ起動

$ bin/server1.exe
2016/07/21 21:01:40 start server :5555
2016/07/21 21:01:40 waiting ....

クライアント起動

$ bin/client1.exe
2016/07/21 21:01:58 start client 127.0.0.1:5555
hoge
2016/07/21 21:02:00 SEND[hoge]
2016/07/21 21:02:00 RECV[HOGE]
ahe
2016/07/21 21:02:02 SEND[ahe]
2016/07/21 21:02:02 RECV[AHE]
$

適当に文字入力した後にCtrl+Cでクライアントを終了。

2016/07/21 21:02:00 RECV[hoge]
2016/07/21 21:02:00 SEND[HOGE]
2016/07/21 21:02:02 RECV[ahe]
2016/07/21 21:02:02 SEND[AHE]
2016/07/21 21:02:02 ERROR: read tcp 127.0.0.1:5555->127.0.0.1:58568: wsarecv: An existing connection was forcibly closed by the remote host.
2016/07/21 21:02:02 stop server
$

クライアントとのTCPコネクションが切断されるとサーバも終了する。

ちょこっとメモ

TCP/IPについては、どの言語でもあまり変わらないし

一般常識なので省略。

今時の言語らしくC言語と比べたら標準パッケージが豊富なのでかなりカジュアルに書ける。

net - The Go Programming Language

で、Go言語特有の話をすると、Go言語には

例外はない。

実際はpanic()を使えば例外っぽい動きはできるらしいのだが、基本的には異常系処理は概ねこんな感じで書くようだ。

  • 返却値が複数返せるので呼び出し元でエラー判定が必要な処理についてはerrorを返す
  • defer構文で後処理を遅延実行する

返却値が複数返せるのでerrorを返す

今回のサーバ側の処理で標準パッケージを呼んでいるところはこんな感じ。

  • src/cmd/server1/main.go
func main() {

	service := ":5555"

	log.Println("start server", service)
	defer log.Println("stop server")

	listener, err := net.Listen("tcp", service)
	if err != nil {
		log.Fatal(err)
	}
}

net.Listen()は以下のような処理になっている。

  • net/dial.go
func Listen(net, laddr string) (Listener, error) {
	addrs, err := resolveAddrList("listen", net, laddr, noDeadline)
	if err != nil {
		return nil, &OpError{Op: "listen", Net: net, Source: nil, Addr: nil, Err: err}
	}
	var l Listener
	switch la := addrs.first(isIPv4).(type) {
	case *TCPAddr:
		l, err = ListenTCP(net, la)
	case *UnixAddr:
		l, err = ListenUnix(net, la)
	default:
		return nil, &OpError{Op: "listen", Net: net, Source: nil, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}}
	}
	if err != nil {
		return nil, err // l is non-nil interface containing nil pointer
	}
	return l, nil
}

このように、エラーが伴う関数は戻り値とは別にエラー発生時の戻り値(error)を返すのがGoのプログラミングスタイルらしい。初めて遭遇するパターンなので最初は面を食らうが、ある意味定型句なので覚えるしかない。因みに返却されたエラーを無視する場合にも意図的に処理を書かなければいけないので例外のようにキャッチし忘れることもなさそう。ということで、自作の関数やメソッドについても同様にすることでコードの統一感が増すので積極的に取り入れていきたい。

defer構文で後処理を遅延実行する

Go言語ではdefer構文を使うことで関数が終了する際に実行すべき処理を記述することができる。

  • src/cmd/server1/main.go
func main() {

	service := ":5555"

	log.Println("start server", service)
	defer log.Println("stop server")

このようにmain関数前半でdefer構文で宣言された処理は

2016/07/21 21:02:02 ERROR: read tcp 127.0.0.1:5555->127.0.0.1:58568: wsarecv: An existing connection was forcibly closed by the remote host.
2016/07/21 21:02:02 stop server

実行時にはmain関数を抜けた時に実行される。具体的な用途としては

  • src/cmd/client1/main.go
func main() {
	service := "127.0.0.1:5555"

	log.Println("start client", service)
	defer log.Println("stop client")

	conn, err := net.Dial("tcp", service)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

のように、取得したリソースの解放処理を取得直後にdefer定義することで、コード上の見通しを良くする意味合いがあるようだ。勿論、定義後の関数内のどこからreturnしても呼ばれることが保証されるのであるルートで呼び忘れるということもない。但し、関数から抜ける前にプログラム自体が終了してしまうと呼び出されない点には注意が必要。

$ bin/client1.exe
2016/07/21 21:01:58 start client 127.0.0.1:5555
hoge
2016/07/21 21:02:00 SEND[hoge]
2016/07/21 21:02:00 RECV[HOGE]
ahe
2016/07/21 21:02:02 SEND[ahe]
2016/07/21 21:02:02 RECV[AHE]
$

defer定義している「log.Println("stop client")」が出力されないのは、Ctrl+Cで強制終了している為。

今回のエラー関連の処理については、以下の記事が参考になる。

blog.amedama.jp

ということで、まずはエラー処理について学習した。実のところ、今回のビッグECHOプログラムにはそもそもGoルーチンすらでてきておらず他にも色々とツッコミどころが満載なのであるが、今後の記事の為にあえて放置しているのだッ!(多分)。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

スターティングGo言語

スターティングGo言語

スポンサーリンク