DevTools

Cheatsheet Go

Linguagem de programação do Google

Voltar às linguagens
Go
84 cards encontrados
Categorias:
Versões:

Sintaxe Básica


11 cards
Variáveis
package main

import "fmt"

func main() {
    var nome string = "Ana"
    var idade int = 30
    var ativo bool = true
    var x, y int = 1, 2

    fmt.Println(nome, idade, ativo, x, y)
}

Declara variáveis com var, indicando o tipo ou deixando o Go inferi-lo. Podes declarar várias na mesma linha. O valor inicial, se omitido, é o zero value do tipo.

Operadores
// Aritméticos
+  -  *  /  %

// Comparação
==  !=  >  <  >=  <=

// Lógicos
&&  ||  !

// Bitwise
&  |  ^  <<  >>

// Endereço e dereferência
&x   *p

Go tem operadores aritméticos, de comparação, lógicos (&&, ||, !) e bitwise. % dá o resto da divisão. & obtém o endereço de uma variável e * desreferencia um ponteiro.

Conversões de tipo
var f float64 = 3.99
var i int = int(f)        // 3 (trunca)

var n int = 65
var c string = string(n)  // "A" (rune)

// Não há conversão implícita
var x int = 10
// var y float64 = x        // ERRO
var y float64 = float64(x)  // OK

Go não faz conversões automáticas entre tipos: é preciso converter explicitamente com int(), float64(), etc. Converter float64 para int trunca a parte decimal.

Inferência com :=
func main() {
    preco := 9.99     // float64
    nome := "Ana"     // string
    contador := 0     // int
    ok := true        // bool

    // várias de uma vez
    a, b := 1, 2
}

O operador := declara e infere o tipo a partir do valor, mas só funciona dentro de funções. É a forma mais comum de criar variáveis locais. Ao nível do pacote usa var.

Zero values
var i int            // 0
var f float64        // 0.0
var s string         // ""
var b bool           // false
var p *int           // nil
var sl []int         // nil
var m map[string]int // nil

Toda variável em Go tem um valor inicial automático, o zero value: 0 para números, string vazia para string, false para bool e nil para ponteiros, slices e maps. Não há variáveis não inicializadas.

strconv
import "strconv"

// int -> string
s := strconv.Itoa(42)          // "42"

// string -> int
n, err := strconv.Atoi("123")  // 123

// outros
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
strconv.ParseFloat("3.14", 64)
strconv.FormatBool(true)       // "true"
strconv.ParseBool("true")

O pacote strconv converte entre strings e tipos básicos. Itoa() transforma int em string e Atoi() faz o inverso, devolvendo também um error se o texto não for válido.

Tipos de dados
// Inteiros
int, int8, int16, int32, int64
uint, uint8, uint16, uint32, uint64

// Números com vírgula
float32, float64

// Outros
string  // texto (imutável)
bool    // true / false
byte    // alias de uint8
rune    // alias de int32 (Unicode)
complex64, complex128

Go é fortemente tipado. Os inteiros têm tamanhos fixos (int8 a int64); int depende da arquitetura. float64 é o padrão para decimais e rune representa um carácter Unicode.

Strings básicas
s := "Olá Mundo"

len(s)     // comprimento em bytes
s[0]       // primeiro byte
s[0:3]     // substring "Olá"

// Concatenar
nome := "Olá, " + "Ana"

// String bruta (multi-linha)
texto := `linha 1
linha 2`

Strings são sequências imutáveis de bytes. len() devolve o tamanho em bytes e s[0:3] extrai uma parte. Crases criam strings brutas que aceitam várias linhas sem escapes.

Saída com fmt
import "fmt"

fmt.Println("Olá")             // com newline
fmt.Print("sem ", "newline")
fmt.Printf("Nome: %s\n", "Ana")

// Verbos comuns
fmt.Printf("%d\n", 42)         // inteiro
fmt.Printf("%.2f\n", 3.14159)  // 3.14
fmt.Printf("%t\n", true)       // bool
fmt.Printf("%v\n", objeto)     // valor padrão
fmt.Printf("%+v\n", struct1)   // com campos
fmt.Printf("%T\n", 3.14)       // tipo
fmt.Sprintf("x=%d", 5)         // retorna string

O pacote fmt imprime e formata. Println() adiciona quebra de linha e Printf() usa verbos: %d inteiro, %s string, %f float, %v valor padrão. Sprintf() retorna a string formatada.

Constantes e iota
const Pi = 3.14159

const (
    StatusOK       = 200
    StatusNotFound = 404
)

