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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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.:
inteint64). - 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
nilrepresenta “ausente”. - Evite ponteiros para tipos já “referência” como
map,sliceechanna 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
AccountIDdeve ser um tipo definido (não alias) para não confundir com outros IDs.MoneyCentsdeve ser um tipo definido baseado emint64para 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 int642) 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 emintoufloat64) e crie um métodoApplyBonus(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: seProfilefornilouBiovazia, retorne"Account #<ID>"; caso contrário, retorne a bio. - Crie um tipo
FrozenAccountque componhaAccounte adicione um campoFrozen bool. ImplementeDeposit/Withdrawque deleguem paraAccountapenas se não estiver congelada (sem herança; use composição e métodos).