Bem-vindo à aula sobre reflexão (reflect) em Go. A reflexão é uma capacidade poderosa que permite a um programa inspecionar e manipular seus próprios tipos e valores em tempo de execução. Em linguagens como Python ou JavaScript, isso é algo natural e frequente; em Go, a reflexão existe, mas deve ser usada com cautela, pois possui custos de desempenho e pode tornar o código mais difícil de entender e manter.

Nesta aula, vamos mergulhar nos dois pilares do pacote reflect: reflect.Type e reflect.Value. Vamos entender como obtê-los, como usá-los para inspecionar structs, slices, interfaces e outros tipos, e quando é apropriado recorrer à reflexão em vez de soluções baseadas em tipos concretos ou interfaces. Também discutiremos os custos em termos de performance e segurança de tipo, e veremos exemplos práticos, como um validador genérico ou um conversor de struct para mapa.

reflect.Type e reflect.Value

O pacote reflect oferece duas entidades fundamentais: reflect.Type e reflect.Value. reflect.Type representa o tipo dinâmico de uma variável, ou seja, o tipo real que está armazenado em uma interface ou variável. Por exemplo, se você tem uma variável do tipo interface{} que contém um int, reflect.TypeOf retornará um reflect.Type que descreve o tipo int. Já reflect.Value representa o valor dinâmico em si, permitindo que você o inspecione, modifique (se for endereçável) ou chame métodos.

Para obter esses objetos, usamos as funções reflect.TypeOf() e reflect.ValueOf(). Ambas recebem um interface{} e retornam o tipo ou valor correspondente. Veja um exemplo básico:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x int = 42
    t := reflect.TypeOf(x)
    v := reflect.ValueOf(x)

    fmt.Println("Tipo:", t)        // int
    fmt.Println("Valor:", v)       // 42
    fmt.Println("Kind:", t.Kind()) // int
}

O método Kind() retorna uma constante do tipo reflect.Kind que indica a categoria do tipo (por exemplo, reflect.Int, reflect.Struct, reflect.Slice). Isso é útil para tomar decisões genéricas sem conhecer o tipo exato.

Quando usar

A reflexão deve ser usada com moderação, pois ela contorna o sistema de tipos estático do Go. Em geral, você deve considerar reflexão quando:

  • Você precisa lidar com valores de tipos desconhecidos em tempo de compilação, como em funções que recebem interface{} e precisam inspecionar a estrutura interna.
  • Você está implementando bibliotecas de serialização (como JSON, XML), ORMs, ou ferramentas de validação que operam em qualquer tipo de struct.
  • Você precisa comparar dois valores de tipos arbitrários, como em testes de igualdade profunda.
  • Você precisa construir ferramentas de depuração ou profiling que mostram informações de tipos em tempo de execução.

Por outro lado, evite reflexão quando:

  • Você conhece os tipos envolvidos e pode usar interfaces ou type switches.
  • Você está preocupado com desempenho crítico, pois a reflexão é significativamente mais lenta que operações diretas.
  • Você quer manter a segurança de tipo e a legibilidade do código.

Um exemplo clássico de uso é a função reflect.DeepEqual, que compara dois valores arbitrariamente complexos. Ela é usada em testes para verificar se duas estruturas são iguais em profundidade. Em vez de escrever comparações campo a campo, você pode usar reflect.DeepEqual.

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p1 := Person{"Alice", 30}
    p2 := Person{"Alice", 30}
    p3 := Person{"Bob", 25}

    fmt.Println(reflect.DeepEqual(p1, p2)) // true
    fmt.Println(reflect.DeepEqual(p1, p3)) // false
}

Custos

O uso de reflexão tem custos que devem ser considerados:

  • Desempenho: Operações de reflexão são muito mais lentas que operações diretas com tipos concretos. Isso ocorre porque o compilador não pode otimizar chamadas dinâmicas e há overhead de alocação e introspecção. Em benchmarks, a reflexão pode ser de 10 a 100 vezes mais lenta para certas operações.
  • Segurança de tipo: A reflexão contorna a verificação estática de tipos. Erros que seriam capturados em tempo de compilação (como acessar um campo inexistente) só aparecem em tempo de execução, geralmente na forma de pânico (panic). Isso pode levar a bugs difíceis de rastrear.
  • Legibilidade: Código que usa reflexão tende a ser mais genérico e abstrato, o que pode dificultar a compreensão. O leitor precisa entender o que está acontecendo em tempo de execução, e isso nem sempre é óbvio.
  • Manutenção: Alterações nos tipos (como renomear campos) podem não ser detectadas se a reflexão for usada com strings (por exemplo, FieldByName). Isso aumenta o risco de erros silenciosos.

