Tutorial de Go

Converter PDF para Markdown em Go

Uma forma tipada e sem dependências de converter PDF em Markdown a partir de Go. O exemplo usa apenas net/http e encoding/json da biblioteca padrão, decodifica o trabalho em um struct e compila para um único binário estático que você pode colocar em um serviço.

Resposta breve

Um struct, três requisições

Modele o trabalho como um struct de Go com tags json, depois faça três chamadas com net/http: POST do PDF para /api/v2/jobs para obter um job_id, consulte /api/v2/jobs/{job_id} até que Status seja ready, e faça GET de /api/v2/jobs/{job_id}/download para o Markdown. Sem SDK, sem cgo, sem GPU: a conversão, o OCR e o trabalho de tabelas acontecem todos no servidor.

Como fazer

O programa completo

Salve como convert.go e execute go run convert.go. Apenas a biblioteca padrão.

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

Por que um struct: decodificar em um job tipado te dá segurança em tempo de compilação sobre Status, ErrorCode e Truncated, e o decodificador simplesmente ignora qualquer campo da resposta que você não modele.

A força do Go

Converta muitos PDFs de uma vez

Onde Go brilha é no fan-out. Para converter um lote, execute a sequência de criar-consultar-baixar de cada arquivo em uma goroutine e limite-as com um semáforo para não sobrecarregar a API.

Pool de workers limitado

Use um canal com buffer como semáforo: sem := make(chan struct{}, 5). Cada goroutine pega um espaço antes de começar e o libera ao terminar, assim no máximo cinco conversões rodam ao mesmo tempo. Recolha os resultados com um sync.WaitGroup.

Context e tempos limite

context.WithTimeout mais NewRequestWithContext cancela de forma limpa uma requisição travada.
error_code processing_timeout ou conversion_failed te diz por que um trabalho falhou; truncated marca um resultado parcial.

Prefere outra linguagem? Veja o tutorial de Node.js, o tutorial de Python, ou a receita de cURL.

Integrando em um serviço?

A conversão também é um endpoint MCP hospedado, então um agente baseado em Go pode chamá-la como uma ferramenta. A referência completa e a especificação OpenAPI estão no hub para desenvolvedores.

Concorrência na prática

Um pool de workers limitado

Envolva o fluxo de um único arquivo de cima em convertOne, depois abra o leque sobre uma lista de PDFs com um semáforo que limita quantos rodam ao mesmo tempo.

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

O canal com buffer é todo o limitador de velocidade: uma goroutine não pode começar o trabalho real até ter colocado algo em sem, e só existem cinco fichas. Aumente o buffer para mais vazão em um plano superior, reduza-o para ser gentil com o plano gratuito. Combine isso com um Idempotency-Key por arquivo para que uma nova tentativa após uma falha não reconverta o que já terminou, e o mesmo pool lida com uma pasta de dez PDFs ou de dez mil.

Perguntas frequentes

Perguntas comuns

O exemplo de Go precisa de dependências?

Não. Ele usa apenas a biblioteca padrão (net/http e encoding/json), então go run convert.go funciona sem módulos para adicionar e compila para um único binário estático.

Como mapeio a resposta JSON em Go?

Defina um struct com tags json para job_id, status, error_code, error_message e truncated, depois decodifique com json.NewDecoder. Os campos que você não listar são simplesmente ignorados.

Como converto muitos PDFs de forma concorrente em Go?

Execute a sequência de criar, consultar e baixar dentro de goroutines e limite-as com um canal com buffer usado como semáforo, assim você converte vários arquivos de uma vez sem sobrecarregar a API.

Como adiciono um tempo limite ou cancelamento?

Use context.WithTimeout com http.NewRequestWithContext para que uma requisição travada seja cancelada. O servidor também impõe seu próprio orçamento de tempo e retorna error_code processing_timeout quando um documento é grande demais.

Como envio um arquivo PDF local em Go?

Monte um corpo multipart/form-data com mime/multipart, escreva o PDF em um campo file, defina o Content-Type para o boundary do multipart, e faça POST para /api/v2/jobs em vez do corpo JSON com url.