// iota: contador automático
const (
    Domingo = iota // 0
    Segunda        // 1
    Terca          // 2
    Quarta         // 3
)

Constantes declaram-se com const e não podem mudar. Dentro de um bloco, iota gera números sequenciais automáticos (0, 1, 2...), ideal para enumerações.

Pacote strings
import "strings"

s := "Olá Mundo Go"

strings.Contains(s, "Mundo")          // true
strings.HasPrefix(s, "Olá")           // true
strings.HasSuffix(s, "Go")            // true
strings.Split(s, " ")                 // [Olá Mundo Go]
strings.Join([]string{"a", "b"}, "-") // "a-b"
strings.Replace(s, "o", "0", -1)
strings.ToUpper(s)
strings.ToLower(s)
strings.Trim("  x  ", " ")            // "x"

O pacote strings traz funções para manipular texto: Contains() procura, Split() divide e Join() junta. Replace() com -1 substitui todas as ocorrências.

Controlo de Fluxo


9 cards
if / else
idade := 20

if idade >= 18 {
    fmt.Println("adulto")
} else if idade > 12 {
    fmt.Println("jovem")
} else {
    fmt.Println("criança")
}

O if em Go não usa parênteses na condição, mas as chavetas são obrigatórias. Podes encadear else if e else.

for como while e infinito
// while
x := 10
for x > 0 {
    x--
}

// loop infinito
for {
    if condicao {
        break
    }
}

Go não tem while: usa-se for só com a condição. Um for sem nada cria um ciclo infinito, que termina com break ou return.

defer
f, err := os.Open("ficheiro.txt")
if err != nil {
    return err
}
defer f.Close() // corre no fim

// vários defer: ordem LIFO
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// saída: 3, 2, 1

defer adia a execução de uma chamada para o final da função, ideal para limpeza (fechar ficheiros e ligações). Vários defer executam-se em ordem inversa (LIFO).

if com inicialização
if err := fazer(); err != nil {
    log.Fatal(err)
}
// err não existe aqui

if n, ok := m["chave"]; ok {
    fmt.Println(n)
}

Podes declarar uma variável antes da condição, separada por ;. Ela fica limitada ao bloco do if. É o padrão idiomático para verificar erros e resultados.

break e continue
for i := 0; i < 10; i++ {
    if i == 3 {
        continue // salta o 3
    }
    if i == 7 {
        break // pára no 7
    }
    fmt.Println(i)
}

// break com label (ciclos aninhados)
Loop:
for _, a := range x {
    for _, b := range a {
        if b == alvo {
            break Loop
        }
    }
}

continue salta para a próxima iteração e break termina o ciclo. Em ciclos aninhados, um label (como Loop:) permite ao break sair do ciclo exterior.

for clássico
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// contador decrescente
for i := 10; i > 0; i-- {
    fmt.Println(i)
}

O for é o único ciclo em Go. Na forma clássica tem três partes: inicialização, condição e pós-instrução, separadas por ;. Não usa parênteses.

switch
dia := 2

switch dia {
case 1:
    fmt.Println("Segunda")
case 2, 3:
    fmt.Println("Terça ou Quarta")
case 4, 5:
    fmt.Println("Quinta ou Sexta")
default:
    fmt.Println("Fim de semana")
}

O switch compara um valor com vários case. Em Go, cada caso termina automaticamente (sem break). Podes agrupar vários valores no mesmo case, separados por vírgula.

for range
// slice
for i, v := range slice {
    fmt.Println(i, v)
}

// map
for k, v := range mapa {
    fmt.Println(k, v)
}

// string (runes)
for i, r := range "Olá" {
    fmt.Println(i, string(r))
}

// ignorar índice
for _, v := range slice { }

range percorre slices, maps, strings e channels, devolvendo índice e valor. Usa _ para ignorar o índice. Em strings, itera por rune (carácter Unicode).

switch sem condição
hora := 14

switch {
case hora < 12:
    fmt.Println("Bom dia")
case hora < 20:
    fmt.Println("Boa tarde")
default:
    fmt.Println("Boa noite")
}

// fallthrough (raro)
switch x {
case 1:
    fmt.Println("um")
    fallthrough
case 2:
    fmt.Println("dois")
}

Um switch sem expressão funciona como uma cadeia de if/else if, avaliando cada condição por ordem. fallthrough força a execução do caso seguinte (pouco usado).

Funções


9 cards
Função simples
func somar(a, b int) int {
    return a + b
}

// parâmetros do mesmo tipo
func media(a, b, c float64) float64 {
    return (a + b + c) / 3
}

