👻

GoでAPIを書く前に、HTTPレスポンスを“触って”理解する

に公開

はじめに

Go言語でAPIを作ってみようと思い、GPTと一緒に実装を進めていました。
実際に進めていくと、コードは動くし、レスポンスも返ってくる。
一見すると問題なく実装できているように感じていました。

ただ、実装を進める中で、
「なぜこのコードを書いているのかを自分の言葉で説明できない」
という違和感がありました。

具体的には、こんな疑問です。

  • Content-Type って何だろう?
  • なぜJSONを返すときにHeaderを設定しているんだろう?

その場では「こう書けば動く」という理由で実装していましたが、
正直なところ、APIの本質を理解しないままコードを書いている 感覚がありました。

APIの正体が HTTPレスポンス であること自体は理解していました。
そして、Go言語はそれを作るための手段の一つにすぎません。
ただ、本質を理解していないと、今後の応用が効かないと感じました。

そこで今回は、いきなりGoでAPIを実装するのではなく、

  • APIは何を返しているのか
  • HTTPレスポンスはどんな構造をしているのか

といった部分を、実際にレスポンスを見ながら整理していこうと思います。

この記事では、
まず curl を使ってHTTPレスポンスを確認し、
その構造を理解した上で、Go言語で同じレスポンスを実装していきます。

APIの構成を理解することを目的にしつつ、
それをGoでどう表現するのかを確認していきます。


1. なぜいきなりGoでAPIを書かないのか

私はGo言語でAPIの勉強をしていることもあり、
「APIを作ろう → とりあえずGoでサーバを書いてみよう」
という思考になっていました。

実際、net/http を使えば、JSONを返すAPIはすぐに書くことができます。
curlで叩けばレスポンスも返ってきますし、一見すると問題なく動いているように見えます。
簡単なプログラムであれば、ある程度のものは書けてしまいます。

ただ、その状態では次のような疑問に、私はうまく答えられませんでした。

  • なぜこのHeaderを設定しているのか
  • Content-Typeがないと何が起きるのか
  • そもそもAPIは何を返しているのか

これは、HTTPレスポンスの構造を理解しないまま実装していたことが原因でした。

APIはGoの機能ではありません。
APIの正体は HTTPレスポンス であり、Goはそれを組み立てるための道具にすぎません。

それにもかかわらず、先にGoのコードから書き始めてしまうと、

  • HTTPの話
  • Goの文法
  • APIの設計

が頭の中で混ざってしまい、
「結局、何を理解できていないのか」が分からなくなってしまいます。

そこでこの記事では、あえて順番を逆にします。

まずは 既存のAPIが返しているHTTPレスポンスをそのまま観察し、
その構造を理解することにします。
その上で、Go言語を使って 同じ構造のレスポンスを再現していきます。

この順番で進めることで、

  • なぜこのコードを書くのか
  • Goで何を表現しているのか

について、自然に理解できるようになることを目指します。

では、次の章で
curl を使って外部APIにアクセスし、
HTTPレスポンスを実際に見ていきましょう。


2. curlで外部APIにアクセスしてみる

まずは、自分でAPIを作る前に、
すでに存在しているAPIがどんなレスポンスを返しているのかを見ていきましょう。

ここでは、シンプルなJSONを返す外部APIに対して
curl を使ってアクセスしてみます。

今回使うAPI

今回は、以下のAPIを利用します。

https://jsonplaceholder.typicode.com/todos/1

このAPIは、シンプルなJSONデータを返してくれます。
HTTPレスポンスの構造を確認するのに最適です。


curlでアクセスしてみる

ターミナルで、次のコマンドを実行します。

curl https://jsonplaceholder.typicode.com/todos/1

すると、次のようなレスポンスが返ってきます。

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

ここで、一度立ち止まってみましょう。

普段「APIを叩く」と言うと、
この JSONだけ を見て終わってしまいがちです。
実際にはこれは HTTPレスポンスの一部 にすぎません。


レスポンス全体を確認する

HTTPレスポンス全体を見るために、
-i オプションをつけて再度curlを実行します。

curl -i https://jsonplaceholder.typicode.com/todos/1

すると、次のような情報が表示されます。

HTTP/2 200
date: Sat, 24 Jan 2026 12:26:02 GMT
content-type: application/json; charset=utf-8
content-length: 83
...
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

一部省略していますが、
ここで表示されているのが HTTPレスポンスの全体像 です。


