Go/ginで外部APIリクエスト、結果を表示する簡易アプリを作成

# はじめに
ここ最近Go言語の入門を開始し、チュートリアル等で基本的な文法を学んだので、何か簡単なアプリを作成してみようと思い、ブラウザからアクセスするシンプルなアプリを作成してみました。

# 作成したアプリ
指定の都市の現在の天気情報を取得して結果（都市名・気温・天候）を画面に表示する簡易アプリを作成しました。（といっても都市は事前に指定したSelectリスト内の中から選択する形式）
天気情報は <a href="https://openweathermap.org/" target="_blank" rel="noopener noreferrer">OpenWeatherMap</a> から取得してます。
要アカウント作成ですが、フリーアカウントで1日1000リクエストまで使え、自分には十分なリクエスト数でした。

（画面キャプチャ）
画面で都市を選択

![スクリーンショット 2024-01-09 234212.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519408/018d11f7-08b5-816b-659d-84cd003f1d18.png)

取得ボタンクリック

![スクリーンショット 2024-01-09 234245.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519408/de6ce1ee-d7d3-2021-d2f6-736dae8ab4c3.png)



# 使用技術
* <a href="https://go.dev/" target="_blank" rel="noopener noreferrer">Golang</a>
* <a href="https://github.com/gin-gonic/gin" target="_blank" rel="noopener noreferrer">gin</a>
* <a href="https://openweathermap.org/" target="_blank" rel="noopener noreferrer">OpenWeatherMap</a>

# 作成手順
下記の内容にセクションを分けて記載していきます。（※Goはインストール済みであることを前提）
1. ginを使ってリクエストをハンドリングし、HTMLを返せるようにする
1. OpenWeatherMapにリクエストを送信し、取得したレスポンスを返せるようにする
1. 画面にOpenWeatherMapからのレスポンスを表示できるようにする
1. 画面で選択した都市の天気情報を表示できるようにする（最終的に今回作成したアプリ）

## 作成手順1-ginを使ってリクエストをハンドリングし、HTMLを返せるようにする

まずはアプリ実行用の環境を用意します。（環境はWindowsのWSL2-Ubunthディストリビューションを使用しています）
```shell
mkdir sample
cd sample
go mod init sample
touch main.go
```
`main.go`には、以下の内容を記述します。
（※ <a href="https://github.com/gin-gonic/gin?tab=readme-ov-file#running-gin" target="_blank" rel="noopener noreferrer">ginのREADME</a> を参考にしました）

```go:main.go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "test",
		})
	})

	r.Run()
}
```
但し、「"github.com/gin-gonic/gin"」のimportでコンパイルエラーになると思うので、下記を実行。

```shell
go mod tidy
```
すると、`go.mod`に「"github.com/gin-gonic/gin"」の定義が追加され、コンパイルエラーも解消しました。

さて、ここでいったん起動してみます。
```shell
go run main.go
```

<a href="http://localhost:8080/" target="_blank" rel="noopener noreferrer">http://localhost:8080/</a> にブラウザからアクセスすると、下記JSONが表示されるかと思います。

```json
{
    "message": "test"
}
```

これは、下記にてセットした内容をレスポンスとして返せているということですので、ここまででginを使ってルートへのアクセス時に指定のJSONレスポンスを返せるようになりました。

```go
c.JSON(http.StatusOK, gin.H{
			"message": "test",
		})
```


## 作成手順2-OpenWeatherMapにリクエストを送信し、取得したレスポンスを返せるようにする

続いて、今度はOpenWeatherMapから天気情報を取得してみます。
OpenWeatherMapを使うには無料のアカウントを作成してAPIキーを発行する必要があります。
アカウント作成周りは下記を参考にしました。

https://qiita.com/b-wind/items/06e19043a0cd70b10b03

APIキーが発行できたら、ブラウザから下記にアクセスしてみます。（※appidの部分は自身のAPIキーをセットする）

