はじめに
ここ最近Go言語の入門を開始し、チュートリアル等で基本的な文法を学んだので、何か簡単なアプリを作成してみようと思い、ブラウザからアクセスするシンプルなアプリを作成してみました。
作成したアプリ
指定の都市の現在の天気情報を取得して結果(都市名・気温・天候)を画面に表示する簡易アプリを作成しました。(といっても都市は事前に指定したSelectリスト内の中から選択する形式)
天気情報は OpenWeatherMap から取得してます。
要アカウント作成ですが、フリーアカウントで1日1000リクエストまで使え、自分には十分なリクエスト数でした。
(画面キャプチャ)
画面で都市を選択
取得ボタンクリック
使用技術
作成手順
下記の内容にセクションを分けて記載していきます。(※Goはインストール済みであることを前提)
- ginを使ってリクエストをハンドリングし、HTMLを返せるようにする
- OpenWeatherMapにリクエストを送信し、取得したレスポンスを返せるようにする
- 画面にOpenWeatherMapからのレスポンスを表示できるようにする
- 画面で選択した都市の天気情報を表示できるようにする(最終的に今回作成したアプリ)
作成手順1-ginを使ってリクエストをハンドリングし、HTMLを返せるようにする
まずはアプリ実行用の環境を用意します。(環境はWindowsのWSL2-Ubunthディストリビューションを使用しています)
mkdir sample
cd sample
go mod init sample
touch main.go
main.go
には、以下の内容を記述します。
(※ ginのREADME を参考にしました)
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でコンパイルエラーになると思うので、下記を実行。
go mod tidy
すると、go.mod
に「"github.com/gin-gonic/gin"」の定義が追加され、コンパイルエラーも解消しました。
さて、ここでいったん起動してみます。
go run main.go
http://localhost:8080/ にブラウザからアクセスすると、下記JSONが表示されるかと思います。
{
"message": "test"
}
これは、下記にてセットした内容をレスポンスとして返せているということですので、ここまででginを使ってルートへのアクセス時に指定のJSONレスポンスを返せるようになりました。
c.JSON(http.StatusOK, gin.H{
"message": "test",
})
作成手順2-OpenWeatherMapにリクエストを送信し、取得したレスポンスを返せるようにする
続いて、今度はOpenWeatherMapから天気情報を取得してみます。
OpenWeatherMapを使うには無料のアカウントを作成してAPIキーを発行する必要があります。
アカウント作成周りは下記を参考にしました。
APIキーが発行できたら、ブラウザから下記にアクセスしてみます。(※appidの部分は自身のAPIキーをセットする)
https://api.openweathermap.org/data/2.5/weather?id=2128295&appid=xxx
すると、下記のようなレスポンスが取得できます。(※クエリーパラメーターのidに指定しているのは、札幌市の都市ID)
{
"coord": {
"lon": 141.3469,
"lat": 43.0642
},
"weather": [
{
"id": 803,
"main": "Clouds",
"description": "broken clouds",
"icon": "04n"
}
],
"base": "stations",
(略)
今度は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から取得するようにしてます)
ここで、下記でプログラムを起動します。
OPENWEATHERMAP_API_KEY={your api key} go run main.go
http://localhost:8080/weather にブラウザからアクセスすると、下記JSONが表示されるかと思います。
{
"city": "Sapporo"
}
ここまでで、OpenWeatherMap
からデータを受け取り、それを返すところまで実装ができました。
作成手順3-画面にOpenWeatherMapからのレスポンスを表示できるようにする
続いて、OpenWeatherMap
から受け取った内容を画面に表示するようにしていきます。
そこで下記の2つを行います。
- templates フォルダを作成し、この中にHTMLファイルを作成
- GoプログラムからJSONではなくHTMLを返すように修正
まずは「templates フォルダを作成し、この中にHTMLファイルを作成」を行っていきます。
mkdir templates
cd templates/
touch index.html
index.html
には下記を記述します。
({{.CityName}}
の部分は任意のキー名を記述し、ここにginから値を埋め込むことができます)
<!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
を下記に書き換えます。
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をレスポンスする
ここでプログラムを起動します。
cd ..
OPENWEATHERMAP_API_KEY={your api key} go run main.go
http://localhost:8080/weather にブラウザからアクセスすると、下記画面が表示されるかと思います。
作成手順4-画面で選択した都市の天気情報を表示できるようにする(最終的に今回作成したアプリ)
ここまでで、sampleフォルダ内のフォルダ構成は下記のようになっています。
# tree
.
├── go.mod
├── go.sum
├── main.go
└── templates
└── index.html
「画面で選択した都市の天気情報を表示できるようにする」ために、index.html
の1画面を使用していたところから、リクエスト送信画面と結果表示画面の2つに分けます。
templates
フォルダにresult.html
を作成します。
index.html
とresult.htm
をそれぞれ書き換えます。
<!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>
<!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
を修正していきます。
(ファイルが長くなってきたのでいくつかのファイルに処理やモデル定義を分割しています。)
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"`
}
モデル定義です。画面には都市名、気温、天候、天候詳細といった情報を表示するのでそれらの格納用のフィールドを用意しています。
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
から都市名は取得できますが英語表示なので、日本語で表示するためにこちらの関数を用意しました)
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」がリクエストされたときの処理を定義しています。
package main
import "sample/handler"
func main() {
handler.Serve()
}
最後にmain関数です。中身はあっさりと、ハンドラーを実行するだけです。
ここまでで、下記でプログラムを実行すると 作成したアプリ に貼らせていただいたキャプチャの動きが確認できます。
OPENWEATHERMAP_API_KEY={your api key} go run main.go
以上になります。
下記にまとめていただいているようにgin以外にもフレームワークがいくつかあるようなので、それらも触ってみようと思います。