ここで注目したいポイント

このレスポンスには、大きく分けて次の情報が含まれています。

  • ステータス(200 OK)
  • Header(Content-Type など)
  • Body(JSONデータ)

先ほど見ていたJSONは、
この中の Body部分だけ だったことが分かります。


3. HTTPレスポンスは何でできているのか

前の章で、curl -i を使って
HTTPレスポンスの全体像を確認しました。

ここでは、そのレスポンスを
構成要素ごとに分解して見ていきます。

HTTPレスポンスは、大きく分けて
次の3つの要素で構成されています。

  • ステータス
  • ヘッダー
  • ボディ

APIが返しているものの正体は、
この3つの組み合わせとなります。


ステータス(Status)

ステータスは、
リクエストに対する結果を表す情報です。

たとえば、先ほどのレスポンスでは
次のように表示されていました。

HTTP/2 200

ここでの 200 は、
「リクエストが成功した」
という意味を持つステータスコードになります。

ステータスコードはたくさんの種類がありますが、
ここでは詳細は割愛します。

APIにアクセスしたとき、
まず最初に確認すべきなのは
処理が成功したかどうかです。


ヘッダー(Header)

ヘッダーは、
レスポンスに関する付加情報を表します。

たとえば、次のような行です。

content-type: application/json; charset=utf-8

このヘッダーは、
「このレスポンスの中身はJSON形式です」
ということをクライアントに伝えています。

ヘッダーがあることで、
クライアントは
「このデータをどう扱えばよいか」
を判断できます。


ボディ(Body)

ボディは、
実際に返されるデータ本体です。

今回の例では、
次のJSONがボディにあたります。

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

普段「APIがJSONを返す」と言う場合、
多くのケースでは
このボディ部分だけを指しています。


APIが返しているものの正体

ここまで見てきたように、
APIが返しているのは
JSONそのものではありません。

APIが返しているのは、

  • ステータス
  • ヘッダー
  • ボディ

からなる HTTPレスポンス となります。

JSONは、その中の
ボディに含まれているデータ形式の一つにすぎません。


この構造を理解しておくと、
後からGoでAPIを実装するときに、

  • なぜHeaderを設定しているのか
  • なぜJSONを返しているのか

といった点が、
自然に結びつくようになります。

次の章では、
この中でも特に重要な
Headerの役割 について、
もう少し詳しく見ていきます。


4. Headerは何のために存在するのか

前の章で、HTTPレスポンスは

  • ステータス
  • ヘッダー
  • ボディ

の3つで構成されていることを確認しました。

この中でも、特に理解があいまいになりやすいのが
ヘッダー(Header) だと思います。

「なんとなく設定しているけど、
なぜ必要なのかは説明できない」

私も、まさにこの状態でした。


Headerは「本文の説明書」

いつも通りGPTに聞いてみましょう。

ボディをどう解釈すればよいかを伝える情報

なるほど。
言われてみると、なんとなく理解できます。

APIのレスポンスを受け取るクライアントは、
まずヘッダーを見てから、ボディを処理します。

たとえば、次のヘッダーです。

content-type: application/json; charset=utf-8
これは、

  • ボディの中身はJSON形式であること
  • 文字コードはUTF-8であること

をクライアントに伝えています。

つまり、Headerは
「この後に続くデータの説明書」 のような存在です。


Content-Typeがないと何が起きるのか

もし Content-Type が設定されていなかったら、
クライアントは次のような状態になります。

  • このデータはJSONなのか?
  • それともただの文字列なのか?

人間であれば雰囲気で読めるかもしれませんが、
プログラムはそうはいきません。

そのため、APIでは
「何を返しているか」をHeaderで明示する
ことがとても重要になります。


なぜBodyだけ返せばダメなのか

「JSONだけ返せばいいのでは?」
と思うかもしれません。

しかし、APIの世界では
Bodyだけでは情報が足りません。

  • 成功したのか、失敗したのか → ステータス
  • どう解釈すべきデータなのか → ヘッダー
  • 実際のデータ → ボディ

この3つが揃って、
はじめて意味のあるレスポンスになります。


GoでHeaderを設定している理由

GoでAPIを書くと、
次のようなコードがよく登場します。

w.Header().Set("Content-Type", "application/json")

以前の私は
「とりあえず書いているだけ」でした。

ですが今までの記事の内容から

  • HTTPレスポンスの構造があり
  • Headerがその一部であり
  • クライアントに情報を伝える役割を持っている

という文脈の中で、
この1行の意味を理解できるようになっているはずです。


5. GoでHTTPレスポンスを再現してみる

ここまでの記事では、
APIの正体が HTTPレスポンス であり、

  • ステータス
  • ヘッダー
  • ボディ

の3つで構成されていることを確認してきました。

この章では、
それらを Go言語で一つずつ再現 していきます。

ポイントは、
「GoでAPIを作る」ではなく
「GoでHTTPレスポンスを組み立てる」
という視点です。


まずは最小のHTTPサーバを立てる

まずは、最小構成のHTTPサーバを作ります。

package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

このコードを実行(go run main.go)して、
別のターミナルから次のコマンドを実行してみましょう。

curl -i http://localhost:8080

すると、次のようなレスポンスが返ってきます。

HTTP/1.1 200 OK
Date: Sun, 25 Jan 2026 10:45:49 GMT
Content-Length: 6
Content-Type: text/plain; charset=utf-8

hello

この時点で、
すでにHTTPレスポンスが成立している
ことが分かります。


ステータスはどこで決まっているのか

このコードでは、
ステータスコードを明示的に書いていません。

それでも 200 OK が返っています。

これは、Goの net/http が
何も指定されなければ 200 を返す
という仕様になっているからです。

つまり、

  • ステータスも
  • ヘッダーも

Goが裏側で
HTTPレスポンスとして組み立ててくれています。


Headerを自分で設定してみる

次に、Headerを明示的に設定してみます。

func handler(w http.ResponseWriter, r *http.Request) {
	// 以下のコードに修正
    w.Header().Set("Content-Type", "application/json")
	fmt.Fprintln(w, `{"message":"hello"}`)
}

再度、curlで確認します。

curl -i http://localhost:8080

すると、レスポンスは次のようになります。

HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 25 Jan 2026 10:55:28 GMT
Content-Length: 20

{"message":"hello"}

ここでやっていることは、とてもシンプルです。

  • Headerで「これはJSONです」と伝え
  • Bodyで実際のデータを返している

まさに第3章・第4章で見てきた構造そのものです。


GoはHTTPレスポンスを「書いている」だけ

ここまでを整理すると、

  • w.Header().Set() → ヘッダー
  • fmt.Fprintln() → ボディ
  • ステータス → HTTPレスポンスの一部

という対応関係になっています。

GoでAPIを書いているように見えて、
実際には
HTTPレスポンスを1行ずつ組み立てている
だけだということが分かります。


API実装を理解した瞬間

HTTPの構造を理解した上で
Goのコードを見ると、

w.Header().Set("Content-Type", "application/json")

という1行も、
「ああ、ヘッダーを書いているんだな」
と自然に理解できるようになります。

APIの実装とは、
新しい何かを作っているのではなく、

HTTPという既存の仕組みを
プログラミング言語で表現しているだけ

なのだと、実感できるようになりました。


まとめ

この記事では、
Go言語でAPIを実装する前に、
HTTPレスポンスそのものに触れて理解する ことを目的に進めてきました。

curl を使って実際のレスポンスを確認しながら、

  • APIの正体は HTTPレスポンス であること
  • HTTPレスポンスは
    • ステータス
    • ヘッダー
    • ボディ
      の3つで構成されていること
  • Content-Type は
    「返しているデータの説明書」であること

を整理してきました。

その上で、Go言語を使って
同じHTTPレスポンスを再現してみることで、

GoでAPIを書いているように見えて、
実際には
HTTPレスポンスを組み立てているだけ
だという感覚を掴めたと思います。

いきなりフレームワークや実装から入るのではなく、
「APIとは何か」「HTTPとは何か」を
一段抽象度を上げて理解しておくことで、
今後の実装や応用がずっと楽になるのではないかと、
この記事を書きながら感じました。

どうしても「まずはコードを書かないと」と思ってしまいがちですが、
事前に理論や仕組みを理解しておくことで、
結果的に実装が早くなったり、迷いが減ったりします。
そのバランスの難しさを、日々感じています。

今後は、

  • ステータスコードの仕組みや役割
  • 責任の分離

といったテーマについても、
書きながら理解を深めていきたいと考えています。

長い記事を最後まで読んでいただき、ありがとうございました。
図解できそうな部分は、今後随時追加していく予定です。

また読んでいただけると嬉しいです。
では、また。

Discussion