resultado := somar(3, 4) // 7

Uma função declara-se com func, seguida do nome, parâmetros e tipo de retorno. Parâmetros do mesmo tipo podem partilhar o tipo (a, b int). O retorno vem no fim.

Funções como valor
var fn func(int) int

fn = func(x int) int {
    return x * 2
}

fmt.Println(fn(5)) // 10

// em maps
operacoes := map[string]func(int, int) int{
    "soma": func(a, b int) int { return a + b },
}

Funções são valores de primeira classe em Go: podes guardá-las em variáveis, maps ou slices. O tipo func(int) int descreve uma função que recebe e devolve um int.

Recursão
func fatorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * fatorial(n-1)
}

func fibonacci(n int) int {
    if n < 2 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

fmt.Println(fatorial(5)) // 120

Uma função recursiva chama-se a si própria até atingir um caso base. fatorial() multiplica até chegar a 1. Recursão é elegante, mas para casos simples um ciclo pode ser mais eficiente.

Múltiplos retornos
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}

resultado, err := dividir(10, 2)
if err != nil {
    log.Fatal(err)
}

Go permite devolver vários valores. O padrão idiomático é retornar (resultado, error), onde error é nil quando tudo corre bem. O chamador verifica sempre o erro.

Funções anónimas
// executar imediatamente
func() {
    fmt.Println("executada já")
}()

// com argumento
func(nome string) {
    fmt.Println("Olá,", nome)
}("Ana")

// atribuir a variável
dobro := func(x int) int { return x * 2 }
fmt.Println(dobro(4)) // 8

Uma função anónima não tem nome e pode ser definida e chamada no mesmo local. É muito usada em go func() {...}() para goroutines e como argumento de outras funções.

Retornos nomeados
func calcular(a, b int) (soma, produto int) {
    soma = a + b
    produto = a * b
    return // retorna soma e produto
}

s, p := calcular(3, 4) // 7, 12

Os retornos podem ter nome, servindo como variáveis locais. Um return sem valores devolve os valores nomeados atuais. Útil para documentar o significado de cada retorno.

Closures
func contador() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

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

Uma closure é uma função que captura variáveis do ambiente onde foi criada. Aqui, a função anónima mantém o estado de n entre chamadas sucessivas.

