Goの標準ライブラリだけでWebサーバを作る

# はじめに

個人開発での勉強でGoを触ってみたので備忘録も兼ねて残すことにしました。

Goでは標準ライブラリの"net/http"パッケージを使用することで非常に少ないコードでWebサーバを作成することができます。

サンプルとしてHello Worldのような初歩的なものからJSONを返すAPIチックなものまで簡単なアプリケーションを作成します。


## 環境設定

## PC

- MacBook Air M1, 2020
- macOS Ventura 13.0.1


## Goのインストール

まずはGolangのバージョン管理ツールである「goenv」をインストールし、その後goenv経由でGoをインストールします。

goenvを使わずに公式サイト（ https://go.dev/dl/ ）からGoのパッケージをDL&インストールすることも可能です。

ちなみにHomebrew経由でgoenvをインストールするとなぜかインストール可能なGoのバージョンが古かったので諦めました。


### goenvのインストール

gitから最新版を取得します。

```bash
git clone https://github.com/syndbg/goenv.git ~/.goenv
```

zshrcやbashrcに以下を追加した後、sourceコマンドを使い設定を適用します。

```bashrc
export GOENV_ROOT="$HOME/.goenv"
export PATH="$GOENV_ROOT/bin:$PATH"
eval "$(goenv init -)"
```

以下のコマンドでgoenvのバージョンが確認できればOKです。

```bash
goenv --version
```

### Goのインストール

まずはインストール可能なGoのバージョン一覧を表示します。

```bash
goenv install --list
```

バージョンを指定してGoをインストールします。

```bash
goenv install 1.20.1
```

global設定を行います。


```bash
goenv global 1.20.1
```

以下のコマンドでGoのバージョンが確認できればOKです。

```
go version
```

## Webサーバを作成する

### Hello World表示

まずはHello Worldという文字を返すだけのWebサーバを作成してみます。
main.goファイルを作成し以下を記述します。

```go:main.go
package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
```

以下のコマンドでプログラムを実行してサーバーを起動します。

```bash
go run main.go
```

ブラウザで`http://localhost:8080`にアクセスすると「Hello World」と表示されます。

### パラメータを受け取る

次にパラメータを受け取れるようにします。
`FormValue`を使いキーを指定することでパラメータの値を取得することができます。
以下の例ではnameが指定されているかどうかで出力するメッセージを変えています。


```go
package main

import (
    "fmt"
    "net/http"
)

const baseMessage = "Hello World"

func handler(w http.ResponseWriter, r *http.Request) {
    name := r.FormValue("name")
    if name == "" {
        fmt.Fprint(w, baseMessage)
        return
    }

    fmt.Fprintf(w, "%s, %s", baseMessage, name)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
```

ブラウザで`http://localhost:8080?name=Taro`にアクセスすると「Hello World, Taro」と表示されることが確認できます。


nameを指定しない時はこれまで通り「Hello World」を返すようになっています。

#### POSTのみ許可

POSTのみ許可したい場合は次のようにメソッドの判定を追加します。

```diff_go
// ... 略

func handler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+		return
+	}

// ... 略
```

`http.Error`を用いることで、エラーメッセージとHTTPステータスコードを同時に設定することができます。

試しにブラウザでアクセスするとGETメソッドでのアクセスとなるので、「Method Not Allowed」というレスポンスと共に405エラーが返ってきます。


ターミナルからcurlを使いPOSTメソッドでアクセスするとエラーにはならず「Hello World, Taro」が返ってきます。これでHTTPリクエストメソッドに応じた制御が効いていることがわかりました。

```zsh
$ curl -X POST "http://localhost:8080" -d "name=Taro"
Hello World, Taro
```

#### レスポンスをJSONで返す

次にAPIを作ることを想定してレスポンスをJSONで返すようにしてみます。
JSONを扱うためには「encoding/json」というパッケージを使用します。

また、Userという構造体を作り名前を保持できるようにしました。
(リクエストパラメータで指定されたnameをUserにセットしてJSONのレスポンスとして返す構成)
また、nameが指定されていない場合は400エラーになるようにエラー処理を追加しています。


```go
package main

import (
    "net/http"
    "encoding/json"
)

type User struct {
    Name string `json:"name"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return
    }

    name := r.FormValue("name")
    if name == "" {
		http.Error(w, "name is required", http.StatusBadRequest)
        return
    } else {
        user := User{
            Name: name,
        }
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        json.NewEncoder(w).Encode(user)
    }
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
```

curlでアクセスした結果はこちら。`-i`でレスポンスのヘッダーも出力しています。

```zsh
$ curl -X POST "http://localhost:8080" -d "name=Taro" -i
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Tue, 28 Feb 2023 14:23:10 GMT
Content-Length: 16

{"name":"Taro"}
```

ちゃんとJSONで返ってきましたね！
Content-Typeも指定した値になっていることが確認できます。

## おわりに

Go言語を使うことで標準ライブラリだけでも非常に簡単にWebサーバを構築できることがわかりました。

また、今回は紹介しませんでしたがGinやEchoなど様々なMVCフレームワークも提供されているので、より効率的にWebアプリケーションの開発を行いたい場合はこれらのフレームワークを導入してみると良さそうです。

Goを触ったのは今回が初めてですが、他の言語と比べてエラーハンドリングの方法が全く異なる思想で面白いと感じました。(try〜catchのような例外機構ではなく、メソッドの戻り値としてエラーを返す。ただし、defer、panic、recoverを使うことで同じことは可能)