```
https://api.openweathermap.org/data/2.5/weather?id=2128295&appid=xxx
```

すると、下記のようなレスポンスが取得できます。（※クエリーパラメーターのidに指定しているのは、札幌市の都市ID）

```json
{
    "coord": {
        "lon": 141.3469,
        "lat": 43.0642
    },
    "weather": [
        {
            "id": 803,
            "main": "Clouds",
            "description": "broken clouds",
            "icon": "04n"
        }
    ],
    "base": "stations",
(略)
```

今度はGoプログラムからアクセスするように実装していきます。

`main.go`を下記に書き換えます。

```go:main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "test",
		})
	})

	r.GET("/weather", func(c *gin.Context) {
		weatherData, err := getWeatherData()
		if err != nil {
			fmt.Printf("error fetching weather data, %v", err)
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"city": weatherData.Name,
		})
	})

	r.Run()
}

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

func getWeatherData() (Weather, error) {
	apiURL := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?id=%s&appid=%s", "2128295", os.Getenv("OPENWEATHERMAP_API_KEY"))

	var weatherData Weather

	// APIからデータを取得
	resp, err := http.Get(apiURL)
	if err != nil {
		return weatherData, fmt.Errorf("error making request to OpenWeatherMap API: %v", err)
	}
	defer resp.Body.Close()

	// レスポンスボディを読み込む
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return weatherData, fmt.Errorf("error reading response body: %v", err)
	}

	// JSONデコード
	err = json.Unmarshal(body, &weatherData)
	if err != nil {
		return weatherData, fmt.Errorf("error decoding JSON: %v", err)
	}

	return weatherData, nil
}

```

(変更点)
* `/weather` のルーティングを追加
* `/weather` にアクセスしたときに `OpenWeatherMap` にリクエストして天気情報を取得する処理を追加

（※ `OpenWeatherMap` にアクセスする際はAPIキーを渡さないと認証エラーになるため、ここでは環境変数OPENWEATHERMAP_API_KEYから取得するようにしてます）

ここで、下記でプログラムを起動します。

```shell
OPENWEATHERMAP_API_KEY={your api key} go run main.go
```

<a href="http://localhost:8080/weather" target="_blank" rel="noopener noreferrer">http://localhost:8080/weather</a> にブラウザからアクセスすると、下記JSONが表示されるかと思います。

```json
{
    "city": "Sapporo"
}
```

ここまでで、`OpenWeatherMap` からデータを受け取り、それを返すところまで実装ができました。


## 作成手順3-画面にOpenWeatherMapからのレスポンスを表示できるようにする

続いて、`OpenWeatherMap` から受け取った内容を画面に表示するようにしていきます。
そこで下記の２つを行います。
* templates フォルダを作成し、この中にHTMLファイルを作成
* GoプログラムからJSONではなくHTMLを返すように修正

まずは「templates フォルダを作成し、この中にHTMLファイルを作成」を行っていきます。

```shell
mkdir templates
cd templates/
touch index.html
```

`index.html`には下記を記述します。
(`{{.CityName}}`の部分は任意のキー名を記述し、ここにginから値を埋め込むことができます)

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Weather Result</title>
</head>

<body>
    <h1 style="color: rebeccapurple;">Weather Information</h1>
    <p>都市名: {{.CityName}}</p>
</body>

</html>
```

続いては「GoプログラムからJSONではなくHTMLを返すように修正」です。
`main.go`を下記に書き換えます。

```go:main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.LoadHTMLGlob("templates/*")

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "test",
		})
	})

	r.GET("/weather", func(c *gin.Context) {
		weatherData, err := getWeatherData()
		if err != nil {
			fmt.Printf("error fetching weather data, %v", err)
			return
		}
		data := gin.H{
			"CityName": weatherData.Name,
		}
		c.HTML(http.StatusOK, "index.html", data)
	})

	r.Run()
}

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