Funções variádicas
func soma(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

soma(1, 2, 3)       // 6
soma(1, 2, 3, 4, 5) // 15

// expandir um slice
vals := []int{1, 2, 3}
soma(vals...)

Parâmetros com ... aceitam um número variável de argumentos, recebidos como um slice. Podes passar um slice existente expandindo-o com vals.... fmt.Println() é variádico.

Higher-order functions
// receber função como parâmetro
func aplicar(nums []int, fn func(int) int) []int {
    res := make([]int, len(nums))
    for i, v := range nums {
        res[i] = fn(v)
    }
    return res
}

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

Funções de ordem superior recebem ou devolvem outras funções. Passar uma função como parâmetro permite generalizar comportamento, como transformar cada elemento de um slice.

Structs e Interfaces


10 cards
Struct
type Pessoa struct {
    Nome  string
    Idade int
    Ativo bool
}

// instanciar
p := Pessoa{
    Nome:  "Ana",
    Idade: 30,
    Ativo: true,
}

fmt.Println(p.Nome) // Ana

Uma struct agrupa campos de tipos diferentes num único tipo. Cria-se com type e instancia-se indicando os campos por nome. É a base para modelar dados em Go.

Construtores
type Utilizador struct {
    Nome  string
    Email string
}

// convenção: New + nome do tipo
func NewUtilizador(nome, email string) *Utilizador {
    return &Utilizador{
        Nome:  nome,
        Email: email,
    }
}

u := NewUtilizador("Ana", "ana@mail.com")

Go não tem construtores nativos; a convenção é uma função NewX() que devolve um ponteiro para a struct já inicializada. Permite validar dados e definir valores por defeito.

Type assertion
var i any = "olá"

// afirmar o tipo
s := i.(string)
fmt.Println(s)

// forma segura (comma-ok)
s, ok := i.(string)
if ok {
    fmt.Println("é string:", s)
}

n, ok := i.(int) // ok == false

Uma type assertion (i.(string)) extrai o valor concreto de uma interface. A forma segura com ok evita um panic se o tipo não corresponder.

Formas de instanciar
type Ponto struct{ X, Y int }

// com nomes de campo (recomendado)
p1 := Ponto{X: 1, Y: 2}

// por posição (ordem dos campos)
p2 := Ponto{1, 2}

// zero value
var p3 Ponto // {0, 0}

// com new (retorna ponteiro)
p4 := new(Ponto)
p4.X = 5

Podes instanciar uma struct com os nomes dos campos (mais claro) ou por posição. var p Ponto cria o zero value e new() devolve um ponteiro para uma struct zerada.

Interfaces
type Forma interface {
    Area() float64
    Perimetro() float64
}

type Quadrado struct{ Lado float64 }

func (q Quadrado) Area() float64 {
    return q.Lado * q.Lado
}
func (q Quadrado) Perimetro() float64 {
    return 4 * q.Lado
}
// Quadrado implementa Forma implicitamente

Uma interface define um conjunto de métodos. Em Go, a implementação é implícita: qualquer type com esses métodos satisfaz a interface, sem declaração explícita. Isto favorece o desacoplamento.

Type switch
func descrever(i any) {
    switch v := i.(type) {
    case int:
        fmt.Println("inteiro:", v)
    case string:
        fmt.Println("texto:", v)
    case bool:
        fmt.Println("bool:", v)
    case []int:
        fmt.Println("slice de int")
    default:
        fmt.Printf("outro: %T\n", v)
    }
}

Um type switch (i.(type)) testa o tipo concreto de uma interface em vários case. A variável v assume o tipo correto em cada ramo. %T mostra o tipo.

Métodos (receiver por valor)
type Circulo struct {
    Raio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

c := Circulo{Raio: 5}
fmt.Println(c.Area())

Um método é uma função com um receiver, declarado entre func e o nome. Com receiver por valor (c Circulo), o método trabalha sobre uma cópia e não altera o original.

Interface vazia (any)
// any é alias de interface{}
func imprimir(v any) {
    fmt.Println(v)
}

imprimir(42)
imprimir("texto")
imprimir([]int{1, 2, 3})

var x any = "olá"

A interface vazia any (alias de interface{}) aceita qualquer tipo, pois não exige métodos. É útil para funções genéricas, mas perde segurança de tipos — usa com moderação.

Métodos (receiver por ponteiro)
type Contador struct {
    Valor int
}

func (c *Contador) Incrementar() {
    c.Valor++ // altera o original
}

ct := Contador{}
ct.Incrementar()
ct.Incrementar()
fmt.Println(ct.Valor) // 2

Com receiver por ponteiro (c *Contador), o método pode modificar o original e evita copiar structs grandes. Usa ponteiro sempre que precisares de alterar o estado.

Embedding (composição)
type Animal struct {
    Nome string
}

func (a Animal) Descrever() string {
    return "Sou " + a.Nome
}

type Cao struct {
    Animal // campo embutido
    Raca   string
}

c := Cao{Animal: Animal{Nome: "Rex"}, Raca: "Labrador"}
fmt.Println(c.Nome)        // promovido
fmt.Println(c.Descrever()) // método promovido

Go usa composição em vez de herança. Embutir um tipo (sem nome de campo) promove os seus campos e métodos para o tipo exterior. Cao ganha Nome e Descrever() de Animal.

Coleções


10 cards
Arrays
var arr [5]int          // [0,0,0,0,0]
arr[0] = 10

nums := [3]int{1, 2, 3}

// tamanho inferido
vals := [...]int{1, 2, 3, 4}

len(nums) // 3

// tamanho faz parte do tipo
var a [3]int
var b [4]int
// a = b // ERRO: tipos diferentes

Um array tem tamanho fixo que faz parte do tipo ([3]int é diferente de [4]int). É passado por cópia. Na prática, usa-se quase sempre slice, que é dinâmico.

make e copy
// make: cria slice com len e cap
s := make([]int, 5)     // len=5, cap=5
s = make([]int, 0, 10)  // len=0, cap=10

// copy: copia elementos
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // n=3

// map pré-dimensionado
m := make(map[string]int, 100)

make() cria slices, maps e channels com tamanho inicial, evitando realocações. copy(dst, src) copia elementos entre slices de forma segura, sem partilhar o array.

Ordenação (sort)
import "sort"

nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums) // [1,1,3,4,5]

sort.Strings([]string{"b", "a", "c"})

// ordenação customizada
pessoas := []Pessoa{{"Ana", 30}, {"Rui", 25}}
sort.Slice(pessoas, func(i, j int) bool {
    return pessoas[i].Idade < pessoas[j].Idade
})

sort.SearchInts(nums, 3) // pesquisa binária

O pacote sort ordena slices: Ints() e Strings() para tipos básicos, sort.Slice() com uma função de comparação para tipos custom. SearchInts() faz pesquisa binária.

Slices
s := []int{1, 2, 3}

// slice vazio
var vazio []int    // nil
outro := []int{}   // vazio, não nil

len(s) // 3 (comprimento)
cap(s) // capacidade

s[0] // 1
s[2] // 3

Um slice é uma vista dinâmica sobre um array, com comprimento e capacidade. É a coleção mais usada em Go. Declara-se com []tipo sem tamanho fixo.

Maps
m := map[string]int{
    "maçã":   3,
    "banana": 5,
}

// map vazio
vazio := make(map[string]int)

m["laranja"] = 2 // adicionar
m["maçã"] = 10   // atualizar

fmt.Println(m["banana"]) // 5
len(m)

Um map guarda pares chave-valor. Cria-se com map[chave]valor{} ou make(). Aceder e atribuir usa a sintaxe m[chave]. As chaves devem ser comparáveis.

Pacote slices (Go 1.21+)
import "slices"

s := []int{3, 1, 2}

slices.Sort(s)        // ordena
slices.Contains(s, 2) // true
slices.Index(s, 2)    // posição
slices.Reverse(s)     // inverte

slices.Min(s) // mínimo
slices.Max(s) // máximo

slices.Equal(a, b) // compara
slices.Clone(s)    // cópia independente

O pacote slices (Go 1.21+) traz funções genéricas modernas: Contains(), Sort(), Min(), Max() e Clone(). Clone() cria uma cópia que não partilha o array.

append
s := []int{1, 2, 3}

s = append(s, 4)            // [1,2,3,4]
s = append(s, 5, 6)         // vários
s = append(s, []int{7, 8}...) // outro slice

// importante: reatribuir!
s = append(s, 9)

append() adiciona elementos ao fim de um slice e devolve o novo slice — é preciso reatribuir (s = append(s, ...)). Se a capacidade chegar ao limite, o Go aloca um array maior automaticamente.

Maps: delete e comma-ok
m := map[string]int{"a": 1, "b": 2}

// comma-ok: a chave existe?
v, ok := m["a"]
if ok {
    fmt.Println("existe:", v)
}

// valor zero se não existir
v = m["z"] // 0 (sem erro!)

delete(m, "a") // remover

Aceder a uma chave inexistente devolve o zero value sem erro. Por isso usa-se o padrão comma-ok (v, ok := m[chave]) para saber se a chave existe. delete() remove uma entrada.

Fatiar (slicing)
s := []int{0, 1, 2, 3, 4, 5}

s[1:4]   // [1,2,3]
s[:3]    // [0,1,2]
s[3:]    // [3,4,5]
s[:]     // vista completa
s[1:4:4] // low:high:max (limita cap)

// partilha o array subjacente!
sub := s[1:3]
sub[0] = 99 // altera s também

A sintaxe s[low:high] cria um sub-slice. Atenção: o sub-slice partilha o array original, pelo que alterações afetam ambos. Usa copy() ou a forma de três índices para isolar.

Percorrer coleções
// slice
for i, v := range []int{10, 20, 30} {
    fmt.Println(i, v)
}

// map (ordem aleatória!)
for k, v := range m {
    fmt.Println(k, v)
}

// só chaves
for k := range m {
    fmt.Println(k)
}

range percorre slices (índice, valor) e maps (chave, valor). A ordem de iteração de um map é intencionalmente aleatória. Usa _ para ignorar o índice ou a chave.

Concorrência


10 cards
Goroutines
func tarefa(n int) {
    fmt.Println("tarefa", n)
}

func main() {
    go tarefa(1) // goroutine

    go func() {
        fmt.Println("assíncrono")
    }()

    time.Sleep(time.Second) // espera simples
}

Uma goroutine é uma função executada concorrentemente, lançada com go. São extremamente leves (poucos KB). O programa termina quando main() acaba, por isso é preciso sincronizar.

Fechar e percorrer channels
ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // fechar ao terminar
}()

