← Voltar para o blog

Go e WebAssembly: Como Compilar Go para Rodar no Browser

Aprenda a compilar Go para WebAssembly e rodar código Go no browser. Guia prático com exemplos, TinyGo, manipulação do DOM e comparação com outras linguagens.

WebAssembly (WASM) permite rodar código compilado diretamente no browser com performance próxima ao nativo. E o Go tem suporte oficial a WebAssembly desde o Go 1.11 — você pode compilar qualquer programa Go para WASM e executá-lo em qualquer navegador moderno.

Neste guia, você vai aprender a compilar Go para WebAssembly, interagir com o DOM, usar TinyGo para binários menores e construir uma aplicação prática que roda 100% no browser.

Por que Go + WebAssembly?

Se você já programa em Go para backend ou APIs REST, WebAssembly abre a porta para reutilizar lógica no frontend. Alguns casos de uso reais:

  • Validação de dados — a mesma lógica de validação no servidor e no cliente
  • Processamento de dados — parsing de CSV, cálculos financeiros, criptografia
  • Ferramentas interativas — editores, calculadoras, visualizações
  • Jogos simples — engines leves e simulações
  • Portabilidade — bibliotecas Go existentes rodando no browser

A vantagem sobre JavaScript puro é a performance em operações CPU-intensive e a possibilidade de compartilhar código entre backend e frontend.

Configurando o Ambiente

Para compilar Go para WASM, você não precisa instalar nada extra — o compilador padrão do Go já suporta o target GOOS=js GOARCH=wasm. Vamos começar:

// main.go — nosso primeiro programa Go para o browser
package main

import (
    "fmt"
    "syscall/js"
)

func main() {
    fmt.Println("Go rodando no browser via WebAssembly!")

    // Mantém o programa rodando (sem isso, o WASM termina imediatamente)
    select {}
}

Para compilar, use as variáveis de ambiente GOOS e GOARCH:

# Compilar Go para WebAssembly
GOOS=js GOARCH=wasm go build -o main.wasm main.go

# Copiar o arquivo JavaScript de suporte do Go
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

O arquivo wasm_exec.js é o runtime JavaScript que o Go precisa para funcionar no browser. Ele configura o ambiente, gerencia goroutines e faz a ponte entre Go e JavaScript.

Criando a Página HTML

Agora precisamos de uma página HTML que carrega o WASM:

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="utf-8">
    <title>Go WebAssembly Demo</title>
    <script src="wasm_exec.js"></script>
</head>
<body>
    <h1>Go + WebAssembly</h1>
    <div id="resultado"></div>

    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(
            fetch("main.wasm"),
            go.importObject
        ).then((result) => {
            go.run(result.instance);
        });
    </script>
</body>
</html>

Para servir localmente, você pode usar qualquer servidor HTTP. O próprio Go serve:

# Servidor HTTP simples em Go para testar
go run golang.org/x/tools/cmd/httpserve@latest -addr :8080 .

Abra http://localhost:8080 e veja “Go rodando no browser via WebAssembly!” no console do browser.

Interagindo com o DOM

O pacote syscall/js permite manipular o DOM, chamar funções JavaScript e expor funções Go para o JavaScript. Vamos criar uma calculadora interativa:

package main

import (
    "fmt"
    "math"
    "syscall/js"
)

// calcularIMC recebe peso e altura do JavaScript e retorna o IMC
func calcularIMC(this js.Value, args []js.Value) interface{} {
    if len(args) < 2 {
        return "Informe peso (kg) e altura (m)"
    }

    peso := args[0].Float()
    altura := args[1].Float()

    if altura <= 0 {
        return "Altura deve ser maior que zero"
    }

    imc := peso / math.Pow(altura, 2)

    var classificacao string
    switch {
    case imc < 18.5:
        classificacao = "Abaixo do peso"
    case imc < 25:
        classificacao = "Peso normal"
    case imc < 30:
        classificacao = "Sobrepeso"
    default:
        classificacao = "Obesidade"
    }

    resultado := fmt.Sprintf("IMC: %.1f — %s", imc, classificacao)

    // Atualizar o DOM diretamente do Go
    document := js.Global().Get("document")
    elemento := document.Call("getElementById", "resultado")
    elemento.Set("textContent", resultado)

    return resultado
}