func getWeatherData() (Weather, error) {
	apiURL := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?id=%s&appid=%s", "2128295", os.Getenv("OPENWEATHERMAP_API_KEY"))

	var weatherData Weather

	// APIからデータを取得
	resp, err := http.Get(apiURL)
	if err != nil {
		return weatherData, fmt.Errorf("error making request to OpenWeatherMap API: %v", err)
	}
	defer resp.Body.Close()

	// レスポンスボディを読み込む
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return weatherData, fmt.Errorf("error reading response body: %v", err)
	}

	// JSONデコード
	err = json.Unmarshal(body, &weatherData)
	if err != nil {
		return weatherData, fmt.Errorf("error decoding JSON: %v", err)
	}

	return weatherData, nil
}

```

(変更点)
* `r.LoadHTMLGlob`で`templates`フォルダ配下のHTMLを読み込めるようにする
* `gin.H`でHTMLに埋め込む値のキーバリューマップを作成
* ここまでの`c.JSON`ではなく`c.HTML`とすることでHTMLをレスポンスする

ここでプログラムを起動します。

```shell
cd ..
OPENWEATHERMAP_API_KEY={your api key} go run main.go
```

<a href="http://localhost:8080/weather" target="_blank" rel="noopener noreferrer">http://localhost:8080/weather</a> にブラウザからアクセスすると、下記画面が表示されるかと思います。

![スクリーンショット 2024-01-09 075940.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519408/1850214d-17ff-a6c4-868a-abaafa3a34ab.png)


## 作成手順4-画面で選択した都市の天気情報を表示できるようにする（最終的に今回作成したアプリ）

ここまでで、sampleフォルダ内のフォルダ構成は下記のようになっています。

```shell
# tree
.
├── go.mod
├── go.sum
├── main.go
└── templates
    └── index.html
```

「画面で選択した都市の天気情報を表示できるようにする」ために、`index.html`の１画面を使用していたところから、リクエスト送信画面と結果表示画面の２つに分けます。

`templates`フォルダに`result.html`を作成します。
`index.html`と`result.htm`をそれぞれ書き換えます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Weather App</title>
    <script>
        function getWeather() {
            // 選択された都市のIDを取得
            var cityId = document.getElementById("citySelect").value;

            // GETリクエストを送信
            window.location.href = "/weather?id=" + cityId;
        }
    </script>
</head>

<body>
    <h1 style="color: rebeccapurple;">Weather App</h1>

    <label for="citySelect">都市を選択してください:</label>
    <select id="citySelect">
        <option value="2130037">北海道</option>
        <option value="1850144">東京</option>
        <option value="1860291">神奈川</option>
        <option value="1856057">名古屋</option>
        <option value="1853909">大阪</option>
        <option value="1863967">福岡</option>
        <option value="1894616">沖縄</option>
    </select>

    <button onclick="getWeather()">取得</button>

</body>

</html>
```

```html:result.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Weather Result</title>
</head>

<body>
    <h1 style="color: rebeccapurple;">Weather Information</h1>
    <p>都市名: {{.CityName}}</p>
    <p>気温: {{.Temp}}℃</p>
    <p>天候: {{.Weather}}</p>
    <p>詳細: {{.Description}}</p>

    <a href="/">トップに戻る</a>
</body>

</html>
```

最後に、`main.go`を修正していきます。
（ファイルが長くなってきたのでいくつかのファイルに処理やモデル定義を分割しています。）

```go:model/weather.go
package model

// 天気の詳細情報
type WeatherInfo struct {
	ID          int    `json:"id"`
	Main        string `json:"main"`
	Description string `json:"description"`
	Icon        string `json:"icon"`
}

// 気温の詳細情報
type TempInfo struct {
	Temp float64 `json:"temp"`
}

// 天気情報
type Weather struct {
	Name    string        `json:"name"`
	Weather []WeatherInfo `json:"weather"`
	Main    TempInfo      `json:"main"`
}

```

