Goチュートリアル

GoでPDFをMarkdown(マークダウン)に変換

Goから依存関係なしでPDFをMarkdownへ変換する、型安全な方法です。この例では標準ライブラリのnet/httpencoding/jsonだけを使い、ジョブを構造体へデコードします。サービスへ配置できる単一の静的バイナリにコンパイルできます。

要点

1つの構造体、3回のリクエスト

ジョブをjsonタグ付きのGo構造体として定義し、net/httpで3回呼び出します。PDFを/api/v2/jobsへPOSTしてjob_idを取得し、/api/v2/jobs/{job_id}Statusreadyになるまで確認して、/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へデコードすると、StatusErrorCodeTruncatedをコンパイル時に検査できます。定義していないレスポンスフィールドはデコーダーが無視します。

Goの強み

複数のPDFを同時に変換

Goはファンアウト処理に適しています。一括変換では、ファイルごとの作成、状態確認、ダウンロードをgoroutineで実行し、セマフォで並列数を制限してAPIへ過度な負荷をかけないようにします。

上限付きワーカープール

バッファ付きチャネルをセマフォとして使います。sem := make(chan struct{}, 5)なら、各goroutineは開始前にスロットを取得し、完了時に解放するため、同時実行は最大5件です。sync.WaitGroupで結果を待ちます。

コンテキストとタイムアウト

context.WithTimeoutNewRequestWithContextを使うと、停止したリクエストを安全にキャンセルできます。
error_codeprocessing_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/httpencoding/jsonだけを使います。go run convert.goは追加モジュールなしで動き、単一の静的バイナリへコンパイルできます。

GoでJSONレスポンスを構造体へ対応付けるには?

job_idstatuserror_codeerror_messagetruncatedjsonタグを付けた構造体を定義し、json.NewDecoderでデコードします。定義していないフィールドは無視されます。

Goで複数のPDFを並列変換するには?

作成、状態確認、ダウンロードをgoroutine内で実行し、バッファ付きチャネルをセマフォとして並列数を制限します。APIへリクエストを集中させず、複数ファイルを同時に変換できます。

タイムアウトやキャンセルを追加するには?

context.WithTimeouthttp.NewRequestWithContextを使うと、停止したリクエストをキャンセルできます。サーバーにも処理時間の上限があり、文書が大きすぎる場合はerror_code processing_timeoutを返します。

GoでローカルPDFをアップロードするには?

mime/multipartmultipart/form-data本文を作り、PDFをfileフィールドへ書き込みます。Content-Typeをmultipartのboundaryに設定し、JSONのurl本文の代わりに/api/v2/jobsへPOSTしてください。