Linguagem

Guia Completo de Go — Básico

Go 1.23 do zero: tipos, controle de fluxo, funções, slices, maps, interfaces, goroutines, channels.

Progresso
0%

Setup & Hello World

Go é uma linguagem compilada, estaticamente tipada e criada pelo Google em 2009. A instalação entrega um toolchain completo (go build, go test, go fmt, go vet) sem depender de gerenciadores externos. Use o mise ou baixe direto em go.dev/dl.

Instalação e primeiro módulo

# 1. Instale o Go (via mise/asdf ou pacote oficial)
mise use -g [email protected]

# 2. Verifique a versão
go version
# → go version go1.23.0 darwin/arm64

# 3. Crie a pasta do projeto e inicialize um módulo
mkdir hello && cd hello
go mod init github.com/kaique/hello
# → cria o arquivo go.mod

# 4. Estrutura mínima gerada
.
├── go.mod       # manifesto do módulo (equivalente ao package.json)
└── go.sum       # checksums de dependências (só aparece após go get)

Hello World idiomático

// main.go — todo executável Go começa em package main + func main
package main

import "fmt"

func main() {
    fmt.Println("Olá, Go 1.23!")
}
# Executar sem gerar binário (ótimo para scripts)
go run .
# → Olá, Go 1.23!

# Compilar um binário nativo estático
go build -o bin/hello .
./bin/hello
# → Olá, Go 1.23!

# Formatar todo o código do projeto (padrão oficial)
go fmt ./...

# Rodar o linter embutido
go vet ./...
go.mod e go.sum: o go.mod declara o nome do módulo, a versão do Go e as dependências diretas. O go.sum grava hashes criptográficos de cada versão baixada — nunca edite à mão e sempre comite os dois arquivos.

go run

Compila e executa em memória. Usado durante o desenvolvimento para feedback rápido.

go build

Gera um binário nativo estático. Deploy sem runtime — basta copiar o executável.

go mod tidy

Sincroniza o go.mod: adiciona dependências usadas e remove as órfãs.

go install

Compila e instala um binário global em $GOPATH/bin. Ótimo para CLIs.

Tipos e Variáveis

Go tem tipagem estática com inferência via :=. O zero value de cada tipo é sempre definido (0, "", false, nil) — nunca há variável "indefinida" como em JavaScript.

Declaração de variáveis

package main

import "fmt"

func main() {
    // Declaração explícita com var + tipo
    var nome string = "Kaique"
    var idade int = 28

    // Tipo inferido (dentro de funções) — forma idiomática
    ativo := true
    saldo := 1250.75

    // Múltipla atribuição
    x, y := 10, 20

    // Zero values (sem inicialização)
    var contador int       // 0
    var titulo string      // ""
    var pronto bool        // false
    var ptr *int           // nil

    fmt.Println(nome, idade, ativo, saldo, x, y)
    fmt.Println(contador, titulo, pronto, ptr)
}

Tipos primitivos fundamentais

// Inteiros — escolha o tamanho quando importa
var i8  int8  = 127              // -128..127
var i16 int16 = 32_000
var i32 int32 = 2_000_000_000
var i64 int64 = 9_000_000_000_000

// int é alias para int32 ou int64 conforme a arquitetura (use por padrão)
var n int = 42

// Não-assinados (unsigned)
var u8  uint8  = 255
var u16 uint16 = 65_535

// Ponto flutuante
var pi float64 = 3.14159   // padrão para decimais
var f32 float32 = 1.5

// Booleano
var ligado bool = true

// String imutável (UTF-8 sob o capô)
var saudacao string = "Olá, mundo"

// rune = int32 — representa um code point Unicode
var letra rune = 'K'

// byte = uint8 — usado para dados binários e ASCII
var b byte = 'A'

Constantes e iota

const Pi = 3.14159
const MaxRetries int = 5

// Bloco de constantes
const (
    StatusPendente = "pending"
    StatusAtivo    = "active"
    StatusFalha    = "failed"
)