モデル定義です。画面には都市名、気温、天候、天候詳細といった情報を表示するのでそれらの格納用のフィールドを用意しています。


```go:funcs/weather.go
package funcs

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sample/model"
)

func GetWeatherData(apiURL string) (model.Weather, error) {
	var weatherData model.Weather

	// APIからデータを取得
	resp, err := http.Get(apiURL)
	if err != nil {
		return weatherData, fmt.Errorf("error making request to OpenWeatherMap API: %v", err)
	}
	defer resp.Body.Close()

	// レスポンスボディを読み込む
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return weatherData, fmt.Errorf("error reading response body: %v", err)
	}

	// JSONデコード
	err = json.Unmarshal(body, &weatherData)
	if err != nil {
		return weatherData, fmt.Errorf("error decoding JSON: %v", err)
	}

	return weatherData, nil
}

func GetCityNameByID(cityID string) string {
	switch cityID {
	case "2130037":
		return "北海道"
	case "1850144":
		return "東京"
	case "1860291":
		return "神奈川"
	case "1856057":
		return "名古屋"
	case "1853909":
		return "大阪"
	case "1863967":
		return "福岡"
	case "1894616":
		return "沖縄"
	default:
		return "Unknown"
	}
}
```

`OpenWeatherMap` への通信処理をまとめています。
今回、都市名は事前に定義した中から選択する形としており、指定の都市ID(例：2130037)が画面から渡ってくるので、それに対応する都市名を取得する`GetCityNameByID`関数を用意しました。
（`OpenWeatherMap` から都市名は取得できますが英語表示なので、日本語で表示するためにこちらの関数を用意しました）


```go:model/server.go
package handler

import (
	"fmt"
	"net/http"
	"os"

	"sample/funcs"

	"github.com/gin-gonic/gin"
)

// Run Web Server
func Serve() {
	r := gin.Default()

	// templates配下のHTMLをロード
	r.LoadHTMLGlob("templates/*")

	r.GET("/", handleHome)
	r.GET("/weather", handleGetWeatherInfo)

	r.Run()
}

// 初期表示
func handleHome(ctx *gin.Context) {
	ctx.HTML(http.StatusOK, "index.html", nil)
}

// 天気情報取得
func handleGetWeatherInfo(ctx *gin.Context) {
	// idクエリパラメーターを取得
	cityID := ctx.Query("id")

	// OpenWeatherMap APIに対するリクエストURLを構築
	apiURL := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?id=%s&appid=%s", cityID, os.Getenv("OPENWEATHERMAP_API_KEY"))

	// APIからデータを取得
	weatherData, err := funcs.GetWeatherData(apiURL)
	if err != nil {
		fmt.Println(err)
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
		return
	}

	// HTMLテンプレートに渡すデータを生成
	data := gin.H{
		"CityName":    funcs.GetCityNameByID(cityID),
		"Temp":        fmt.Sprintf("%.2f", weatherData.Main.Temp-273.16),
		"Weather":     weatherData.Weather[0].Main,
		"Description": weatherData.Weather[0].Description,
	}

	// HTMLテンプレートを読み込み、データを埋め込んでHTMLを生成
	ctx.HTML(http.StatusOK, "result.html", data)
}

```

ハンドリング処理です。「/」と「/weather」がリクエストされたときの処理を定義しています。

```go:main.go
package main

import "sample/handler"

func main() {
	handler.Serve()
}
```

最後にmain関数です。中身はあっさりと、ハンドラーを実行するだけです。
ここまでで、下記でプログラムを実行すると [作成したアプリ](#作成したアプリ) に貼らせていただいたキャプチャの動きが確認できます。

```shell
OPENWEATHERMAP_API_KEY={your api key} go run main.go
```

以上になります。

下記にまとめていただいているようにgin以外にもフレームワークがいくつかあるようなので、それらも触ってみようと思います。

https://qiita.com/yumin/items/5de33b068ead564ebcbf