Portanto, antes de usar reflexão, pergunte-se se existe uma solução mais simples e segura. Muitas vezes, interfaces e type switches resolvem o problema sem os custos da reflexão.

Exemplos

Vamos explorar alguns exemplos práticos de uso da reflexão.

Inspecionando Structs

Suponha que você queira criar uma função genérica que imprima os campos de qualquer struct. Com reflexão, você pode iterar sobre os campos e acessar seus valores e tags.

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Email string `json:"email,omitempty"`
}

func printFields(s interface{}) {
    t := reflect.TypeOf(s)
    v := reflect.ValueOf(s)

    if t.Kind() != reflect.Struct {
        fmt.Println("Esperava um struct")
        return
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        fmt.Printf("%s (%s) = %v\n", field.Name, field.Type, value.Interface())
    }
}

func main() {
    u := User{"Alice", 30, "alice@example.com"}
    printFields(u)
}

Esse exemplo mostra como usar NumField e Field(i) para percorrer os campos de um struct. Note que value.Interface() converte o valor de volta para um interface{}, permitindo imprimir com %v.

Modificando Valores via Reflexão

Para modificar um valor, você precisa que ele seja endereçável. Isso normalmente ocorre quando você passa um ponteiro para a função. Usamos reflect.ValueOf(s).Elem() para obter o valor apontado.

package main

import (
    "fmt"
    "reflect"
)

type Config struct {
    Host string
    Port int
}

func setDefaults(c interface{}) {
    v := reflect.ValueOf(c)
    if v.Kind() != reflect.Ptr {
        fmt.Println("Esperava um ponteiro")
        return
    }
    v = v.Elem()

    if v.Kind() != reflect.Struct {
        fmt.Println("Esperava um struct")
        return
    }

    hostField := v.FieldByName("Host")
    if hostField.IsValid() && hostField.CanSet() && hostField.Kind() == reflect.String {
        if hostField.String() == "" {
            hostField.SetString("localhost")
        }
    }

    portField := v.FieldByName("Port")
    if portField.IsValid() && portField.CanSet() && portField.Kind() == reflect.Int {
        if portField.Int() == 0 {
            portField.SetInt(8080)
        }
    }
}

func main() {
    cfg := Config{}
    setDefaults(&cfg)
    fmt.Printf("%+v\n", cfg) // {Host:localhost Port:8080}
}

Aqui usamos FieldByName para encontrar campos por nome e CanSet() para verificar se podemos modificar. O método SetString e SetInt modificam o valor.

Criando um Conversor Genérico de Struct para Mapa

Um caso comum é converter um struct em um mapa de strings para valores, útil para construir payloads dinâmicos.

package main

import (
    "fmt"
    "reflect"
)

type Product struct {
    ID    int
    Name  string
    Price float64
}

func structToMap(s interface{}) map[string]interface{} {
    result := make(map[string]interface{})
    v := reflect.ValueOf(s)
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    if v.Kind() != reflect.Struct {
        return result
    }

    t := v.Type()
    for i := 0; i < v.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        // Ignora campos não exportados
        if field.PkgPath != "" {
            continue
        }
        result[field.Name] = value.Interface()
    }
    return result
}

func main() {
    p := Product{1, "Laptop", 999.99}
    m := structToMap(p)
    fmt.Println(m) // map[ID:1 Name:Laptop Price:999.99]
}

Esse exemplo percorre os campos e usa field.PkgPath para detectar campos não exportados (que não devem ser acessados por reflexão de fora).

Boas Práticas e Observações Finais

Ao usar reflexão, siga estas boas práticas:

  • Verifique sempre os Kind() antes de operar, para evitar pânicos.
  • Use IsValid() para checar se um valor de campo é válido (por exemplo, quando usa FieldByName).
  • Respeite a regra de campos não exportados: eles não podem ser modificados via reflexão, e acessá-los pode causar pânico. Use CanSet() e CanInterface() para verificar.
  • Prefira soluções baseadas em interfaces e type switches sempre que possível, pois são mais eficientes e seguras.
  • Se a reflexão for inevitável, isole-a em funções pequenas e bem testadas.