// iota — gerador de inteiros sequenciais (começa em 0)
const (
    Domingo = iota // 0
    Segunda         // 1
    Terca           // 2
    Quarta          // 3
    Quinta          // 4
    Sexta           // 5
    Sabado          // 6
)

// iota com expressões (flags binárias)
const (
    FlagLeitura  = 1 << iota // 1  (001)
    FlagEscrita              // 2  (010)
    FlagExecucao             // 4  (100)
)

Conversão de tipos (explícita sempre)

// Go NÃO faz conversão implícita entre tipos numéricos
var i int = 42
var f float64 = float64(i)   // precisa converter
var u uint = uint(f)          // precisa converter
// var err = i + f            // ❌ erro de compilação

// string ↔ []byte e string ↔ []rune
s := "Olá"
bs := []byte(s)      // [79 108 195 131]
rs := []rune(s)      // [79 108 227]  (code points)
back := string(bs)   // "Olá"

// int ↔ string (via strconv)
import "strconv"
n, err := strconv.Atoi("42")       // "42" → 42
texto := strconv.Itoa(99)           // 99 → "99"
_ = err
rune vs byte: use rune para trabalhar com caracteres Unicode (emojis, acentos) e byte para dados binários ou ASCII puro. Iterar uma string com range entrega runes; indexar com s[i] entrega bytes.
Zero value > null: Go não tem undefined. Variáveis não inicializadas recebem um valor neutro — isso elimina toda uma classe de bugs do JavaScript/Java.

Controle de Fluxo

Go tem apenas uma palavra-chave de loop: for. E também dispensa parênteses em condições. A sintaxe é deliberadamente minimalista — menos formas de escrever a mesma coisa significa código mais uniforme em qualquer codebase.

if, else e atribuição curta

package main

import (
    "fmt"
    "strconv"
)

func main() {
    idade := 20

    if idade >= 18 {
        fmt.Println("maior de idade")
    } else if idade >= 16 {
        fmt.Println("pode votar")
    } else {
        fmt.Println("menor")
    }

    // Short statement — declara e testa na mesma linha
    if n, err := strconv.Atoi("42"); err == nil {
        fmt.Println("número:", n) // n e err só existem aqui dentro
    }
}

switch — mais expressivo que em outras linguagens

func classificar(n int) string {
    switch {
    case n < 0:
        return "negativo"
    case n == 0:
        return "zero"
    case n < 10:
        return "pequeno"
    default:
        return "grande"
    }
}

// Switch com valor + múltiplos casos por linha
func diaUtil(dia string) bool {
    switch dia {
    case "sábado", "domingo":
        return false
    default:
        return true
    }
}

// Type switch — inspeciona o tipo dinâmico de interface{}
func describe(x any) string {
    switch v := x.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("bool: %t", v)
    default:
        return "tipo desconhecido"
    }
}

for — única estrutura de loop

// Clássico C-style
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// Enquanto (equivalente a while)
n := 10
for n > 0 {
    n--
}

// Loop infinito + break
for {
    if algumaCondicao() {
        break
    }
}

// range — itera slices, maps, strings, channels
nomes := []string{"Ana", "Bia", "Caio"}
for i, nome := range nomes {
    fmt.Printf("%d: %s\n", i, nome)
}

// Ignorar o índice com _
for _, nome := range nomes {
    fmt.Println(nome)
}

// range em string entrega runes (Unicode correto)
for i, r := range "Olá" {
    fmt.Printf("byte %d = %c\n", i, r)
}

// range em map (ordem NÃO é garantida)
idades := map[string]int{"Ana": 30, "Bia": 25}
for nome, idade := range idades {
    fmt.Println(nome, idade)
}

// Go 1.22+ — range sobre inteiro
for i := range 10 {
    fmt.Println(i) // 0..9
}

defer — execução adiada

import "os"

