導入
Goでgenko.meの配信APIに接続し、net/httpで記事一覧と記事詳細を取得して構造体へデコードする手順。
このページでは、Goからgenko.meのblogAPIに接続し、記事一覧と記事詳細を取得します。使うのは標準ライブラリだけです。
用意するもの
- genko.meのアカウントとワークスペース。APIの作成とAPIキーの発行にはオーナー権限が必要です。
- Go 1.21以降。以下のコードはジェネリクス(1.18以降)と
http.NewRequestWithContextを使います。 - リスト形式のAPIが1つと、公開済みの記事が1件。まだない場合はクイックスタートの手順2・3で作れます。
以降のyour-workspaceは自分のワークスペースIDに、blogは自分のエンドポイントに置き換えます。
インストール
ありません。 go getするものはなく、以下のコードをプロジェクトに置くだけです。
APIキーとワークスペースIDを設定する
管理画面のURLがhttps://genko.me/w/your-workspaceなら、ワークスペースIDはyour-workspaceです。
APIキーは「設定・プラン」の「APIキー」タブ(/w/your-workspace/settings/api-keys)で作成します。「選択したAPIのみ」でblogだけを許可しておくと、キーが漏れたときの範囲を限定できます。作成直後に表示される値は後から再表示できません。
キーは環境変数から読み、起動時に存在を確かめます。
apiKey := os.Getenv("GENKO_API_KEY")if apiKey == "" { log.Fatal("GENKO_API_KEYが設定されていません。")}os.Getenvは未設定でも空文字を返すだけなので、この確認がないと401を受け取ってから気づくことになります。キーをソースに直接書かないでください。
クライアントを書く
配信APIへのGETを1か所にまとめます。
package genko
import ( "context" "encoding/json" "fmt" "net/http" "net/url")
const baseURL = "https://api.genko.me"
// Client は配信APIへの、1ワークスペース分の接続。type Client struct { Workspace string APIKey string HTTP *http.Client}
// APIError は配信APIが返したエラー応答。type APIError struct { Status int Message string `json:"message"`}
func (e *APIError) Error() string { return fmt.Sprintf("genko: 配信APIが%dを返しました: %s", e.Status, e.Message)}
// List はリスト形式APIの一覧レスポンス。type List[T any] struct { Contents []T `json:"contents"` TotalCount int `json:"totalCount"` Offset int `json:"offset"` Limit int `json:"limit"`}
func (c *Client) get(ctx context.Context, path string, query url.Values, dest any) error { endpoint := baseURL + "/v1/" + c.Workspace + path if len(query) > 0 { endpoint += "?" + query.Encode() }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } req.Header.Set("X-API-KEY", c.APIKey)
res, err := c.HTTP.Do(req) if err != nil { return err } defer res.Body.Close()
if res.StatusCode >= 400 { apiErr := &APIError{Status: res.StatusCode} // message が読めなくてもステータスは返す。 _ = json.NewDecoder(res.Body).Decode(apiErr) return apiErr }
return json.NewDecoder(res.Body).Decode(dest)}
// GetList は一覧を取得する。Goのメソッドは型パラメータを持てないので関数にする。func GetList[T any](ctx context.Context, c *Client, endpoint string, query url.Values) (*List[T], error) { var list List[T] if err := c.get(ctx, "/"+endpoint, query, &list); err != nil { return nil, err } return &list, nil}
// GetEntry はリスト形式APIの1件を取得する。func GetEntry[T any](ctx context.Context, c *Client, endpoint, id string, query url.Values) (*T, error) { var entry T if err := c.get(ctx, "/"+endpoint+"/"+url.PathEscape(id), query, &entry); err != nil { return nil, err } return &entry, nil}url.Values.Encode()を使うと、filtersの角括弧や日本語の値がURLエンコードされます。クエリ文字列を自分で連結しないでください。
スキーマを構造体にする
管理画面のフィールドIDをjsonタグに書きます。
package genko
import "time"
// Post はブログAPIのフィールド。管理画面のフィールドIDをjsonタグに書く。type Post struct { ID string `json:"id"` Title string `json:"title"` // リッチテキストはHTML文字列。 Body string `json:"body"` Category string `json:"category"` Tags []string `json:"tags"` // 画像フィールドの配信値は url と alt だけ。寸法は付かない。 Thumbnail *Image `json:"thumbnail"` // fields で外すと空になる。ゼロ値かどうかで判断する。 PublishedAt time.Time `json:"publishedAt"` UpdatedAt time.Time `json:"updatedAt"`}
type Image struct { URL string `json:"url"` Alt string `json:"alt"`}日時はISO 8601の文字列なので、time.Timeがそのままデコードします。UTCで届くため、表示前にtime.LocalやAsia/Tokyoへ変換してください。
任意項目や、あとから追加したフィールドは、古い記事に値がないことがあります。値がない場合はゼロ値になります。fieldsで項目を絞った場合も同じです。単一参照は*Imageのようにポインタで受け、nilを扱えるようにします。
記事一覧を出す
package main
import ( "context" "fmt" "log" "net/http" "net/url" "os" "time"
"example.com/yourapp/genko")
func main() { apiKey := os.Getenv("GENKO_API_KEY") if apiKey == "" { log.Fatal("GENKO_API_KEYが設定されていません。") }
client := &genko.Client{ Workspace: "your-workspace", APIKey: apiKey, HTTP: &http.Client{Timeout: 10 * time.Second}, }
list, err := genko.GetList[genko.Post](context.Background(), client, "blog", url.Values{ "limit": {"10"}, "orders": {"-publishedAt"}, "fields": {"title,publishedAt"}, }) if err != nil { log.Fatal(err) }
fmt.Printf("全%d件\n", list.TotalCount) for _, post := range list.Contents { fmt.Printf("%s %s %s\n", post.ID, post.Title, post.PublishedAt.Format(time.DateOnly)) }}レスポンスはContents・TotalCount・Offset・Limitを持ちます。該当する記事がない場合もエラーにはならず、Contentsが空スライスになります。
html/templateに渡す場合、テキストフィールドは自動でエスケープされます。リッチテキスト(Body)はHTMLとして出したいので、template.HTML(post.Body)に変換します。この変換はエスケープを無効にします。 管理画面で入力された内容がそのまま出るため、信頼できる書き手だけに編集権限を渡してください。
記事詳細を出す
一覧で得たIDをパスの末尾に付けます。
post, err := genko.GetEntry[genko.Post](ctx, client, "blog", id, nil)
var apiErr *genko.APIErrorif errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound { http.NotFound(w, r) return}if err != nil { http.Error(w, "記事を取得できませんでした", http.StatusInternalServerError) log.Printf("genko: %v", err) return}一覧と違い、詳細のレスポンスはcontentsで包まれず、コンテンツのオブジェクトがそのまま返ります。非公開・削除済み・存在しないIDはどれも404です。
errors.Asで*APIErrorを取り出し、Statusで分岐します。ネットワーク障害など、HTTPレスポンス自体を受け取れなかった失敗は*APIErrorになりません。
エラーの中身をそのままブラウザへ返さないでください。 URLやキーがメッセージに含まれる可能性があります。ログには残し、画面には一般的な文言を出します。
絞り込む
filtersはフィールドIDと演算子を組み合わせた文字列です。
tech, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{ "filters": {"category[equals]tech"}, "orders": {"-publishedAt"}, "limit": {"10"},})
since := time.Now().AddDate(0, 0, -30).UTC().Format("2006-01-02T15:04:05.000Z")recent, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{ "filters": {"publishedAt[greater_than]" + since},})演算子の一覧と、[and]/[or]での連結はクエリ仕様を参照してください。
エラーの扱い
| ステータス | 主な原因 |
|---|---|
400 | クエリの形式・値・演算子が不正 |
401 | APIキーがない、無効、期限切れ |
403 | キーとワークスペースが一致しない |
404 | ワークスペース・API・コンテンツがない、またはキーの対象外 |
429 | 同じAPIキーで1分300リクエストを超えた |
詳しくはエラーと制限にまとめています。
次に読む
ページ送り、保存済み下書きのプレビュー、Webhookの署名検証は実践に続きます。
最終更新: 2026年9月17日