
記事の概要
この記事では、Googleが開発したオープンソースのプログラム言語のGoを使って、Dockerコンテナで動作するWebAPIの開発環境の構築から開発、実行までの流れを紹介します。
この記事を参考に、GoやWebAPI開発のスタートの一助となれば幸いです。
今回の記事の対象者
この記事は次のような方に向けています:
- Goの開発環境を構築してアプリ開発を始めたい方
- Dockerコンテナを使ってサーバー(VPSやクラウド環境)を環境を統一したい方
- シンプルなWebAPIでGoの実装イメージを掴みたい方
はじめに
最近Rustをさわり始めて、同じシステムプログラミング系言語としてもGoに興味があったので勉強を始めてみました。
今回は、クラウドネイティブ向けの言語と噂になっているGoを使ってシンプルなWebAPIの開発をやってみます。
Goは、システムプログラミングに適したプログラミング言語で、Googleが開発しました。シンプルで使いやすく、高速なプログラムを作るために設計されています。主に以下の特徴があります。
- シンプル
- Goのコードは非常にシンプルになります。プログラムを書くときに余計な複雑さを避けるように設計されています。
- 高速
- Goで書かれたプログラムは非常に高速に実行されます。CやC++などの低レベル言語に匹敵する速度を持ちつつ、使いやすさを兼ね備えています。
- 並行処理
- Goの強力な特徴の一つが「goroutine」と呼ばれる並行処理の仕組みです。これにより、複数の処理を同時に実行することが非常に簡単になります。
Goは、以下のようなソフトウェア開発に適しています。
- クラウド&ネットワークサービス
- コマンドラインインタフェース(CLI)
- Webアプリ開発
- 開発運用やサイト信頼性エンジニアリング
Goについての詳細は、公式サイトをご覧下さい。
目次
WebAPI(天気情報取得ツール)概要
天気情報取得ツール
このアプリは、指定された都市の現在の天気情報を取得して、WebAPIのレスポンスとして取得するするシンプルなアプリです。
ユーザーは、WebAPIに都市名を入力し、その都市の天気情報をリアルタイムで取得します。
天気情報は、OpenWeatherMapのAPIを使用して取得します。実装にはAPIキーが必要なので事前に登録して、APIキーを取得して下さい。
プログラムの流れ
- 都市名をパラメータとして天気情報取得ツールを呼び出す
- 天気予報APIから天気情報を取得する
- 成功: 天気情報を表示する
- 失敗: エラーメッセージを表示する
Goの学習ポイント
天気情報取得ツールのソースコードを通して、Goの以下の基本機能について解説します。
- HTTPリクエストの処理
- JSONのエンコードとデコード
- HTTPサーバーの構築
- エラーハンドリング
- ロギング
- その他基本的が文法など
ソフトウェア・ハードウェア
必要なツール、ライブラリ、端末は以下の通りです。
開発ツール
以下、開発ツールとその公式サイトの一覧です。導入しておくと開発作業が楽になります。
ツール名 | 用途 |
---|---|
https://www.jetbrains.com/ja-jp/go/ | Go 開発者向けのIDEです。個人非商用利用や学生は無料となっています。 |
端末
以下、今回の環境を構築する対象の端末スペックです。
本記事で紹介するソフトウェアおよびツールは、筆者の個人的な使用経験に基づくものであり、公式のサポート外の設定や使用方法を含む場合があります。利用に際しては、公式サイトの指示およびガイドラインを参照し、自己責任で行ってください。
Go開発環境の構築
Goのインストール
Go言語は、今回のアプリを作るためのプログラミング言語です。Goの公式サイトからインストーラをダウンロードしてインストールします。
今回は、Apple macOS(ARM64)を選択します。今回は、以下のファイル名となります。
go1.23.0.darwin-arm64.pkg
インストールが終わったら、パソコンのコマンドライン(ターミナル)で以下を実行して、Goがちゃんと使えるか確認します。
go version
Goがインストール正常にされていれば、go versionコマンドでバージョン情報が表示されます。
(base) xxxxx@xxxxx ~ % go version
go version go1.23.0 darwin/arm64
(base) xxxxx@xxxxx ~ %
DockerとDocker Composeのインストール
Dockerは、アプリケーションをコンテナと呼ばれる仮想的な環境に入れて動かすためのツールです。Docker Composeは、複数のコンテナを一度に管理するためのツールです。
Docker Desktopのインストールは、以下の記事を参考にしてください。
Docker Desktopをインストールした後、以下のコマンドで正しくインストールされたか確認します。
docker --version
docker compose --version
DockerとDocker Composeがインストールされ、docker –versionとdocker-compose –versionでバージョン情報が表示されます。
(base) xxxxx@xxxxx ~ % docker --version
Docker version 27.1.1, build 6312585
(base) xxxxx@xxxxx ~ % docker compose version
Docker Compose version v2.29.1-desktop.1
(base) xxxxx@xxxxx ~ %
プロジェクトディレクトリのセットアップ
プロジェクト用のフォルダを作成し、その中で開発を進めます。
プロジェクトディレクトリの作成
プロジェクト専用のフォルダを作成します。今回は、weather-apiという名前にします。
mkdir weather-api
cd weather-api
weather-appというフォルダを作成し、その中に移動しカレントディレクトリを確認します。
(base) xxxxx@xxxxx test-pj % mkdir weather-api
(base) xxxxx@xxxxx test-pj % cd weather-api
(base) xxxxx@xxxxx weather-api % pwd
/Users/xxxxx/test-pj/weather-api
(base) xxxxx@xxxxx weather-api %
Goモジュールの初期化
Go言語のプロジェクトでは、モジュールと呼ばれる仕組みで必要なライブラリ(他の人が作った便利なコード)を管理します。
go mod init weather-api
go.modというファイルが作成され、Goモジュールが初期化されます。
(base) xxxxx@xxxxx weather-api % go mod init weather-api
go: creating new go.mod: module weather-api
(base) xxxxx@xxxxx weather-api % tree .
.
└── go.mod
1 directory, 1 file
(base) xxxxx@xxxxx weather-api %
WebAPI(天気情報)の開発
ソースコードの作成
プログラムのコードを書いて、アプリケーションの機能を実装します。
main.go ファイルの作成
プロジェクトディレクトリのルートにmain.goというファイルを作成し、以下のコードを書きます。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"io/ioutil"
"github.com/joho/godotenv"
)
type WeatherResponse struct {
Name string `json:"name"`
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
Description string `json:"description"`
} `json:"weather"`
}
func getWeather(city string) (WeatherResponse, error) {
var weatherResp WeatherResponse
apiKey := os.Getenv("API_KEY")
url := fmt.Sprintf("<http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric>", city, apiKey)
resp, err := http.Get(url)
if err != nil {
return weatherResp, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return weatherResp, fmt.Errorf("failed to get weather data: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return weatherResp, err
}
err = json.Unmarshal(body, &weatherResp)
if err != nil {
return weatherResp, err
}
return weatherResp, nil
}
func weatherHandler(w http.ResponseWriter, r *http.Request) {
city := r.URL.Query().Get("city")
if city == "" {
http.Error(w, "City parameter is required", http.StatusBadRequest)
return
}
weatherResp, err := getWeather(city)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(weatherResp)
}
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
http.HandleFunc("/weather", weatherHandler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Starting server on port %s...", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
go.mod と go.sum ファイルの作成
コードが書き終わったら、依存するライブラリをインストールします。
以下のコマンドを実行し、go.modとgo.sumを生成します。
go mod tidy
必要な依存関係がインストールされ、go.modとgo.sumファイルが生成されます。
(base) xxxxx@xxxxx weather-api % go mod tidy
go: finding module for package github.com/joho/godotenv
go: downloading github.com/joho/godotenv v1.5.1
go: found github.com/joho/godotenv in github.com/joho/godotenv v1.5.1
(base) xxxxx@xxxxx weather-api %
今回生成されたファイルの内容を以下に示します。
go.mod
module weather-api
go 1.23.0
require github.com/joho/godotenv v1.5.1
go.sum
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
Docker関連ファイルの作成
このアプリケーションをDockerで動かすためのファイルを作成します。
Dockerfile の作成
プロジェクトの中にDockerfileを作成し、以下の内容を記述します。これは、アプリケーションをDockerコンテナで動かすためのレシピのようなものです。
# ベースイメージとしてGoの公式イメージを使用
FROM golang:1.23-alpine
# ワーキングディレクトリを設定
WORKDIR /app
# Goの依存関係をコピーしてインストール
COPY go.mod go.sum ./
RUN go mod download
# アプリケーションのソースコードをコピー
COPY . .
# アプリケーションをビルド
RUN go build -o main .
# コンテナが起動する際に実行するコマンド
CMD ["./main"]
docker-compose.yml の作成
同じく、プロジェクトの中にdocker-compose.ymlを作成し、以下の内容を記述します。これは、複数のコンテナを管理するための設定ファイルです。
services:
weather-api:
build: .
ports:
- "8080:8080"
env_file:
- .env
environment:
- PORT=8080
env ファイルの作成
環境変数を管理するための.envファイルを作成し、APIキーやポート番号を設定します。
API_KEY=your_openweathermap_api_key
PORT=8080
アプリの実行
開発環境の起動とテスト
プロジェクトディレクトリの完成形を以下に示します。作成したファイルに抜け漏れがないかチェックしてください。
(base) xxxxx@xxxxx weather-api % tree
.
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── .env
└── main.go
それでは、開発環境を起動し、アプリケーションが正しく動作するかを確認します。
アプリケーションのビルドと実行
Docker Composeを使ってアプリケーションをビルドし、コンテナを起動します。
プロジェクトディレクトリで、以下のコマンドを実行します。
docker compose up --build
コンテナが起動します。
(base) xxxxx@xxxxx weather-api % docker compose up --build
[+] Building 0.8s (12/12) FINISHED docker:desktop-linux
=> [weather-api internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 531B 0.0s
=> [weather-api internal] load metadata for docker.io/library/golang:1.2 0.8s
=> [weather-api internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [weather-api 1/6] FROM docker.io/library/golang:1.23-alpine@sha256:d0 0.0s
=> [weather-api internal] load build context 0.0s
=> => transferring context: 682B 0.0s
=> CACHED [weather-api 2/6] WORKDIR /app 0.0s
=> CACHED [weather-api 3/6] COPY go.mod go.sum ./ 0.0s
=> CACHED [weather-api 4/6] RUN go mod download 0.0s
=> CACHED [weather-api 5/6] COPY . . 0.0s
=> CACHED [weather-api 6/6] RUN go build -o main . 0.0s
=> [weather-api] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:d39c0ea180a4a1252649d1860c9cb99ad1168c2f27c9a 0.0s
=> => naming to docker.io/library/weather-api-weather-api 0.0s
=> [weather-api] resolving provenance for metadata file 0.0s
[+] Running 2/1
✔ Network weather-api_default C... 0.0s
✔ Container weather-api-weather-api-1 Created 0.0s
Attaching to weather-api-1
weather-api-1 | 2024/08/17 11:29:04 Starting server on port 8080...
http://localhost:8080/weather?city=Tokyoにアクセスすると天気情報が表示されます。
以下に、取得したデータを示します。
{
"name": "Tokyo",
"main": {
"temp": 31.19,
"humidity": 75
},
"weather": [
{
"description": "few clouds"
}
]
}
アプリケーションのテスト
ブラウザやcurlコマンド、PostmanなどのAPIテストツールを使って、APIエンドポイントにアクセスし、動作を確認します。
アプリケーションが起動した状態で、ターミナルで以下のコマンドを実行します。
curl "<http://localhost:8080/weather?city=London>"
APIエンドポイントにアクセスし、正しい天気情報が返ってくることを確認します。
(base) xxxxx@xxxxx weather-api % curl "<http://localhost:8080/weather?city=London>"
{"name":"London","main":{"temp":20.43,"humidity":58},"weather":[{"description":"clear sky"}]}
(base) xxxxx@xxxxx weather-api %
コンテナの停止
開発が終了したら、Docker Composeを使ってコンテナを停止します。docker compose up –buildを実行したものと別のコンソールを開いて、プロジェクトディレクトリで以下のコマンドを実行してください。
docker compose down
コンテナが停止し、リソースが解放されます。
(base) xxxxx@xxxxx weather-api % docker compose down
[+] Running 2/2
✔ Container weather-api-weather-api-1 Removed 0.1s
✔ Network weather-api_default R... 0.0s
(base) xxxxx@xxxxx weather-api %
開発の続き・デバッグ
アプリケーションのコードを修正したり、新しい機能を追加したりする際の手順を紹介します。
コードの変更と反映
天気情報取得ツールの初期コードには、Go1.16以降で非推奨となっているパッケージの利用があります。非推奨パッケージの詳細は、公式サイトをご覧下さい。
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"io/ioutil" // 非推奨パッケージの利用
"github.com/joho/godotenv"
)
・・・省略・・・
// 非推奨メソッドを利用(ioutil.ReadAll)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return weatherResp, err
}
・・・省略・・・
このコードを推奨されているパッケージに変更します。また、変更したコードの前後にログを仕込んでコードが正しく実行されたことを確認します。
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"io" // リファクタリング後
"github.com/joho/godotenv"
)
・・・省略・・・
// 非推奨メソッドを利用しないようにリファクタリング
log.Printf("リファクタリングしたコードが実行されます")
body, err := io.ReadAll(resp.Body)
log.Printf("リファクタリングしたコードが実行されました")
if err != nil {
return weatherResp, err
}
・・・省略・・・
コードを修正した後、再度docker compose up –buildコマンドを実行して、コンテナを再ビルドおよび再起動します。
コードの変更がコンテナに反映され、再起動されたアプリケーションで動作を確認できます。再度、ブラウザやcurlコマンドからAPIを呼び出して、正常にデータが取得できるか確認してください。
ログの確認
コンテナのログを確認して、アプリケーションが期待通りに動作しているかどうかを確認します。
docker compose logs
ログを確認し、エラーや問題がないかチェックできます。今回は、先ほどリファクタリングしたコードの前後でログが出力されていることが確認出来ます。
(base) xxxxx@xxxxx weather-api % docker compose logs
weather-api-1 | 2024/08/17 11:31:34 Starting server on port 8080...
(base) xxxxx@xxxxx weather-api % docker compose logs
weather-api-1 | 2024/08/17 11:31:34 Starting server on port 8080...
weather-api-1 | 2024/08/17 11:32:09 リファクタリングしたコードが実行されます
weather-api-1 | 2024/08/17 11:32:09 リファクタリングしたコードが実行されました
(base) xxxxx@xxxxx weather-api %
終了とクリーンアップ
作業が終わった後、開発環境を終了し、不要なリソースをクリーンアップします。
コンテナの停止
Docker Composeを使ってコンテナを停止します。
docker compose down
クリーンアップ
不要になったDockerのリソースを削除します。
docker system prune
不要なDockerリソースが削除されます。削除後、<none>が削除されていることが確認出来ます。
削除前
(base) xxxxx@xxxxx weather-api % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
weather-api-weather-api latest d39c0ea180a4 18 minutes ago 330MB
<none> <none> fbb30e2063b9 40 minutes ago 330MB
削除後
(base) xxxxx@xxxxx weather-api % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
weather-api-weather-api latest d39c0ea180a4 18 minutes ago 330MB
コードの解説
天気情報取得ツールで使ったGoの基本機能を紹介します。更に詳しくGoのライブラリの詳細について知りたい方は、標準ライブラリの文書をご覧下さい。
1. パッケージ管理
package main
この行は、Goのプログラムがどのパッケージに属するかを示しています。mainというパッケージは、プログラムのエントリーポイントを示し、ここから実行が始まります。
Goのプログラムは複数のパッケージに分けることができ、他のファイルやディレクトリからコードを再利用することが可能です。
2. 変数とデータ型
var weatherResp WeatherResponse
•varキーワード: varは変数を宣言するために使います。ここでは、weatherRespという変数をWeatherResponseという型で宣言しています。
•データ型: Goにはさまざまなデータ型があります。たとえば、intは整数、stringは文字列、float64は小数点を含む数値です。このアプリでは、APIのレスポンスを扱うために構造体(後述)をデータ型として使用しています。
3. 関数の定義
func getWeather(city string) (WeatherResponse, error) {
// 関数の中身
}
•funcキーワード: funcは関数を定義するためのキーワードです。関数は、特定のタスクを実行するためのコードのまとまりです。
•引数: getWeather(city string)の部分で、cityという引数を指定しています。この引数は、関数が呼び出されたときに渡される情報(ここでは都市名)を表します。
•戻り値: (WeatherResponse, error)は、この関数が2つの値(天気情報のデータとエラー情報)を返すことを示しています。
4. エラーハンドリング
if err != nil {
return weatherResp, err
}
•エラーのチェック: Goでは、関数の戻り値としてエラー情報を返すのが一般的です。この例では、errがnil(エラーがない)でない場合、エラーが発生したと判断し、すぐに関数を終了してエラーを返します。これにより、エラーが発生した場合に適切に対応できます。
5. HTTPリクエストの処理
resp, err := http.Get(url)
•HTTPリクエスト: http.Get(url)は、指定したURLに対してHTTP GETリクエストを送信します。このアプリでは、天気情報を取得するためにAPIにリクエストを送っています。respにはリクエストの結果が、errにはエラー情報が格納されます。
6. JSONのエンコードとデコード
err = json.Unmarshal(body, &weatherResp)
•JSONデコード: json.Unmarshalは、JSON形式のデータをGoの構造体に変換するための関数です。APIから返された天気情報はJSON形式なので、それをGoのweatherRespという構造体に変換して使いやすくしています。
7. 環境変数の使用
apiKey := os.Getenv("API_KEY")
•環境変数: 環境変数は、プログラムの外部から設定を与える方法です。ここでは、os.Getenvを使って、.envファイルに設定したAPI_KEYを読み込んでいます。これにより、APIキーをコードに直接書くことなく管理でき、セキュリティが向上します。
8. HTTPサーバーの構築
http.HandleFunc("/weather", weatherHandler)
http.ListenAndServe(":8080", nil)
•HTTPサーバー: http.HandleFuncは、特定のURL(エンドポイント)にリクエストが来たときに呼び出される関数を指定します。http.ListenAndServeは、指定したポート(ここでは8080)でサーバーを起動し、リクエストを待ち受けます。このアプリは、/weatherというエンドポイントで天気情報を提供します。
9. 構造体(Struct)の使用
type WeatherResponse struct {
Name string `json:"name"`
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
Description string `json:"description"`
} `json:"weather"`
}
•構造体: structは、複数のデータを一つの型にまとめることができるデータ構造です。このアプリでは、天気情報のデータを一つの構造体としてまとめ、扱いやすくしています。また、json:”name”のようなタグを使って、JSONデータとの対応を定義しています。
10. ロギング
log.Printf("Starting server on port %s...", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
•ロギング: log.Printfやlog.Fatalは、プログラムの動作状況を記録するためのものです。これにより、サーバーがどのポートで起動したかや、エラーが発生したときにその内容を記録し、デバッグに役立てます。
11. ファイル操作と環境ファイルの読み込み
err := godotenv.Load()
ファイル操作: godotenv.Load()は、.envファイルを読み込み、その中の設定を環境変数としてプログラムに読み込むために使います。これにより、環境設定を外部ファイルで管理し、簡単に変更できるようになります。
13. 条件分岐
if city == "" {
http.Error(w, "City parameter is required", http.StatusBadRequest)
return
}
•条件分岐: if-else文は、特定の条件が満たされたときに別の処理を行うためのものです。ここでは、cityという変数が空でないかどうかをチェックし、空の場合にはエラーメッセージを返しています。
14. フォーマット文字列
url := fmt.Sprintf("<http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric>", city, apiKey)
•フォーマット文字列: fmt.Sprintfは、文字列を組み合わせて一つの文字列を作成するための関数です。%sの部分に、指定された変数の値が挿入されます。ここでは、APIリクエスト用のURLを作成するために使っています。
まとめ
プログラム言語のGoを使って、Dockerコンテナで動作するWebAPIの開発環境の構築から開発、実行までの流れを紹介しました。また、Goの基本的な機能を実際のコードをベースに実践的な内容の解説も行いました。
これらの手順や機能を学ぶことで、Goの基礎を具体的に理解することが出来ていればすごく嬉しいです。
これからもGoとDockerコンテナを活用して、より高度なアプリケーション開発に挑戦してみてください!最後に、この記事がGoの学習やアプリ開発に役立つことを願っています。