func lerArquivo(caminho string) error {
    f, err := os.Open(caminho)
    if err != nil {
        return err
    }
    defer f.Close() // executa no return, mesmo em panic

    // ... trabalho com o arquivo
    return nil
}

// defers empilham em ordem LIFO (último entra, primeiro sai)
func ordem() {
    defer fmt.Println("primeiro")
    defer fmt.Println("segundo")
    defer fmt.Println("terceiro")
    // Saída: terceiro → segundo → primeiro
}
defer é o equivalente idiomático do try/finally. Use sempre que precisar garantir cleanup: fechar arquivos, liberar mutex, desconectar DB. Coloque o defer logo após adquirir o recurso — fica impossível esquecer.

Funções e Erros

Funções em Go são cidadãos de primeira classe: podem retornar múltiplos valores, aceitar argumentos variádicos, formar closures e servir de parâmetro. O tratamento de erros é explícito via retorno (T, error) — sem exceptions.

Múltiplos retornos e named returns

package main

import (
    "errors"
    "fmt"
)

// Retorno múltiplo — padrão (valor, erro)
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}

// Named returns — documenta e permite return "pelado"
func minMax(nums []int) (min, max int, err error) {
    if len(nums) == 0 {
        err = errors.New("slice vazio")
        return
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return // retorna min, max, err (zero value)
}

func main() {
    r, err := dividir(10, 2)
    if err != nil {
        fmt.Println("erro:", err)
        return
    }
    fmt.Println("resultado:", r)
}

Variádicas e closures

// Variádica — 0..N argumentos do mesmo tipo
func soma(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

soma()              // 0
soma(1, 2, 3)       // 6
nums := []int{1, 2, 3, 4}
soma(nums...)       // expansão de slice com ...

// Closure — captura variáveis do escopo
func contador() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

c := contador()
fmt.Println(c(), c(), c()) // 1 2 3

// Função como parâmetro
func aplicar(nums []int, f func(int) int) []int {
    out := make([]int, len(nums))
    for i, n := range nums {
        out[i] = f(n)
    }
    return out
}

dobro := aplicar([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2 4 6]

Tratamento de erros idiomático

import (
    "errors"
    "fmt"
    "os"
)

// 1. Sentinel error — valor exportado para comparação
var ErrNaoEncontrado = errors.New("usuário não encontrado")

func buscar(id string) (string, error) {
    if id == "" {
        return "", ErrNaoEncontrado
    }
    return "Kaique", nil
}

// 2. Custom error type — permite carregar contexto
type ErroHTTP struct {
    Status int
    Msg    string
}

func (e *ErroHTTP) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.Status, e.Msg)
}

// 3. Wrap de erro com %w — preserva a cadeia
func carregar() error {
    _, err := os.Open("config.json")
    if err != nil {
        return fmt.Errorf("carregar config: %w", err)
    }
    return nil
}

// 4. errors.Is — compara com sentinela (mesmo após wrap)
// 5. errors.As — extrai tipo concreto da cadeia
func tratar(err error) {
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("arquivo não existe, criando default")
        return
    }

    var httpErr *ErroHTTP
    if errors.As(err, &httpErr) {
        fmt.Printf("status %d\n", httpErr.Status)
    }
}

panic e recover (use com cautela)

// panic — interrompe o fluxo normal (reservado para erros irrecuperáveis)
func dividirUnsafe(a, b int) int {
    if b == 0 {
        panic("divisão por zero!")
    }
    return a / b
}

// recover — captura panic dentro de defer (contém o estrago)
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recuperado: %v", r)
        }
    }()
    return dividirUnsafe(a, b), nil
}
Padrão canônico: chame a função → teste err != nil → retorne cedo (early return). Evite panic em código de biblioteca — reserve para bugs de programação (nil pointer, índice fora do range) ou inicialização que falhou em tempo de boot.
Nunca ignore erros com _, _ = f() em código de produção. Se não importa o erro, comente explicitamente. O linter errcheck vigia isso para você.

Estruturas Compostas

