Go-Tutorial

PDF in Markdown umwandeln mit Go

Ein typisierter, abhängigkeitsfreier Weg, PDFs aus Go in Markdown umzuwandeln. Das Beispiel nutzt nur net/http und encoding/json aus der Standardbibliothek, dekodiert den Job in ein Struct und kompiliert zu einer einzigen statischen Binärdatei, die du in einen Dienst einbauen kannst.

Kurze Antwort

Ein Struct, drei Anfragen

Modelliere den Job als Go-Struct mit json-Tags, dann mache drei net/http-Aufrufe: sende die PDF per POST an /api/v2/jobs, um eine job_id zu erhalten, frage /api/v2/jobs/{job_id} ab, bis Status auf ready steht, und rufe /api/v2/jobs/{job_id}/download per GET für das Markdown ab. Kein SDK, kein cgo, keine GPU – die Konvertierung, das OCR und die Tabellenarbeit passieren alle serverseitig.

Anleitung

Das vollständige Programm

Speichere es als convert.go und führe go run convert.go aus. Nur Standardbibliothek.

// 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")
}

Warum ein Struct: die Dekodierung in einen typisierten job gibt dir Compile-Zeit-Sicherheit bei Status, ErrorCode und Truncated, und der Decoder ignoriert einfach alle Antwortfelder, die du nicht modellierst.

Die Stärke von Go

Wandle viele PDFs gleichzeitig um

Wo Go glänzt, ist beim Fan-out. Um einen Stapel zu konvertieren, führe die Anlegen-Abfragen-Herunterladen-Abfolge jeder Datei in einer Goroutine aus und begrenze sie mit einem Semaphor, um die API nicht zu überlasten.

Begrenzter Worker-Pool

Nutze einen gepufferten Channel als Semaphor: sem := make(chan struct{}, 5). Jede Goroutine belegt einen Slot, bevor sie startet, und gibt ihn beim Beenden frei, so laufen höchstens fünf Konvertierungen gleichzeitig. Sammle die Ergebnisse mit einer sync.WaitGroup.

Context und Timeouts

context.WithTimeout plus NewRequestWithContext bricht eine hängende Anfrage sauber ab.
error_code processing_timeout oder conversion_failed sagt dir, warum ein Job fehlgeschlagen ist; truncated markiert ein Teilergebnis.

Bevorzugst du eine andere Sprache? Sieh dir das Node.js-Tutorial, das Python-Tutorial oder das cURL-Rezept an.

Baust du es in einen Dienst ein?

Die Konvertierung ist auch ein gehosteter MCP-Endpunkt, sodass ein Go-basierter Agent sie als Tool aufrufen kann. Die vollständige Referenz und die OpenAPI-Spezifikation findest du im Entwickler-Hub.

Nebenläufigkeit in der Praxis

Ein begrenzter Worker-Pool

Kapsele den Einzeldatei-Ablauf von oben in convertOne und fächere dann über eine Liste von PDFs mit einem Semaphor auf, das begrenzt, wie viele gleichzeitig laufen.

// 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()
}

Der gepufferte Channel ist der gesamte Ratenbegrenzer: eine Goroutine kann die eigentliche Arbeit nicht beginnen, bevor sie etwas in sem geschoben hat, und es existieren nur fünf Tokens. Erhöhe den Puffer für mehr Durchsatz auf einem höheren Tarif, verringere ihn, um den Gratis-Tarif zu schonen. Kombiniere das mit einem Idempotency-Key pro Datei, damit ein erneuter Versuch nach einem Absturz nicht neu konvertiert, was bereits fertig war, und derselbe Pool bewältigt einen Ordner mit zehn PDFs oder zehntausend.

FAQ

Häufige Fragen

Braucht das Go-Beispiel Abhängigkeiten?

Nein. Es nutzt nur die Standardbibliothek (net/http und encoding/json), daher funktioniert go run convert.go ohne hinzuzufügende Module und kompiliert zu einer einzigen statischen Binärdatei.

Wie mappe ich die JSON-Antwort in Go?

Definiere ein Struct mit json-Tags für job_id, status, error_code, error_message und truncated, dann dekodiere mit json.NewDecoder. Felder, die du nicht aufführst, werden einfach ignoriert.

Wie konvertiere ich viele PDFs nebenläufig in Go?

Führe die Abfolge aus Anlegen, Abfragen und Herunterladen in Goroutinen aus und begrenze sie mit einem gepufferten Channel, der als Semaphor dient, so konvertierst du mehrere Dateien gleichzeitig, ohne die API zu überlasten.

Wie füge ich ein Timeout oder eine Abbruchmöglichkeit hinzu?

Verwende context.WithTimeout mit http.NewRequestWithContext, damit eine hängende Anfrage abgebrochen wird. Der Server erzwingt zudem sein eigenes Zeitbudget und liefert error_code processing_timeout, wenn ein Dokument zu groß ist.

Wie lade ich eine lokale PDF-Datei in Go hoch?

Baue einen multipart/form-data-Body mit mime/multipart, schreibe die PDF in ein file-Feld, setze den Content-Type auf die Multipart-Boundary und sende ihn per POST an /api/v2/jobs statt des JSON-Bodys mit url.