CData Software Blog

クラウド連携のCData Software の技術ブログです。

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

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
}