1つの構造体、3回のリクエスト
ジョブをjsonタグ付きのGo構造体として定義し、net/httpで3回呼び出します。PDFを/api/v2/jobsへPOSTしてjob_idを取得し、/api/v2/jobs/{job_id}をStatusがreadyになるまで確認して、/api/v2/jobs/{job_id}/downloadをGETします。SDK、cgo、GPUは不要で、変換、OCR、表の処理はすべてサーバー側で行われます。
完全なプログラム
convert.goとして保存し、go run convert.goを実行します。標準ライブラリだけを使います。
// Go 1.21+, standard library only. go run convert.go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const api = "https://pdf2md.dev/api/v2"
type job struct {
JobID string `json:"job_id"`
Status string `json:"status"`
ErrorCode string `json:"error_code"`
ErrorMessage string `json:"error_message"`
Truncated bool `json:"truncated"`
}
func authed(ctx context.Context, method, url string, body io.Reader) *http.Request {
req, _ := http.NewRequestWithContext(ctx, method, url, body)
req.Header.Set("Authorization", "Bearer p2m_your_key")
return req
}
func main() {
ctx := context.Background()
// 1) create a job from a PDF URL
payload, _ := json.Marshal(map[string]string{"url": "https://example.com/report.pdf"})
req := authed(ctx, "POST", api+"/jobs", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "report-2026-01")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var j job
json.NewDecoder(res.Body).Decode(&j)
res.Body.Close()
// 2) poll until ready or error
for j.Status != "ready" && j.Status != "error" {
time.Sleep(3 * time.Second)
res, _ := http.DefaultClient.Do(authed(ctx, "GET", api+"/jobs/"+j.JobID, nil))
json.NewDecoder(res.Body).Decode(&j)
res.Body.Close()
}
if j.Status == "error" {
fmt.Fprintf(os.Stderr, "conversion failed: %s %s\n", j.ErrorCode, j.ErrorMessage)
os.Exit(1)
}
// 3) download the Markdown
res, _ = http.DefaultClient.Do(authed(ctx, "GET", api+"/jobs/"+j.JobID+"/download", nil))
md, _ := io.ReadAll(res.Body)
res.Body.Close()
os.WriteFile("report.md", md, 0o644)
fmt.Println("saved report.md")
}
構造体を使う理由: 型付きのjobへデコードすると、Status、ErrorCode、Truncatedをコンパイル時に検査できます。定義していないレスポンスフィールドはデコーダーが無視します。
複数のPDFを同時に変換
Goはファンアウト処理に適しています。一括変換では、ファイルごとの作成、状態確認、ダウンロードをgoroutineで実行し、セマフォで並列数を制限してAPIへ過度な負荷をかけないようにします。
上限付きワーカープール
バッファ付きチャネルをセマフォとして使います。sem := make(chan struct{}, 5)なら、各goroutineは開始前にスロットを取得し、完了時に解放するため、同時実行は最大5件です。sync.WaitGroupで結果を待ちます。
コンテキストとタイムアウト
NewRequestWithContextを使うと、停止したリクエストを安全にキャンセルできます。processing_timeoutまたはconversion_failedで失敗理由を確認でき、truncatedは部分結果を示します。ほかの言語を使う場合は、Node.jsチュートリアル、Pythonチュートリアル、cURLの手順をご覧ください。
サービスへ組み込むには
この変換はホスト型MCPエンドポイントでも利用できるため、Go製エージェントからツールとして呼び出せます。全リファレンスとOpenAPI仕様は開発者向けハブにあります。
上限付きワーカープール
上の単一ファイル処理をconvertOneで包み、同時実行数を制限するセマフォを使ってPDFの一覧へファンアウトします。
// convertOne runs the create -> poll -> download flow for one URL.
func convertAll(ctx context.Context, urls []string) {
sem := make(chan struct{}, 5) // at most 5 conversions in flight
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
sem <- struct{}{} // take a slot (blocks if full)
defer func() { <-sem }() // release it
if err := convertOne(ctx, url); err != nil {
log.Printf("skip %s: %v", url, err)
}
}(u)
}
wg.Wait()
}
バッファ付きチャネルがレート制限のすべてです。goroutineはsemへ値を送るまで実処理を開始できず、トークンは5つしかありません。上位プランではバッファを増やしてスループットを上げ、無料プランでは減らして負荷を抑えます。ファイルごとのIdempotency-Keyを組み合わせれば、クラッシュ後の再実行で完了済みの変換が重複せず、10件でも1万件でも同じプールで処理できます。
よくある質問
Goの例に依存関係は必要ですか?
いいえ。標準ライブラリのnet/httpとencoding/jsonだけを使います。go run convert.goは追加モジュールなしで動き、単一の静的バイナリへコンパイルできます。
GoでJSONレスポンスを構造体へ対応付けるには?
job_id、status、error_code、error_message、truncatedにjsonタグを付けた構造体を定義し、json.NewDecoderでデコードします。定義していないフィールドは無視されます。
Goで複数のPDFを並列変換するには?
作成、状態確認、ダウンロードをgoroutine内で実行し、バッファ付きチャネルをセマフォとして並列数を制限します。APIへリクエストを集中させず、複数ファイルを同時に変換できます。
タイムアウトやキャンセルを追加するには?
context.WithTimeoutとhttp.NewRequestWithContextを使うと、停止したリクエストをキャンセルできます。サーバーにも処理時間の上限があり、文書が大きすぎる場合はerror_code processing_timeoutを返します。
GoでローカルPDFをアップロードするには?
mime/multipartでmultipart/form-data本文を作り、PDFをfileフィールドへ書き込みます。Content-Typeをmultipartのboundaryに設定し、JSONのurl本文の代わりに/api/v2/jobsへPOSTしてください。