Convertir un PDF en Markdown en Go
Une façon typée et sans dépendances de transformer des PDF en Markdown depuis Go. L'exemple utilise uniquement net/http et encoding/json de la bibliothèque standard, décode la tâche dans un struct et compile en un unique binaire statique que vous pouvez intégrer à un service.
Un struct, trois requêtes
Modélisez la tâche comme un struct Go avec des tags json, puis effectuez trois appels avec net/http : POST du PDF vers /api/v2/jobs pour obtenir un job_id, interrogez /api/v2/jobs/{job_id} jusqu'à ce que Status soit ready, et faites un GET de /api/v2/jobs/{job_id}/download pour le Markdown. Sans SDK, sans cgo, sans GPU : la conversion, l'OCR et le traitement des tableaux se déroulent tous côté serveur.
Le programme complet
Enregistrez-le sous convert.go et exécutez go run convert.go. Bibliothèque standard uniquement.
// 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")
}
Pourquoi un struct : décoder dans un job typé vous offre une sécurité à la compilation sur Status, ErrorCode et Truncated, et le décodeur ignore simplement tout champ de la réponse que vous ne modélisez pas.
Convertir de nombreux PDF à la fois
Là où Go brille, c'est dans le fan-out. Pour convertir un lot, exécutez la séquence créer-interroger-télécharger de chaque fichier dans une goroutine et bornez-les avec un sémaphore pour ne pas saturer l'API.
Pool de workers borné
Utilisez un canal bufferisé comme sémaphore : sem := make(chan struct{}, 5). Chaque goroutine prend un emplacement avant de démarrer et le libère à la fin, ce qui fait qu'au plus cinq conversions s'exécutent en même temps. Collectez les résultats avec un sync.WaitGroup.
Context et délais d'expiration
NewRequestWithContext annule proprement une requête bloquée.processing_timeout ou conversion_failed vous indique pourquoi une tâche a échoué ; truncated signale un résultat partiel.Vous préférez un autre langage ? Consultez le tutoriel Node.js, le tutoriel Python, ou la recette cURL.
Vous l'intégrez dans un service ?
La conversion est aussi un endpoint MCP hébergé, donc un agent basé sur Go peut l'appeler comme un outil. La référence complète et la spécification OpenAPI se trouvent sur le hub pour développeurs.
Un pool de workers borné
Encapsulez le flux pour un seul fichier ci-dessus dans convertOne, puis déployez l'éventail sur une liste de PDF avec un sémaphore qui limite combien s'exécutent à la fois.
// 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()
}
Le canal bufferisé est à lui seul tout le limiteur de débit : une goroutine ne peut pas commencer le vrai travail tant qu'elle n'a pas poussé quelque chose dans sem, et il n'existe que cinq jetons. Augmentez le buffer pour plus de débit sur une offre supérieure, réduisez-le pour ménager l'offre gratuite. Associez cela à un Idempotency-Key par fichier pour qu'une nouvelle tentative après un plantage ne reconvertisse pas ce qui était déjà terminé, et le même pool gère un dossier de dix PDF ou de dix mille.
Questions fréquentes
L'exemple Go nécessite-t-il des dépendances ?
Non. Il utilise uniquement la bibliothèque standard (net/http et encoding/json), donc go run convert.go fonctionne sans module à ajouter et compile en un unique binaire statique.
Comment mapper la réponse JSON en Go ?
Définissez un struct avec des tags json pour job_id, status, error_code, error_message et truncated, puis décodez avec json.NewDecoder. Les champs que vous ne listez pas sont simplement ignorés.
Comment convertir de nombreux PDF en parallèle en Go ?
Exécutez la séquence créer, interroger et télécharger dans des goroutines et bornez-les avec un canal bufferisé utilisé comme sémaphore, afin de convertir plusieurs fichiers à la fois sans saturer l'API.
Comment ajouter un délai d'expiration ou une annulation ?
Utilisez context.WithTimeout avec http.NewRequestWithContext pour qu'une requête bloquée soit annulée. Le serveur impose aussi son propre budget de temps et renvoie error_code processing_timeout lorsqu'un document est trop volumineux.
Comment téléverser un fichier PDF local en Go ?
Construisez un corps multipart/form-data avec mime/multipart, écrivez le PDF dans un champ file, réglez le Content-Type sur le boundary du multipart, et faites un POST vers /api/v2/jobs au lieu du corps JSON avec url.