// range termina quando o channel fecha
for v := range ch {
    fmt.Println(v)
}

// comma-ok: channel fechado?
v, ok := <-ch

close(ch) indica que não haverá mais envios. Um for range sobre o channel termina automaticamente quando fecha. O padrão v, ok := <-ch devolve ok == false se estiver fechado e vazio.

atomic e RWMutex
import "sync/atomic"

var total int64
atomic.AddInt64(&total, 1) // incremento atómico
v := atomic.LoadInt64(&total)

// RWMutex: várias leituras, 1 escrita
var rw sync.RWMutex
rw.RLock() // leitura
// ... ler dados ...
rw.RUnlock()
rw.Lock() // escrita
// ... escrever ...
rw.Unlock()

sync/atomic faz operações atómicas sem locks, ideal para contadores simples. sync.RWMutex permite várias leituras simultâneas (RLock()) mas apenas uma escrita (Lock()).

Channels
ch := make(chan string)

go func() {
    ch <- "olá" // envia (bloqueia)
}()

msg := <-ch // recebe (bloqueia)
fmt.Println(msg)

Um channel comunica entre goroutines de forma segura. ch <- valor envia e <-ch recebe. Um channel sem buffer bloqueia até haver um recetor e um emissor prontos.

select
select {
case msg := <-ch1:
    fmt.Println("ch1:", msg)
case msg := <-ch2:
    fmt.Println("ch2:", msg)
case <-time.After(time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("nenhum pronto")
}

select espera por várias operações de channel, executando o primeiro caso pronto. Se vários estiverem prontos, escolhe aleatoriamente. default evita o bloqueio e time.After() cria timeouts.

Context
ctx, cancel := context.WithTimeout(
    context.Background(),
    5*time.Second,
)
defer cancel()

select {
case res := <-fazer(ctx):
    fmt.Println(res)
case <-ctx.Done():
    fmt.Println("cancelado:", ctx.Err())
}

O pacote context controla cancelamento e timeouts entre goroutines. WithTimeout() cria um contexto que expira; ctx.Done() é um channel que fecha ao cancelar. Passa ctx como primeiro parâmetro.

Channel com buffer
ch := make(chan int, 3) // buffer de 3

ch <- 1 // não bloqueia
ch <- 2
ch <- 3
// ch <- 4 // BLOQUEIA (cheio)

fmt.Println(<-ch)         // 1 (FIFO)
fmt.Println(len(ch), cap(ch)) // 2 3

Um channel com buffer (make(chan int, 3)) aceita envios sem bloquear enquanto houver espaço. Só bloqueia quando está cheio (envio) ou vazio (receção).

WaitGroup
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}

wg.Wait() // bloqueia até todas terminarem
fmt.Println("feito")

sync.WaitGroup espera que um conjunto de goroutines termine. Add(1) regista cada uma, Done() (em defer) marca a conclusão e Wait() bloqueia até todas acabarem.

Channels direcionais
// só envia
func produtor(ch chan<- int) {
    ch <- 42
}

// só recebe
func consumidor(ch <-chan int) {
    fmt.Println(<-ch)
}

ch := make(chan int)
go produtor(ch)
consumidor(ch)

Podes restringir a direção de um channel no tipo: chan<- int só envia, <-chan int só recebe. Isto torna as APIs mais claras e seguras, indicando quem produz e quem consome.

Mutex
var (
    mu       sync.Mutex
    contador int
)

func incrementar() {
    mu.Lock()
    defer mu.Unlock()
    contador++
}

// 1000 goroutines seguras
for i := 0; i < 1000; i++ {
    go incrementar()
}

Um sync.Mutex protege dados partilhados: Lock() bloqueia o acesso e Unlock() liberta-o. Usa defer mu.Unlock() para garantir a libertação mesmo em caso de erro.

Tratamento de Erros


8 cards
Padrão error
f, err := os.Open("dados.txt")
if err != nil {
    return fmt.Errorf("abrir ficheiro: %w", err)
}
defer f.Close()

// usar f

Em Go, erros são valores. Funções que podem falhar devolvem um error como último retorno. O padrão idiomático é verificar if err != nil imediatamente e propagar o erro.

errors.Is
import (
    "errors"
    "os"
)

_, err := os.Open("inexistente.txt")

if errors.Is(err, os.ErrNotExist) {
    fmt.Println("ficheiro não existe")
}

// compara ao longo da cadeia de wrapping

errors.Is(err, alvo) verifica se um erro (ou qualquer erro envolvido na cadeia) corresponde a um valor específico, como os.ErrNotExist. Substitui a comparação direta, que não funciona com wrapping.

Criar erros
import (
    "errors"
    "fmt"
)

// erro simples
err := errors.New("algo falhou")

// erro formatado
err = fmt.Errorf("valor %d é inválido", n)

return err

errors.New() cria um erro com uma mensagem fixa. fmt.Errorf() cria erros formatados, como Sprintf. Ambos devolvem um valor que implementa a interface error.

errors.As
var nf *NotFoundError

if errors.As(err, &nf) {
    // nf agora tem o tipo concreto
    fmt.Println("faltou:", nf.Nome)
}

// extrai o primeiro erro do tipo indicado

errors.As(err, &alvo) procura na cadeia o primeiro erro de um tipo concreto e copia-o para alvo. Permite aceder a campos específicos de erros customizados mesmo após wrapping.

Wrapping com %w
func lerConfig() error {
    _, err := os.Open("config.yaml")
    if err != nil {
        return fmt.Errorf("ler config: %w", err)
    }
    return nil
}

// %w envolve o erro original,
// preservando a cadeia de causas

O verbo %w em fmt.Errorf() envolve o erro original, criando uma cadeia. Isto permite inspecionar a causa mais tarde com errors.Is() e errors.As(), sem perder contexto.

errors.Join (Go 1.20+)
import "errors"

err1 := validarNome(nome)
err2 := validarEmail(email)

// combinar vários erros
err := errors.Join(err1, err2)
if err != nil {
    return err // mostra ambos
}

errors.Is(err, err1) // true

errors.Join() (Go 1.20+) combina vários erros num só, útil para validar múltiplos campos. Os erros originais continuam detetáveis com errors.Is() e errors.As().

Erro customizado
type NotFoundError struct {
    Nome string
}

func (e *NotFoundError) Error() string {
    return e.Nome + " não encontrado"
}

// usar
return &NotFoundError{Nome: "utilizador"}

Qualquer tipo com um método Error() string implementa a interface error. Erros customizados podem transportar dados adicionais (como Nome), úteis para o chamador.

panic e recover
func seguro() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recuperado:", r)
        }
    }()

    panic("erro fatal")
}

// panic interrompe; recover só em defer

panic() interrompe o fluxo e propaga-se pela pilha; recover(), chamado dentro de um defer, captura-o e retoma o controlo. É último recurso — em Go, prefere devolver error.

Pacotes e Módulos


8 cards
go.mod
module github.com/utilizador/projeto

go 1.22

require (
    github.com/gorilla/mux v1.8.1
)

O ficheiro go.mod define o módulo: o caminho (nome) e a versão do Go. Cria-se com go mod init. As dependências ficam em require e as versões exatas em go.sum.

go get
# adicionar dependência
go get github.com/gorilla/mux

# versão específica
go get github.com/gorilla/mux@v1.8.1

# atualizar
go get -u ./...

# remover não usadas
go mod tidy

go get descarrega e adiciona dependências ao go.mod. Podes fixar uma versão com @v1.8.1 e atualizar tudo com -u. go mod tidy limpa as que já não são usadas.

import
import (
    "fmt"
    "os"
    "net/http"

    alias "outro/pacote" // renomear
    _ "driver/mysql"     // só side-effects
    . "math"             // dot import (evitar)
)

fmt.Println("olá")

import traz pacotes para o ficheiro. Podes dar um alias para evitar conflitos, usar _ para executar apenas a inicialização (drivers) ou . para importar sem prefixo (desaconselhado).

go mod
# criar módulo
go mod init github.com/user/proj

# limpar dependências
go mod tidy

# descarregar todas
go mod download

# verificar integridade
go mod verify

# ver o grafo de dependências
go mod graph

Os comandos go mod gerem o módulo: init cria o go.mod, tidy sincroniza as dependências com o código, download guarda-as na cache e verify confirma as somas.

Exportação (maiúscula)
package calculo

// Maiúscula = exportado (público)
func Somar(a, b int) int {
    return a + b
}

type Servidor struct {
    Host  string // campo público
    porta int    // campo privado
}

// minúscula = privado ao pacote
func interno() { }

Em Go, a visibilidade é por capitalização: nomes com maiúscula (Somar, Servidor) são exportados e acessíveis fora do pacote. Nomes com minúscula são privados ao pacote.

Organização de projeto
meuprojeto/
├── go.mod
├── go.sum
├── main.go          // package main
├── cmd/
│   └── server/
│       └── main.go  // outro binário
├── internal/        // privado ao módulo
│   └── handler/
└── pkg/             // reutilizável
    └── util/

Um projeto Go organiza-se por pacotes em pastas. cmd/ guarda os binários, internal/ código privado ao módulo (o compilador impede import externo) e pkg/ código reutilizável.

init()
package config

var baseURL string

func init() {
    // executado antes do main()
    baseURL = os.Getenv("API_URL")
    if baseURL == "" {
        baseURL = "http://localhost"
    }
}

A função init() corre automaticamente antes do main(), uma vez por pacote. Serve para inicializar estado, registar drivers ou validar configuração. Pode haver várias por ficheiro.

package main
package main

import "fmt"

// ponto de entrada obrigatório
func main() {
    fmt.Println("Olá, mundo!")
}

Um programa executável precisa de package main e de uma função main() sem parâmetros nem retorno. É o ponto de entrada. Pacotes de biblioteca usam outros nomes.

Dicas e CLI


9 cards
CLI: comandos essenciais
go run main.go    # compila e executa
go build          # gera binário
go install        # compila e instala
go test ./...     # corre testes
go fmt ./...      # formata código
go mod tidy       # limpa dependências
go clean          # limpa caches

O Go tem uma ferramenta única, go. run compila e executa, build gera o binário, test corre os testes e fmt formata o código no estilo padrão. ./... abrange todos os pacotes.

Interfaces úteis
// fmt.Stringer
type Ponto struct{ X, Y int }
func (p Ponto) String() string {
    return fmt.Sprintf("(%d,%d)", p.X, p.Y)
}

// io.Reader / io.Writer
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}

A biblioteca padrão define interfaces pequenas e ubíquas. fmt.Stringer (String()) personaliza a impressão. io.Reader e io.Writer abstraem qualquer fonte ou destino de dados.

Boas práticas
// 1. Tratar sempre os erros
if err != nil {
    return err
}

// 2. Nomes curtos e claros
i, v := range s // não "index, value"

// 3. Composição em vez de herança

// 4. Receptor curto
func (s *Servidor) Start() { }

// 5. Context como 1.º parâmetro
func Get(ctx context.Context, id string) { }

Go valoriza simplicidade: trata sempre os erros, usa nomes curtos, prefere composição a herança e passa context.Context como primeiro parâmetro. Formata sempre com go fmt.

Testes (testing)
// soma_test.go
package calculo

import "testing"

func TestSomar(t *testing.T) {
    got := Somar(2, 3)
    want := 5
    if got != want {
        t.Errorf("Somar(2,3) = %d; quero %d", got, want)
    }
}

Testes ficam em ficheiros _test.go com funções TestX(t *testing.T). Corre-os com go test. t.Errorf() regista uma falha sem parar; t.Fatalf() pára o teste.

io.Reader na prática
import (
    "io"
    "os"
    "strings"
)

r := strings.NewReader("olá mundo")

// copiar de um Reader para um Writer
io.Copy(os.Stdout, r)

// ler tudo
data, _ := io.ReadAll(r)

// ficheiro é um Reader
f, _ := os.Open("f.txt")
io.Copy(os.Stdout, f)

io.Reader é a abstração central de entrada. io.Copy() transfere dados de um Reader para um Writer de forma eficiente. Ficheiros, strings e respostas HTTP são todos Reader.

Table-driven tests
func TestSomar(t *testing.T) {
    testes := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    for _, tt := range testes {
        if got := Somar(tt.a, tt.b); got != tt.want {
            t.Errorf("Somar(%d,%d) = %d; quero %d",
                tt.a, tt.b, got, tt.want)
        }
    }
}

Testes orientados por tabela definem vários casos numa slice de structs e iteram sobre eles com range. É o padrão idiomático em Go: compacto, fácil de estender e com mensagens de erro claras.

JSON
type Pessoa struct {
    Nome  string `json:"nome"`
    Idade int    `json:"idade"`
}

// struct -> JSON
data, _ := json.Marshal(Pessoa{"Ana", 30})

// JSON -> struct
var p Pessoa
json.Unmarshal(data, &p)

// com formatação
json.MarshalIndent(p, "", "  ")

O pacote encoding/json serializa structs. Marshal() converte para JSON e Unmarshal() faz o inverso. As tags json:"nome" definem os nomes dos campos no JSON.

go vet e formatação
# análise estática
go vet ./...

# formatar
go fmt ./...

# formatar + organizar imports
goimports -w .

# linter completo (externo)
golangci-lint run

go vet deteta erros suspeitos que compilam mas estão errados. go fmt formata o código no estilo oficial. goimports também organiza os imports automaticamente.

Generics (Go 1.18+)
// type parameter
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Max(3, 7)     // 7
Max("a", "b") // "b"

// slice genérico
func Map[T, U any](s []T, f func(T) U) []U {
    res := make([]U, len(s))
    for i, v := range s {
        res[i] = f(v)
    }
    return res
}

Generics permitem funções e tipos parametrizados por tipo, com [T ...]. A restrição constraints.Ordered limita T a tipos ordenáveis. Reduz código duplicado mantendo segurança de tipos.