Go の net/http を使って Web API にリクエストを行う:CData API Server

by 杉本和也 | 2021年03月22日

f:id:sugimomoto:20210321211049p:plain

こんにちは。ここ最近、Go言語をちまちま触り始めた、CDataの杉本です。

シンプルかつわかりやすい言語仕様で面白いなーと思いながら、色々と試しています。

最近公開されたMS Learnのコンテンツもわかりやすくて良いですね。

docs.microsoft.com

今回はせっかくなので、APIに対してリクエストし、構造体にデータを格納するプログラムを試してみました。

APIは以前私のBlogで紹介した、O'Reilly のブックリストを返すAPIです。

kageura.hatenadiary.jp

APIはCData API Serverを使って構成しています。

www.cdata.com

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」パッケージを使用しました。

golang.org

とりあえず単純にデータ取得だけを行いレスポンスのBodyに含まれるJSONを構造体にして、参照します。

JSON→構造体の変換は「encoding/json」パッケージです。このあたりも標準で備えているのがありがたいですね。

golang.org

プロジェクトの準備

とりあえず以下のコマンドで適当にプロジェクトを準備しました。

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」として定義して、そのままプライベート扱いにしてしまうと変換が行われません。

golang.org

そのため、元のプロパティ名と関連付けるために「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
}

関連コンテンツ

トライアル・お問い合わせ

30日間無償トライアルで、CData のリアルタイムデータ連携をフルにお試しいただけます。記事や製品についてのご質問があればお気軽にお問い合わせください。