DEV Community

Mufthi Ryanda
Mufthi Ryanda

Posted on

Building a Google Docs Package in Go: From API Setup to Document Management

Preface
Google's document APIs are powerful but can be intimidating to work with directly. Between authentication flows, service accounts, and API quirks, you end up writing a lot of boilerplate just to read or write documents. I built this package after getting tired of copying the same Google Docs integration code across projects. It wraps the complexity behind simple functions while handling authentication and common operations cleanly. This guide walks through the complete setup process and implementation, including the Google Cloud configuration that trips up most developers.

Google Cloud Setup
Start by creating a fresh Google Cloud project. Head to the Google Cloud Console and click "Create or select a project", then "New project". Give it a name and hit create.
Next, enable the APIs you'll need. Navigate to "APIs & Services" from the hamburger menu, then click "Enable APIs and services". Search for and enable both "Google Drive API" and "Google Docs API".
Now create a service account for authentication. Go to "IAM & Admin" → "Service Accounts" → "Create Service Account". Fill in a name like "google-docs-service" and add a description. For permissions, select "Owner" role to keep things simple during development.
After creating the service account, click on it and go to the "Keys" tab. Click "Add Key" → "Create new key" and choose JSON format. This downloads your credential file - keep it secure since it can't be recovered if lost.
Save this JSON file in your project and you're ready to start coding.

Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description
Image description

Package Implementation
Our Google Docs package wraps both the Docs and Drive APIs into a single clean interface. The core struct DocsPkg holds authenticated clients for both services, which we need since document operations span both APIs.

package google

import (
    "context"
    "fmt"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/docs/v1"
    "google.golang.org/api/drive/v3"
    "google.golang.org/api/option"
    "io"
    "os"
)

// PlaceholderData contains all placeholder values to be replaced
type PlaceholderData map[string]string

// DocsPkg wraps the Google Docs and Drive services
type DocsPkg struct {
    ctx         context.Context
    docsClient  *docs.Service
    driveClient *drive.Service
}

// NewDocsService creates a new service with Google API authentication
func NewDocsService(ctx context.Context, credentialsFile string) (*DocsPkg, error) {
    // Read credentials file
    credBytes, err := os.ReadFile(credentialsFile)
    if err != nil {
        return nil, fmt.Errorf("failed to read credentials file: %w", err)
    }

    // Create config from credentials
    config, err := google.JWTConfigFromJSON(credBytes,
        docs.DocumentsScope,
        drive.DriveScope)
    if err != nil {
        return nil, fmt.Errorf("failed to create JWT config: %w", err)
    }

    // Create HTTP client with the config
    client := config.Client(ctx)

    // Initialize the Docs service
    docsService, err := docs.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        return nil, fmt.Errorf("failed to create Docs service: %w", err)
    }

    // Initialize the Drive service
    driveService, err := drive.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        return nil, fmt.Errorf("failed to create Drive service: %w", err)
    }

    return &DocsPkg{
        ctx:         ctx,
        docsClient:  docsService,
        driveClient: driveService,
    }, nil
}

// CopyTemplate creates a copy of the template document
func (s *DocsPkg) CopyTemplate(templateID string, documentName string) (string, error) {
    // Create a copy of the template
    file := &drive.File{
        Name: documentName,
    }

    // Execute the copy operation
    copiedFile, err := s.driveClient.Files.Copy(templateID, file).Do()
    if err != nil {
        return "", fmt.Errorf("failed to copy template: %w", err)
    }

    return copiedFile.Id, nil
}

// ReplacePlaceholders replaces all placeholders in the document
func (s *DocsPkg) ReplacePlaceholders(documentID string, data PlaceholderData) error {
    // Create a batch update request
    var requests []*docs.Request

    // Add a replace text request for each placeholder
    for placeholder, value := range data {
        requests = append(requests, &docs.Request{
            ReplaceAllText: &docs.ReplaceAllTextRequest{
                ContainsText: &docs.SubstringMatchCriteria{
                    Text:      "{{" + placeholder + "}}",
                    MatchCase: true,
                },
                ReplaceText: value,
            },
        })
    }

    // Execute the batch update
    _, err := s.docsClient.Documents.BatchUpdate(documentID, &docs.BatchUpdateDocumentRequest{
        Requests: requests,
    }).Do()

    if err != nil {
        return fmt.Errorf("failed to replace placeholders: %w", err)
    }

    return nil
}

// ExportToPDF exports the document as PDF
func (s *DocsPkg) ExportToPDF(documentID string) ([]byte, error) {
    // Export the file as PDF
    resp, err := s.driveClient.Files.Export(documentID, "application/pdf").Download()
    if err != nil {
        return nil, fmt.Errorf("failed to export as PDF: %w", err)
    }
    defer resp.Body.Close()

    // Read the response
    pdfBytes, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("failed to read PDF content: %w", err)
    }

    return pdfBytes, nil
}

Enter fullscreen mode Exit fullscreen mode

The NewDocsService function handles the messy authentication setup. It reads your JSON credentials, creates JWT config with proper scopes, and initializes both API clients. This gives you everything needed for document operations in one go.

Testing the Package
Here's a simple test to verify our Google Docs package works:

func TestGoogleDocsPackage(t *testing.T) {
    ctx := context.Background()

    // Initialize the service with your credentials
    docsService, err := google.NewDocsService(ctx, "path/to/your/credentials.json")
    if err != nil {
        t.Fatalf("Failed to create docs service: %v", err)
    }

    // Template document ID (create a test document in Google Drive first)
    templateID := "your-template-document-id"

    // Copy the template
    newDocID, err := docsService.CopyTemplate(templateID, "Test Document Copy")
    if err != nil {
        t.Fatalf("Failed to copy template: %v", err)
    }
    t.Logf("Created document: %s", newDocID)

    // Replace placeholders
    placeholders := google.PlaceholderData{
        "NAME":    "John Doe",
        "DATE":    "June 15, 2025",
        "COMPANY": "Test Corp",
    }

    err = docsService.ReplacePlaceholders(newDocID, placeholders)
    if err != nil {
        t.Fatalf("Failed to replace placeholders: %v", err)
    }

    // Export to PDF
    pdfBytes, err := docsService.ExportToPDF(newDocID)
    if err != nil {
        t.Fatalf("Failed to export PDF: %v", err)
    }

    if len(pdfBytes) == 0 {
        t.Fatal("PDF export returned empty data")
    }

    t.Logf("PDF exported successfully, size: %d bytes", len(pdfBytes))
}
Enter fullscreen mode Exit fullscreen mode

This test demonstrates the complete workflow: copying a template document, replacing placeholder text with actual values, and exporting the result as PDF. Create a test document in Google Drive with placeholders like {{NAME}} and {{DATE}} to see the replacement in action.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.