Go oferece quatro estruturas compostas principais: array (tamanho fixo), slice (tamanho dinâmico), map (dicionário) e struct (registro nomeado). Structs + métodos + interfaces pequenas formam o coração do modelo de objetos de Go.

Arrays e slices

// Array — tamanho fixo na declaração (raro no dia a dia)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{10, 20, 30} // compilador conta

// Slice — wrapper dinâmico sobre um array (estrutura principal)
nums := []int{1, 2, 3, 4, 5}

// make — aloca slice com tamanho e capacidade
s := make([]int, 3, 10) // len=3, cap=10 — pré-aloca sem realocar

// append — cresce o slice (pode realocar se cap estourar)
s = append(s, 99, 100)

// Slicing — subfatias (compartilha array subjacente!)
meio := nums[1:4] // [2 3 4]

// len e cap
fmt.Println(len(s), cap(s))

// Cópia profunda
dst := make([]int, len(nums))
copy(dst, nums)

Maps (hash tables)

// Literal
idades := map[string]int{
    "Ana": 30,
    "Bia": 25,
}

// make — alocação explícita
m := make(map[string]int)
m["Caio"] = 40

// Acesso com "comma-ok" — distingue zero value de ausência
if idade, ok := idades["Ana"]; ok {
    fmt.Println("Ana tem", idade)
} else {
    fmt.Println("Ana não está no map")
}

// Remover entrada
delete(idades, "Ana")

// Iterar (ordem aleatória a cada execução — by design)
for nome, idade := range idades {
    fmt.Println(nome, idade)
}

// Map vazio ≠ nil
var vazio map[string]int           // nil — escrita causa panic!
usavel := map[string]int{}         // usável
outro  := make(map[string]int)     // idem

Structs e métodos

// Struct — registro tipado (equivalente a class sem herança)
type Usuario struct {
    ID    string
    Nome  string
    Email string
    Idade int
}

// Instanciação
u1 := Usuario{ID: "1", Nome: "Kaique", Email: "[email protected]", Idade: 28}
u2 := Usuario{} // todos os campos com zero value
u3 := &Usuario{Nome: "Ana"} // ponteiro para heap

// Acesso com ponto (mesmo em ponteiro — auto-deref)
fmt.Println(u1.Nome, u3.Nome)

// Método com value receiver — opera numa cópia
func (u Usuario) Saudacao() string {
    return "Olá, " + u.Nome
}

// Método com pointer receiver — permite mutação
func (u *Usuario) Envelhecer() {
    u.Idade++
}

u1.Envelhecer() // Go auto-converte para (&u1).Envelhecer()
fmt.Println(u1.Saudacao())

Embedding (composição sem herança)

type Timestamp struct {
    CriadoEm     time.Time
    AtualizadoEm time.Time
}

// Embedding — Artigo "herda" campos e métodos de Timestamp
type Artigo struct {
    Timestamp          // campo anônimo
    Titulo   string
    Conteudo string
}

a := Artigo{
    Timestamp: Timestamp{CriadoEm: time.Now()},
    Titulo:    "Go é simples",
}
fmt.Println(a.CriadoEm) // acesso direto (promovido)

Interfaces pequenas

// Interface = contrato (conjunto de métodos)
// Go é ESTRUTURALMENTE tipado: quem implementa os métodos, satisfaz a interface
type Saudador interface {
    Saudacao() string
}

// Usuario já implementa Saudacao — nenhuma declaração "implements" necessária
var s Saudador = u1
fmt.Println(s.Saudacao())

// Interface vazia — any em Go 1.18+
func debug(x any) {
    fmt.Printf("%T: %v\n", x, x)
}

// Type assertion — extrair tipo concreto
var i any = "hello"
str, ok := i.(string) // "hello", true
n, ok := i.(int)      // 0, false
Interfaces pequenas ganham: a stdlib prefere interfaces de 1–3 métodos (io.Reader, io.Writer, fmt.Stringer). Quanto menor a interface, mais fácil de implementar e mockar. O provérbio de Go: "accept interfaces, return structs".