Lembre-se: reflexão é uma ferramenta poderosa, mas com grandes poderes vêm grandes responsabilidades. Use com critério.

Referências

Exercícios

  1. Escreva uma função inspectType que recebe um interface{} e imprime o tipo, o kind e se é um ponteiro ou não. Teste com um int, um struct e um slice.

✓ Resposta:
func inspectType(i interface{}) {
    t := reflect.TypeOf(i)
    v := reflect.ValueOf(i)
    fmt.Printf("Tipo: %v, Kind: %v, É ponteiro? %v\n", t, t.Kind(), t.Kind() == reflect.Ptr)
    fmt.Printf("Valor: %v\n", v)
}

// Teste:
inspectType(42)
inspectType(struct{ Name string }{"Alice"})
inspectType([]int{1,2,3})
  • Crie uma função setIfZero que recebe um ponteiro para uma struct e um nome de campo (string). Se o campo existir e for uma string vazia, defina-o como "default". Use reflexão.
  • ✓ Resposta:
    func setIfZero(s interface{}, fieldName string) {
        v := reflect.ValueOf(s)
        if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
            return
        }
        v = v.Elem()
        f := v.FieldByName(fieldName)
        if f.IsValid() && f.CanSet() && f.Kind() == reflect.String && f.String() == "" {
            f.SetString("default")
        }
    }
    
  • Implemente uma função mapToStruct que preenche os campos de um struct a partir de um mapa map[string]interface{}. Considere apenas campos exportados e tipos compatíveis (string, int, float64, bool).
  • ✓ Resposta:
    func mapToStruct(m map[string]interface{}, s interface{}) {
        v := reflect.ValueOf(s)
        if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
            return
        }
        v = v.Elem()
        t := v.Type()
        for i := 0; i < t.NumField(); i++ {
            field := t.Field(i)
            if field.PkgPath != "" {
                continue
            }
            if val, ok := m[field.Name]; ok {
                f := v.Field(i)
                if f.CanSet() && f.Type() == reflect.TypeOf(val) {
                    f.Set(reflect.ValueOf(val))
                }
            }
        }
    }
    
  • Escreva uma função callMethod que recebe um valor e o nome de um método (string), e o invoca sem argumentos. Use reflexão para verificar se o método existe e chamá-lo. Trate o caso de método inexistente.
  • ✓ Resposta:
    func callMethod(s interface{}, methodName string) {
        v := reflect.ValueOf(s)
        m := v.MethodByName(methodName)
        if !m.IsValid() {
            fmt.Println("Método não encontrado")
            return
        }
        result := m.Call(nil)
        fmt.Println("Resultado:", result)
    }
    
  • Implemente uma função deepCopy que faz uma cópia profunda de um valor usando reflexão. Deve suportar tipos básicos (int, string, bool), slices, maps e structs. Dica: use reflect.New e reflect.Copy.
  • ✓ Resposta:
    func deepCopy(src interface{}) interface{} {
        v := reflect.ValueOf(src)
        if !v.IsValid() {
            return nil
        }
        switch v.Kind() {
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
             reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
             reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
            return v.Interface()
        case reflect.Slice:
            newSlice := reflect.MakeSlice(v.Type(), v.Len(), v.Cap())
            for i := 0; i < v.Len(); i++ {
                newSlice.Index(i).Set(reflect.ValueOf(deepCopy(v.Index(i).Interface())))
            }
            return newSlice.Interface()
        case reflect.Map:
            newMap := reflect.MakeMap(v.Type())
            for _, key := range v.MapKeys() {
                newMap.SetMapIndex(key, reflect.ValueOf(deepCopy(v.MapIndex(key).Interface())))
            }
            return newMap.Interface()
        case reflect.Struct:
            newStruct := reflect.New(v.Type()).Elem()
            for i := 0; i < v.NumField(); i++ {
                if v.Field(i).CanInterface() {
                    newStruct.Field(i).Set(reflect.ValueOf(deepCopy(v.Field(i).Interface())))
                }
            }
            return newStruct.Interface()
        default:
            return v.Interface()
        }
    }