チャットアプリからgoroutineが実際にどう使われているか見てみよう！

# はじめに

この記事は[Go 3 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go3)の19日目です。

勉強のために`goroutine`を使うアプリを作りたいと思い、チャットアプリを作ろうと思いました。

web上でチャットアプリのようなインタラクティブなやり取りは、`websocket`を使うことが多いかと思います。goで使用するとなると[gorilla/websocket](https://github.com/gorilla/websocket)のプラグインを使うと思いますが、そのリポジトリ内にある[チャットアプリのサンプル](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go)がとてもシンプルで、丁度良いレベルだったのでとても参考になりました。

そこで今回は、そのチャットアプリのサンプルコードを説明していこうと思います！

# 書くこと
`goroutine`の基礎知識については全く書いてないです。代わりにチャットアプリのサンプルソースを通して、実際どのように`goroutine`が使用されているかを書いていきます。

# ファイル構成
サンプルアプリのファイルは以下となっております。

- main.go
- hub.go
- client.go
- home.html

各ファイルのポイントをピックアップして説明していこうと思います。

## main.go

```golang:main.go
...

func main() {
	flag.Parse()
	hub := newHub()
	go hub.run() // hubをgoroutineで起動
	http.HandleFunc("/", serveHome)
	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		serveWs(hub, w, r) // クライアントサイド（js）とwebsocketを接続
	})
	err := http.ListenAndServe(*addr, nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
```
`main.go`では、webサーバーの立ち上げとindexページのテンプレート表示を行っております。`hub.run()`を`goroutine`で起動して、ハブ（チャットルーム）の起動をしております。

## hub.go
```golang
package main

// Hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
	// Registered clients.
	clients map[*Client]bool

	// Inbound messages from the clients.
	broadcast chan []byte

	// Register requests from the clients.
	register chan *Client

	// Unregister requests from clients.
	unregister chan *Client
}

func newHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

func (h *Hub) run() {
	for {
		select {
		case client := <-h.register: // 入室
			h.clients[client] = true
		case client := <-h.unregister: // 退室
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast: // 全員に送信
			for client := range h.clients {
				select {
				case client.send <- message: // 送信
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}
```
こちらではチャットアプリのハブ（チャットルーム）となる部分です。`hub#run()`は前述したとおり`main.go`から`goroutine`で呼ばれます。`websocket`作成時に入室処理が走りクライアントが登録され、メッセージが送られてくるまで待機します。送られてくればbroadcast（全員へ送信）し、メッセージが各クライアントへ渡されます。

## client.go
```go:client.go
func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c // メッセージ取得のループを抜けたときは退室
		c.conn.Close()
	}()

	...

	for {
		_, message, err := c.conn.ReadMessage() // クライアントサイドから送られてきたメッセージを取得

		...

		c.hub.broadcast <- message // broadcastする
	}
}

func (c *Client) writePump() {
	...

	for {
		select {
		case message, ok := <-c.send: // broadcastで送られてきたメッセージ

			...

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			// Add queued chat messages to the current websocket message.
			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-c.send) // メッセージをクライアントサイド（js）へ送信
			}
			...
		}
	}
}

...

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil) // httpリクエストをwebsocketにアップグレード
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
	client.hub.register <- client // 入室

	// Allow collection of memory referenced by the caller by doing all work in
	// new goroutines.
	go client.writePump() // broadcastされてきたメッセージをクライアントサイド（js）へ送る
	go client.readPump() // クライアントサイド（js）から送られてきたメッセージをbroadcastする
}
```

`serveWs()`は`/ws`のエンドポイントへアクセスが来たときに呼ばれます。入室処理後に`writePump()`と`readPump()`をgoroutineで呼びます。

`readPump()`では、`websocket`でクライアントサイド（js）から送られてきたメッセージをbroadcastします。`writePump()`では、broadcastされたメッセージをクライアントサイド（js）へ送ります。

## home.html
こちらindexページのテンプレートです。一緒にjsも書かれてます。jsではformに書かれたメッセージを`websocket`を利用してサーバーサイドと送受信してます。

```js:home.html

...

    document.getElementById("form").onsubmit = function () {
        if (!conn) {
            return false;
        }
        if (!msg.value) {
            return false;
        }
        conn.send(msg.value); // submitが押されたらwebsocketでサーバーにメッセージ送信
        msg.value = "";
        return false;
    };

    if (window["WebSocket"]) {
        conn = new WebSocket("ws://" + document.location.host + "/ws");
        conn.onclose = function (evt) {
            var item = document.createElement("div");
            item.innerHTML = "<b>Connection closed.</b>";
            appendLog(item);
        };
        conn.onmessage = function (evt) { // サーバーサイドからwebsocketでメッセージ取得時
            var messages = evt.data.split('\n');
            for (var i = 0; i < messages.length; i++) {
                var item = document.createElement("div");
                item.innerText = messages[i];
                appendLog(item); // チャット内容を表示
            }
        };
    }

...
```
つまり、

1. あるユーザーがメッセージ送信（js）
2. メッセージを受信し各クライアント（送信者も含む）へbroadcast（go）
3. 各クライアントはメッセージを受信し、jsへメッセージを送信（go）
4. メッセージを表示（js）

となります。

# さいごに
いかがだったでしょうか。実際に世の中で使用されているチャットアプリはもっと複雑だと思いますが、仕組み自体は結構シンプルだったのではないでしょうか。

`goroutine`を勉強しようとしても、何を作ろうかと迷って行き詰まる方も多いかなと思います。そんなときは、今回紹介したサンプルアプリを参考にチャットアプリを作ってみてはいかがでしょうか。

↓（おまけ）自分で作った簡単なチャットアプリ
![Screen Recording 2020-12-19 at 1.46.18.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137952/0d9e68fd-eebb-2f49-33cd-74c541d65c2c.gif)