func main() {
    // Expor a função Go para o JavaScript global
    js.Global().Set("calcularIMC", js.FuncOf(calcularIMC))

    fmt.Println("Funções Go registradas no browser!")

    // Manter o programa ativo
    select {}
}

Agora o JavaScript pode chamar calcularIMC(80, 1.75) diretamente. A função Go manipula o DOM, faz o cálculo e atualiza a página — tudo rodando em WebAssembly.

Callbacks e Eventos do DOM

Você pode registrar event listeners diretamente do Go:

func registrarEventos() {
    document := js.Global().Get("document")

    // Registrar click handler em um botão
    botao := document.Call("getElementById", "btn-calcular")
    botao.Call("addEventListener", "click", js.FuncOf(
        func(this js.Value, args []js.Value) interface{} {
            // Ler valores dos inputs
            peso := document.Call("getElementById", "peso").Get("value").String()
            altura := document.Call("getElementById", "altura").Get("value").String()

            fmt.Printf("Calculando IMC: peso=%s, altura=%s\n", peso, altura)
            // ... processar
            return nil
        },
    ))
}

Essa abordagem é poderosa para criar interfaces interativas complexas. Mas repare: o código fica verboso comparado ao JavaScript. É aqui que o Go + WASM brilha em lógica pesada, não em manipulação simples do DOM.

TinyGo: Binários 90% Menores

O compilador padrão do Go gera binários WASM relativamente grandes (2-10 MB) porque inclui o runtime completo com garbage collector, scheduler de goroutines e todo o sistema de tipos. Para aplicações web, isso é um problema.

TinyGo é um compilador alternativo otimizado para targets pequenos como WebAssembly e microcontroladores:

# Instalar TinyGo
# macOS
brew install tinygo

# Compilar com TinyGo (binários muito menores!)
tinygo build -o main.wasm -target wasm main.go

Comparação de tamanhos típicos:

CompiladorBinário WASMTempo de carga
Go padrão~5 MB~2s
TinyGo~200 KB~50ms
TinyGo + gzip~80 KB~20ms

A diferença é enorme. TinyGo consegue isso removendo reflection, simplificando o GC e otimizando agressivamente o código morto. A desvantagem? Nem todos os pacotes da standard library são suportados. Mas para WASM no browser, a maioria dos casos funciona perfeitamente.

Exemplo Prático: Validador de CPF no Browser

Vamos criar algo útil — um validador de CPF que roda inteiramente no browser:

package main

import (
    "fmt"
    "strconv"
    "strings"
    "syscall/js"
)

// validarCPF implementa o algoritmo de validação de CPF
func validarCPF(cpf string) bool {
    // Remover pontos e traço
    cpf = strings.ReplaceAll(cpf, ".", "")
    cpf = strings.ReplaceAll(cpf, "-", "")

    if len(cpf) != 11 {
        return false
    }

    // Verificar se todos os dígitos são iguais (ex: 111.111.111-11)
    todosIguais := true
    for i := 1; i < len(cpf); i++ {
        if cpf[i] != cpf[0] {
            todosIguais = false
            break
        }
    }
    if todosIguais {
        return false
    }

    // Calcular primeiro dígito verificador
    soma := 0
    for i := 0; i < 9; i++ {
        digito, _ := strconv.Atoi(string(cpf[i]))
        soma += digito * (10 - i)
    }
    resto := soma % 11
    primeiroDigito := 0
    if resto >= 2 {
        primeiroDigito = 11 - resto
    }

    // Calcular segundo dígito verificador
    soma = 0
    for i := 0; i < 10; i++ {
        digito, _ := strconv.Atoi(string(cpf[i]))
        soma += digito * (11 - i)
    }
    resto = soma % 11
    segundoDigito := 0
    if resto >= 2 {
        segundoDigito = 11 - resto
    }

    // Comparar com os dígitos informados
    digitoVerif1, _ := strconv.Atoi(string(cpf[9]))
    digitoVerif2, _ := strconv.Atoi(string(cpf[10]))

    return primeiroDigito == digitoVerif1 && segundoDigito == digitoVerif2
}