Value vs pointer receiver

Use pointer (*T) quando o método muda o estado ou o struct é grande. Value quando é imutável e pequeno.

Tags de struct

Metadados para reflection: json:"nome", bson:"_id", validate:"required".

Concorrência Básica

Concorrência é a feature estrela de Go. Goroutines são threads ultraleves (2KB de stack inicial) e channels permitem comunicação sem shared memory. O lema: "Don't communicate by sharing memory; share memory by communicating."

Goroutines

package main

import (
    "fmt"
    "time"
)

func trabalhar(id int) {
    fmt.Printf("worker %d começou\n", id)
    time.Sleep(200 * time.Millisecond)
    fmt.Printf("worker %d terminou\n", id)
}

func main() {
    // go + chamada = dispara em goroutine (assíncrono)
    for i := 1; i <= 3; i++ {
        go trabalhar(i)
    }

    // main não espera goroutines — precisamos sincronizar
    time.Sleep(500 * time.Millisecond)
    fmt.Println("fim")
}

Channels — comunicação tipada

// Channel unbuffered — envio e recepção bloqueiam até sincronizar
ch := make(chan int)

go func() {
    ch <- 42 // envia
}()

valor := <-ch // recebe
fmt.Println(valor) // 42

// Channel buffered — até N envios não bloqueiam
buf := make(chan string, 3)
buf <- "a"
buf <- "b"
buf <- "c"
// buf <- "d"  // bloqueia (buffer cheio)

close(buf) // sinaliza fim

// range consome até o channel ser fechado
for msg := range buf {
    fmt.Println(msg) // a, b, c
}

// Comma-ok detecta channel fechado
v, ok := <-buf
if !ok {
    fmt.Println("channel fechado")
}

select — multiplexação de channels

func consumirComTimeout(dados <-chan string) {
    select {
    case msg := <-dados:
        fmt.Println("recebi:", msg)
    case <-time.After(2 * time.Second):
        fmt.Println("timeout!")
    }
}

// select com default — não-bloqueante
func tryReceive(ch <-chan int) {
    select {
    case v := <-ch:
        fmt.Println("valor:", v)
    default:
        fmt.Println("nada disponível agora")
    }
}

sync.WaitGroup — esperar goroutines terminarem

import "sync"

func baixarTudo(urls []string) {
    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1) // incrementa ANTES da goroutine
        go func(u string) {
            defer wg.Done() // decrementa ao terminar
            fmt.Println("baixando", u)
            time.Sleep(300 * time.Millisecond)
        }(url) // passa url por valor para evitar captura buggy
    }

    wg.Wait() // bloqueia até contador zerar
    fmt.Println("tudo pronto")
}

sync.Mutex — proteção de estado compartilhado

type Contador struct {
    mu sync.Mutex
    n  int
}

func (c *Contador) Incrementa() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

func (c *Contador) Valor() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}

func main() {
    c := &Contador{}
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Incrementa()
        }()
    }
    wg.Wait()
    fmt.Println("total:", c.Valor()) // 1000 (sempre)
}

context.Context — cancelamento e deadlines

import (
    "context"
    "fmt"
    "time"
)

func tarefaLonga(ctx context.Context) error {
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err() // cancelled / deadline exceeded
        case <-time.After(500 * time.Millisecond):
            fmt.Println("passo", i)
        }
    }
    return nil
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    if err := tarefaLonga(ctx); err != nil {
        fmt.Println("encerrada:", err)
    }
}
Regra de ouro: todo context.Context deve ser o primeiro parâmetro da função (ctx context.Context). Toda chamada I/O (HTTP, DB, cache) deve aceitar ctx para permitir cancelamento propagado.
Cuidado com vazamentos: uma goroutine bloqueada num channel que nunca recebe valor vaza memória. Sempre garanta um caminho de saída — use context, select com default ou feche channels explicitamente.
Roadmap Go← Todos os treinamentos