こんにちは。ここ最近、Go言語をちまちま触り始めた、CDataの杉本です。
シンプルかつわかりやすい言語仕様で面白いなーと思いながら、色々と試しています。
最近公開されたMS Learnのコンテンツもわかりやすくて良いですね。
今回はせっかくなので、APIに対してリクエストし、構造体にデータを格納するプログラムを試してみました。
APIは以前私のBlogで紹介した、O'Reilly のブックリストを返すAPIです。
APIはCData API Serverを使って構成しています。
Goのバージョンは1.16を使用しました。
対象のAPIの仕様
「https://oreillydemoapi.azurewebsites.net/api.rsc/OReillyBooks/」に対してGETリクエストを投げることで、書籍の一覧が取得できます。
認証はカスタムヘッダー「x-cdata-authtoken」を使用します。
GET /api.rsc/OReillyBooks/ HTTP/1.1 Host: oreillydemoapi.azurewebsites.net x-cdata-authtoken: 7y3E6q4b6V1v9f0D2m9j
レスポンスのサンプルはこんな感じです。
Rootとなるオブジェクトに「value」というレコードの配列を持つプロパティが存在します。このデータが書籍の一覧になっています。
{ "@odata.context": "https://oreillydemoapi.azurewebsites.net/api.rsc/$metadata#OReillyBooks", "value": [ { "RowId": 2, "ImageUrl": "https://www.oreilly.co.jp/books/images/picture_large4-87311-063-7.jpeg", "ISBN": "4-87311-063-7", "Price": "3080", "PublishDate": "37196", "Title": "C++プログラミング入門 新版", "URL": "https://www.oreilly.co.jp/books/4873110637/" }, { "RowId": 3, "ImageUrl": "https://www.oreilly.co.jp/books/images/picture_large4-87311-065-3.jpeg", "ISBN": "4-87311-065-3", "Price": "3300", "PublishDate": "37226", "Title": "サーバ負荷分散技術", "URL": "https://www.oreilly.co.jp/books/4873110653/" } ] }
API リクエストで使用するパッケージ
標準で組み込まれている「net/http」パッケージを使用しました。
とりあえず単純にデータ取得だけを行いレスポンスのBodyに含まれるJSONを構造体にして、参照します。
JSON→構造体の変換は「encoding/json」パッケージです。このあたりも標準で備えているのがありがたいですね。
プロジェクトの準備
とりあえず以下のコマンドで適当にプロジェクトを準備しました。
mkdir OReillyAPIRequestSample cd OReillyAPIRequestSample touch main.go go mod init
レスポンスを格納するための構造体の準備
まずはResponseを格納する構造体を定義します。
RootとBooksの2つの構造体で成り立っていて、valueでBooks構造体の配列を保持しています。
type Root struct { Value []Books `json:"value"` } type Books struct { RowId int ImageUrl string ISBN string Price string PublishDate string Title string URL string }
ここで一点注意したいのは、構造体のプロパティ名です。
Goは構造体のプロパティ名やパッケージに含まれるファンクション名の命名規則として、頭が大文字かどうかでプライベートとパブリックを区分けしています。
今回レスポンスのデータを json.Unmarshal で構造体に変換しますが、この際に元のJSONの値に従って「value」として定義して、そのままプライベート扱いにしてしまうと変換が行われません。
そのため、元のプロパティ名と関連付けるために「json:"value"
」を設定しています。
API リクエストを行う
「net/http」を使って一番簡単にGETリクエストを行う方法は 「http.Get」メソッドを利用する方法です。
ただ、この「http.Get」メソッドはカスタムヘッダーなどを追加で登録することができません。
そのため、「http.NewRequest」メソッドで一度リクエスト内容を定義して、リクエストを管理する「new(http.Client)」を生成し、「client.Do(req)」で生成したリクエストを送信します。
url := "https://oreillydemoapi.azurewebsites.net/api.rsc/OReillyBooks/" authHeaderName := "x-cdata-authtoken" authHeaderValue := "7y3E6q4b6V1v9f0D2m9j" req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Set(authHeaderName, authHeaderValue) client := new(http.Client) resp, err := client.Do(req)
NewRequestのエラーの返り値はメソッドの名前やURL文字列のバリデーションを行っています。今回は無視しました。
client.Do(req)の戻り値エラーはリクエスト前のバリデーションチェックやサーバーと疎通できなかった場合のTimeoutが発生した場合のエラーになるようです。
サーバーからのレスポンスとして、Service Unavailable や 401 Unauthroized 等が発生した際には、ResponseのStatusCodeを確認する必要があります。なんとなく、client.Do(req)のエラーに含まれそうかもと思ったんですが、API用パッケージというよりはHTTPリクエストのためのパッケージなので、納得です。
// URLがnilだったり、Timeoutが発生した場合にエラーを返す模様。 // サーバーからのレスポンスとなる 401 Unauthroized Error などはResponseをチェックする。 // サーバーとの疎通が開始する前の動作のよう。 if err != nil { fmt.Println("Error Request:", err) return } // resp.Bodyはクローズすること。クローズしないとTCPコネクションを開きっぱなしになる。 defer resp.Body.Close() // 200 OK 以外の場合はエラーメッセージを表示して終了 if resp.StatusCode != 200 { fmt.Println("Error Response:", resp.Status) return }
あとはResponseBodyのJSONを読み取りし、「json.Unmarshal」で構造体に反映させます。これで無事データを取得できました。
// Response Body を読み取り body, _ := io.ReadAll(resp.Body) // JSONを構造体にエンコード var Books Root json.Unmarshal(body, &Books) fmt.Printf("%-v", Books)
ソースコード全体
package main import ( "encoding/json" "fmt" "io" "net/http" ) func main() { fmt.Println("Start!") url := "https://oreillydemoapi.azurewebsites.net/api.rsc/OReillyBooks/?$top=3" authHeaderName := "x-cdata-authtoken" authHeaderValue := "7y3E6q4b6V1v9f0D2m9j" req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Set(authHeaderName, authHeaderValue) client := new(http.Client) resp, err := client.Do(req) // URLがnilだったり、Timeoutが発生した場合にエラーを返す模様。 // サーバーからのレスポンスとなる 401 Unauthroized Error などはResponseをチェックする。 // サーバーとの疎通が開始する前の動作のよう。 if err != nil { fmt.Println("Error Request:", err) return } // resp.Bodyはクローズすること。クローズしないとTCPコネクションを開きっぱなしになる。 defer resp.Body.Close() // 200 OK 以外の場合はエラーメッセージを表示して終了 if resp.StatusCode != 200 { fmt.Println("Error Response:", resp.Status) return } // とりあえずResponsの構造体を全部出力 fmt.Printf("%-v", resp) // Response Body を読み取り body, _ := io.ReadAll(resp.Body) // JSONを構造体にエンコード var Books Root json.Unmarshal(body, &Books) fmt.Printf("%-v", Books) } type Root struct { Value []Books `json:"value"` } type Books struct { RowId int ImageUrl string ISBN string Price string PublishDate string Title string URL string }