// validarCPFJS expõe a validação para JavaScript
func validarCPFJS(this js.Value, args []js.Value) interface{} {
    if len(args) < 1 {
        return "Informe um CPF"
    }

    cpf := args[0].String()
    valido := validarCPF(cpf)

    document := js.Global().Get("document")
    resultado := document.Call("getElementById", "resultado")

    if valido {
        resultado.Set("innerHTML", fmt.Sprintf(
            "<span style='color:green'>✓ CPF %s é válido</span>", cpf))
    } else {
        resultado.Set("innerHTML", fmt.Sprintf(
            "<span style='color:red'>✗ CPF %s é inválido</span>", cpf))
    }

    return valido
}

func main() {
    js.Global().Set("validarCPF", js.FuncOf(validarCPFJS))
    fmt.Println("Validador de CPF Go+WASM carregado!")
    select {}
}

Esse é um caso perfeito para WASM: a mesma lógica de validação que você usa no backend Go agora roda no browser, sem duplicar código em JavaScript.

Go WASM vs Outras Linguagens

Como o Go se compara a Rust e C++ para WebAssembly?

AspectoGoRustC/C++
Facilidade★★★★★★★★★★
Tamanho binário★★ (★★★★ com TinyGo)★★★★★★★★★
Performance★★★★★★★★★★★★★★
Ecossistema WASM★★★★★★★★★★★
Interop com JS★★★★★★★★★

Go ganha na simplicidade: qualquer desenvolvedor Go consegue gerar WASM em minutos. Rust oferece melhor performance e binários menores, mas com curva de aprendizado muito maior. Se você já domina Go, WASM é uma extensão natural do seu skillset.

Boas Práticas para Go + WASM

  1. Use TinyGo para produção — binários menores significam carregamento mais rápido
  2. Minimize a manipulação do DOM — use Go para lógica pesada, JavaScript para UI simples
  3. Gerencie memóriajs.FuncOf cria funções que precisam de Release() quando não mais necessárias
  4. Sirva com gzip/brotli — WASM comprime muito bem (60-80% de redução)
  5. Use Web Workers — rode o WASM em uma thread separada para não bloquear a UI
  6. Teste no Go padrão primeiro — use GOOS=js apenas no build final, teste a lógica com testes normais

Quando Usar (e Quando Não Usar)

Use Go + WASM quando:

  • Precisa compartilhar lógica entre backend e frontend
  • Tem processamento CPU-intensive no browser
  • Já tem código Go que quer reutilizar no cliente
  • Está construindo ferramentas/utilitários web (como o validador de CPF acima)

Não use quando:

  • A aplicação é puramente UI/DOM-heavy
  • O tamanho do bundle é crítico e TinyGo não suporta os pacotes necessários
  • Um framework JavaScript resolve mais facilmente

Próximos Passos

WebAssembly com Go é uma tecnologia madura e prática. Se você quer criar aplicações web modernas sem depender de frameworks JavaScript pesados, considere também a combinação de Go com HTMX para interfaces dinâmicas server-side.

Para se aprofundar, explore os módulos Go para organizar seus projetos WASM, e confira como tratar erros corretamente nas funções que expõe ao JavaScript — erros silenciosos no WASM são difíceis de debugar.

Se você está começando com Go, nosso guia de primeiros passos cobre tudo que você precisa para configurar o ambiente. E para quem quer entender como Go se compara com outras linguagens, temos comparações com Rust, Python e Java.