The library Go's ecosystem doesn't have.

Python and Node both have an imperfect-but-usable Office parser. Go doesn't — and its OCR story means cgo, which breaks the static, cross-compiled binary Go programs are supposed to be.

the-parser-zoo

Go's document-parsing ecosystem is thin compared to Python's or Node's, and the gap is structural, not just a missing package. PDF text extraction has options — ledongthuc/pdf covers basic digital-native text, UniDoc/unipdf is capable but commercially licensed for anything beyond noncommercial use — but there's no python-docx or mammoth equivalent with meaningful adoption for reading .docx/.pptx/.xlsx as plain text; teams either shell out to a converter or hand-parse the OOXML zip-of-XML themselves. OCR is the sharper problem: the practical option is gosseract, a cgo binding to the system libtesseract — which means CGO_ENABLED=1, a matching Tesseract install on every build and deploy target, and goodbye to the single static binary and easy cross-compilation Go is usually chosen for. A Lambda or scratch-container Go binary that needs OCR either bundles a Tesseract shared library manually or gives up the zero-dependency deploy story entirely.

librarycoversstops at
ledongthuc/pdfDigital-native PDF text extractionNo OCR, no Office formats; struggles with complex layouts
unipdf (UniDoc)PDF text, forms, and more, capablyCommercial license required beyond noncommercial/eval use
gosseractOCR via cgo bindings to libtesseractRequires CGO_ENABLED=1 and a matching native Tesseract install on every build/deploy target
(community OOXML readers)Fragmented, low-adoption .docx/.xlsx readersNo mammoth/python-docx-level standard; most teams hand-parse the zip themselves

one-request

txtfetch replaces the whole gap with one request: POST a file or pass ?url= to https://api.txtfetch.com/v1/extract using net/http from the standard library — no cgo, no CGO_ENABLED=1, no Tesseract shared library to vendor alongside your binary. The response is the same { "status": "success", "extracted_text": "..." } whether the source was a PDF, an Office file, or a scan, so a Go service stays a single static binary regardless of what it's asked to extract.

Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

type extractResponse struct {
	Status        string `json:"status"`
	ExtractedText string `json:"extracted_text"`
}

func main() {
	f, err := os.Open("report.pdf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "report.pdf")
	if err != nil {
		panic(err)
	}
	if _, err := io.Copy(part, f); err != nil {
		panic(err)
	}
	writer.Close()

	req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result extractResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		panic(err)
	}
	fmt.Println(result.ExtractedText)
}
{
  "status": "success",
  "extracted_text": "..."
}

from-a-url

Skip the download entirely — pass a url parameter and txtfetch fetches the document server-side:

Go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

type extractResponse struct {
	Status        string `json:"status"`
	ExtractedText string `json:"extracted_text"`
}

func main() {
	endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
	if err != nil {
		panic(err)
	}
	q := endpoint.Query()
	q.Set("url", "https://example.com/report.pdf")
	endpoint.RawQuery = q.Encode()

	req, err := http.NewRequest("POST", endpoint.String(), nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result extractResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		panic(err)
	}
	fmt.Println(result.ExtractedText)
}

errors

Every non-success response carries a stable error.code — match on that, not on error.message. See the full error reference for every code and HTTP status txtfetch can return.

Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

type extractError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

type extractResponse struct {
	Status        string        `json:"status"`
	ExtractedText string        `json:"extracted_text"`
	Error         *extractError `json:"error"`
}

func main() {
	f, err := os.Open("report.pdf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "report.pdf")
	if err != nil {
		panic(err)
	}
	if _, err := io.Copy(part, f); err != nil {
		panic(err)
	}
	writer.Close()

	req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result extractResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		panic(err)
	}

	switch {
	case resp.StatusCode == 200:
		fmt.Println(result.ExtractedText)
	case resp.StatusCode == 429:
		fmt.Printf("back off: %s, retry after %ss\n", result.Error.Code, resp.Header.Get("Retry-After"))
	default:
		fmt.Printf("extraction failed: %s — %s\n", result.Error.Code, result.Error.Message)
	}
}

big-files-and-batches

Large uploads or slow documents are routed to an async job automatically — 202 plus a job_id to poll — and ?async=true forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use ?url= for anything larger — server-side fetches aren't held to the upload ceiling. See async jobs & webhooks for the full lifecycle, including webhook delivery instead of polling.

Go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"time"
)

type jobResponse struct {
	Status        string `json:"status"`
	JobID         string `json:"job_id"`
	ExtractedText string `json:"extracted_text"`
}

func poll(req *http.Request) (*jobResponse, error) {
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var result jobResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}
	return &result, nil
}

func main() {
	token := "Bearer " + os.Getenv("TXTFETCH_KEY")

	endpoint, _ := url.Parse("https://api.txtfetch.com/v1/extract")
	q := endpoint.Query()
	q.Set("url", "https://example.com/report.pdf")
	q.Set("async", "true")
	endpoint.RawQuery = q.Encode()

	submitReq, _ := http.NewRequest("POST", endpoint.String(), nil)
	submitReq.Header.Set("Authorization", token)
	job, err := poll(submitReq)
	if err != nil {
		panic(err)
	}

	pollURL := "https://api.txtfetch.com/v1/extract/" + job.JobID
	var result *jobResponse
	for {
		pollReq, _ := http.NewRequest("GET", pollURL, nil)
		pollReq.Header.Set("Authorization", token)
		result, err = poll(pollReq)
		if err != nil {
			panic(err)
		}
		if result.Status != "processing" {
			break
		}
		time.Sleep(2 * time.Second)
	}

	fmt.Println(result.ExtractedText)
}

gotchas

  • gosseract's cgo dependency means CGO_ENABLED=0 builds — the common way to produce a truly static, scratch-container-friendly Go binary — can't use it at all.
  • Cross-compiling a cgo-dependent binary (say, building for linux/arm64 from an amd64 CI runner) means cross-compiling libtesseract too, not just your Go code — a build-pipeline problem most Go teams would rather not own.
  • There's no single OOXML reader with the adoption level of Python's python-docx — most Go codebases either shell out to LibreOffice/Pandoc for conversion or hand-walk the .docx zip's document.xml.
  • unipdf's licensing (commercial past noncommercial/evaluation use) is worth checking before it ends up load-bearing in a shipped product.

formats

faq

How do I extract text from a PDF in Go without cgo?
POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract using net/http — no cgo, no CGO_ENABLED flag, no native library to link. The response is { "status": "success", "extracted_text": "..." }, whether the PDF is digital-native or scanned.
Is there a Go library for reading .docx or .xlsx files?
Nothing with the adoption level of Python's python-docx or openpyxl — most Go teams shell out to a converter or hand-parse the OOXML zip. txtfetch handles the whole Office family (.docx/.pptx/.xlsx and the legacy .doc/.xls/.ppt) through the same endpoint as everything else.
How do I OCR a scanned document in Go without gosseract's cgo dependency?
POST the scan to txtfetch — OCR runs server-side, so your Go binary never links libtesseract, never sets CGO_ENABLED=1, and stays cross-compilable to any target the same way it was before you needed OCR.
Does txtfetch have an official Go SDK?
No — and it doesn't need one. It's a single JSON-in, JSON-out HTTP request; the standard library's net/http and encoding/json cover the whole client in well under fifty lines, shown above.

go-further

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →