Tipos em Go: aliases, conversões, ponteiros e composição

Capítulo 6

Tempo estimado de leitura: 8 minutos

+ Exercício

Tipos definidos vs aliases

Em Go, o sistema de tipos é intencionalmente explícito. Isso ajuda a evitar conversões implícitas perigosas e torna o código mais previsível. Um ponto importante é diferenciar tipo definido (new defined type) de alias de tipo (type alias).

Tipo definido (novo tipo)

Um tipo definido cria um tipo novo e distinto, mesmo que tenha a mesma representação subjacente. Isso é útil para dar significado ao domínio e impedir misturas acidentais.

package main

import "fmt"

type UserID int

type OrderID int

func main() {
	var u UserID = 10
	var o OrderID = 10

	fmt.Println(u)
	fmt.Println(o)

	// u = o // ERRO: tipos diferentes
	u = UserID(o) // conversão explícita
	fmt.Println(u)
}

Note que UserID e OrderID não são intercambiáveis. Isso reduz bugs em modelagem de domínio.

Alias de tipo

Um alias não cria um tipo novo; ele apenas dá outro nome ao mesmo tipo. É comum para compatibilidade, refactors e APIs.

package main

import "fmt"

type ID = int // alias

func main() {
	var a ID = 1
	var b int = 2
	// Sem conversão: ID e int são o mesmo tipo
	fmt.Println(a + b)
}

Quando usar cada um

  • Tipo definido: quando você quer segurança e significado (ex.: Money, Email, UserID), e quer evitar misturar valores por engano.
  • Alias: quando você quer apenas renomear sem criar barreiras de tipo (ex.: migração de nomes, compatibilidade com versões).

Conversões explícitas: quando e por que são necessárias

Go exige conversões explícitas entre tipos diferentes, mesmo que sejam numéricos. Isso força você a pensar sobre possíveis perdas de informação e sobre a intenção do código.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Conversão entre numéricos

package main

import "fmt"

func main() {
	var a int = 10
	var b int64 = 20

	// fmt.Println(a + b) // ERRO: tipos diferentes
	fmt.Println(int64(a) + b)
}

Passo a passo prático:

  • Identifique os tipos envolvidos (ex.: int e int64).
  • Escolha um tipo alvo que faça sentido (ex.: promover para int64).
  • Converta explicitamente o valor menor/mais restrito para o tipo alvo.

Conversão entre tipos definidos e seus subjacentes

package main

import "fmt"

type Celsius float64

type Fahrenheit float64

func main() {
	var c Celsius = 25
	// var f Fahrenheit = c // ERRO: tipos definidos diferentes

	f := Fahrenheit(float64(c)*9.0/5.0 + 32)
	fmt.Println(f)
}

Aqui há duas ideias: (1) Celsius e Fahrenheit são tipos distintos; (2) conversões podem ser usadas junto com regras do domínio (a fórmula).

Conversão de string para []byte e vice-versa

Strings em Go são imutáveis; []byte é mutável. Converter cria uma cópia (na maioria dos casos), o que é importante para performance e segurança.

package main

import "fmt"

func main() {
	s := "go"
	b := []byte(s)
	b[0] = 'G'
	fmt.Println(s)        // "go" (não mudou)
	fmt.Println(string(b)) // "Go"
}

Ponteiros: semântica de valor, cópia e mutabilidade controlada

Em Go, variáveis normalmente são passadas por cópia. Ponteiros permitem que você compartilhe acesso ao mesmo dado, controlando mutabilidade e evitando cópias grandes quando necessário.

Semântica de valor: passagem por cópia

package main

import "fmt"

type Counter struct {
	N int
}

func IncByValue(c Counter) {
	c.N++
}

func main() {
	c := Counter{N: 1}
	IncByValue(c)
	fmt.Println(c.N) // 1 (não mudou)
}

O que aconteceu: IncByValue recebeu uma cópia de c. Alterar a cópia não altera o original.

Mutabilidade controlada com ponteiros

package main

import "fmt"

type Counter struct {
	N int
}

func IncByPointer(c *Counter) {
	c.N++ // equivalente a (*c).N++
}

func main() {
	c := Counter{N: 1}
	IncByPointer(&c)
	fmt.Println(c.N) // 2
}

Passo a passo prático:

  • Defina a função que precisa mutar o estado recebendo *T.
  • Ao chamar, passe o endereço com &variavel.
  • Dentro da função, acesse campos com ptr.Campo (Go faz a desreferenciação automaticamente em seletores).

nil: ponteiro sem apontar para nada

Um ponteiro pode ser nil. Isso é útil para representar ausência, mas exige checagem para evitar panic.

package main

import "fmt"

type Profile struct {
	Bio string
}

type User struct {
	Name    string
	Profile *Profile
}

func main() {
	u := User{Name: "Ana"}

	if u.Profile == nil {
		fmt.Println("sem perfil")
	}

	u.Profile = &Profile{Bio: "Gopher"}
	fmt.Println(u.Profile.Bio)
}

Ponteiro vs valor: regras práticas

  • Use valor quando o tipo é pequeno, imutável por design, ou quando você quer evitar efeitos colaterais.
  • Use ponteiro quando precisa mutar o receptor, quando copiar seria caro, ou quando nil representa “ausente”.
  • Evite ponteiros para tipos já “referência” como map, slice e chan na maioria dos casos; eles já carregam um cabeçalho que referencia dados internos.

Composição e embedding: alternativa à herança

Go não tem herança de classes. Em vez disso, você modela comportamento com composição (um tipo contém outro) e pode usar embedding (campo sem nome explícito) para promover métodos e campos.

Composição explícita (campo nomeado)

package main

import "fmt"

type Address struct {
	Street string
	City   string
}

type Customer struct {
	Name    string
	Address Address
}

func main() {
	c := Customer{Name: "João", Address: Address{Street: "Rua A", City: "SP"}}
	fmt.Println(c.Address.City)
}

Aqui fica claro que Customer tem um Address.

Embedding (campo promovido)

Com embedding, você escreve o tipo sem nome de campo. Os campos/métodos do tipo embutido podem ser acessados diretamente no tipo externo (promoção).

package main

import "fmt"

type Logger struct{}

func (Logger) Info(msg string) {
	fmt.Println("INFO:", msg)
}

type Service struct {
	Logger // embedding
	Name   string
}

func main() {
	s := Service{Name: "Billing"}
	s.Info("iniciando") // método promovido
}

Observação: embedding não é “é-um” (herança). É uma forma de reutilizar implementação e expor uma API mais conveniente.

Composição com interfaces (polimorfismo idiomático)

Em Go, polimorfismo costuma vir de interfaces pequenas. Você compõe tipos que implementam uma interface, sem precisar de hierarquia.

package main

import "fmt"

type Notifier interface {
	Notify(msg string)
}

type EmailNotifier struct {
	To string
}

func (e EmailNotifier) Notify(msg string) {
	fmt.Println("email para", e.To, ":", msg)
}

type OrderService struct {
	N Notifier
}

func (s OrderService) PlaceOrder() {
	s.N.Notify("pedido criado")
}

func main() {
	s := OrderService{N: EmailNotifier{To: "a@b.com"}}
	s.PlaceOrder()
}

Exercício: modelagem de domínio com composição e ponteiros (seguro com nil)

Objetivo: modelar um domínio simples de “Conta” com perfil opcional e auditoria embutida, usando tipos definidos para evitar misturas e ponteiros para opcionalidade/mutação controlada.

Requisitos do modelo

  • AccountID deve ser um tipo definido (não alias) para não confundir com outros IDs.
  • MoneyCents deve ser um tipo definido baseado em int64 para representar saldo em centavos.
  • Profile é opcional: uma conta pode não ter perfil (nil).
  • Use composição/embedding para auditoria (CreatedAt, UpdatedAt).
  • Implemente operações seguras: depositar, sacar (não permitir saldo negativo), atualizar bio do perfil criando o perfil se estiver ausente.

Passo a passo

1) Defina tipos do domínio (tipos definidos)

type AccountID int64

type MoneyCents int64

2) Crie structs compondo responsabilidades

package main

import (
	"errors"
	"time"
)

type AccountID int64

type MoneyCents int64

type Audit struct {
	CreatedAt time.Time
	UpdatedAt time.Time
}

type Profile struct {
	Bio string
}

type Account struct {
	Audit   // embedding (campos promovidos)
	ID      AccountID
	Balance MoneyCents
	Profile *Profile // opcional
}

3) Construtor para inicializar invariantes

func NewAccount(id AccountID) Account {
	now := time.Now()
	return Account{
		Audit: Audit{CreatedAt: now, UpdatedAt: now},
		ID:    id,
	}
}

4) Métodos com ponteiro para mutar estado

func (a *Account) touch() {
	a.UpdatedAt = time.Now()
}

func (a *Account) Deposit(amount MoneyCents) error {
	if amount <= 0 {
		return errors.New("amount deve ser positivo")
	}
	a.Balance += amount
	a.touch()
	return nil
}

func (a *Account) Withdraw(amount MoneyCents) error {
	if amount <= 0 {
		return errors.New("amount deve ser positivo")
	}
	if a.Balance < amount {
		return errors.New("saldo insuficiente")
	}
	a.Balance -= amount
	a.touch()
	return nil
}

5) Uso seguro de ponteiro opcional (nil) no perfil

func (a *Account) SetBio(bio string) {
	if a.Profile == nil {
		a.Profile = &Profile{}
	}
	a.Profile.Bio = bio
	a.touch()
}

6) Demonstração de uso e conversões explícitas

package main

import "fmt"

func main() {
	acc := NewAccount(AccountID(1001))

	_ = acc.Deposit(MoneyCents(1500))
	_ = acc.Withdraw(MoneyCents(400))
	acc.SetBio("Conta principal")

	fmt.Println("ID:", acc.ID)
	fmt.Println("Saldo (centavos):", acc.Balance)
	if acc.Profile != nil {
		fmt.Println("Bio:", acc.Profile.Bio)
	}
}

Tarefas do exercício

  • Adicione um tipo definido Percent (baseado em int ou float64) e crie um método ApplyBonus(p Percent) que aumenta o saldo. Exija conversões explícitas quando necessário.
  • Crie um método DisplayName() que retorne um nome amigável: se Profile for nil ou Bio vazia, retorne "Account #<ID>"; caso contrário, retorne a bio.
  • Crie um tipo FrozenAccount que componha Account e adicione um campo Frozen bool. Implemente Deposit/Withdraw que deleguem para Account apenas se não estiver congelada (sem herança; use composição e métodos).

Agora responda o exercício sobre o conteúdo:

Em Go, qual afirmação descreve corretamente a diferença entre um tipo definido e um alias de tipo?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Tipo definido cria um tipo novo (mesma representação subjacente, mas não intercambiável), forçando conversões explícitas. Alias apenas dá outro nome ao mesmo tipo, mantendo compatibilidade e permitindo uso direto sem conversão.

Próximo capitúlo

Structs em Go: modelagem, tags e boas práticas de inicialização

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
33%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.