DEV Community

Cover image for Como usar tipos customizados em Golang
Renan de Andrade
Renan de Andrade

Posted on

Como usar tipos customizados em Golang

Introdução

Uma das coisas mais comuns em códigos escritos na linguagem Go é o uso de tipos customizados utilizando structs. Geralmente usamos estes tipos para declarar entidades, transportar valores de forma estruturada e etc. Por exemplo, O código acima é muito comum em muitas aplicações:

type Person struct {
  Name string
  Document string
  Email string
}
Enter fullscreen mode Exit fullscreen mode

Outros tipos customizados

Mas assim como structs, podemos utilizar outros tipos primitivos da linguagem para criar tipos customizados, abrindo assim um leque de oportunidades. O processo de criação é idêntico ao mostrado anteriormente, mas usando outro tipo primitivo como bases. Aqui vão alguns exemplos:

// Tipo customizado baseado em string
type MyCustomString string

// Baseado em int
type MyCustomInt int

// Baseado em float
type MyCustomFloat64 float64

// Baseado em boolean
type MyCustomBool bool

// Baseado em slice
type MyCustomSlice []string

// Baseado em... já deu para entender, né!?
Enter fullscreen mode Exit fullscreen mode

Mas como eu uso?

Para usar esses tipos novos é tão simples quanto você está pensa. Eles operam da mesma forma que seus tipos base, assim como a struct. Dá uma olhada:

func main() {
    // Declarando variáveis com tipos personalizados
    var str MyCustomString = "example"
    var num MyCustomInt = 42
    var flt MyCustomFloat64 = 3.14
    var boolean MyCustomBool = true
    var slice = MyCustomSlice{"example1", "example2"}

    // Operando com as variáveis
    str += " string"
    num += 10
    flt *= 2.0
    boolean = !boolean
    slice = append(slice, "example3")

    // Exibindo os valores e tipos das variáveis
    log.Printf("Value of str: %s, type: %T", str, str)             //Value of str: example string, type: main.MyCustomString
    log.Printf("Value of num: %d, type: %T", num, num)             //Value of num: 52, type: main.MyCustomInt
    log.Printf("Value of flt: %.2f, type: %T", flt, flt)           //Value of flt: 6.28, type: main.MyCustomFloat64
    log.Printf("Value of boolean: %t, type: %T", boolean, boolean) //Value of boolean: false, type: main.MyCustomBool
    log.Printf("Value of boolean: %s, type: %T", slice, slice)     //Value of boolean: [example1 example2 example3], type: main.MyCustomSlice
}
Enter fullscreen mode Exit fullscreen mode

Legal, né!?

Adicionando métodos

Uma possibilidade legal que esta abordagem nos traz é a capacidade de as variáveis criadas a partir destes tipo chamarem métodos customizados. Isso pode ter várias aplicações interessantes. Olha só:

type MyCustomInt int

func (my MyCustomInt) Positive() bool {
    return my > 0
}

func main() {
  var num MyCustomInt = 42
  log.Println("num Positive:", num.Positive()) // num Positive: true
}
Enter fullscreen mode Exit fullscreen mode

Olhando aquele nosso exemplo inicial da struct Person, podemos aplicar esses princípios para os campos Email e Document ao criar um novo tipo para cada um:

type Document string

func (d Document) Validate() bool {
    return len(d) > 0
}

type Email string

func (e Email) Validate() bool {
    if len(e) == 0 {
        return false
    }

    return strings.Contains(string(e), "@") && strings.Contains(string(e), ".")
}
Enter fullscreen mode Exit fullscreen mode

Assim cada tipo sabe como fazer sua própria validação. A Person fica assim então:

type Person struct {
    Name     string
    Document Document
    Email    Email
}
Enter fullscreen mode Exit fullscreen mode

Olha como o uso fica legal:

    person := Person{
        Name:     "John Doe",
        Document: "123456789",
        Email:    "johndoe@test.com",
    }

    log.Printf("Person: %+v", person)                                   // Person: {Name:John Doe Document:123456789 Email:johndoe@test.com}
    log.Printf("Person Document valid: %t", person.Document.Validate()) //Person Document valid: true
    log.Printf("Person Email valid: %t", person.Email.Validate()) //Person Email valid: true
Enter fullscreen mode Exit fullscreen mode

Dá pra usar interfaces

Para melhorar ainda mais nosso exemplo, podemos fazer a pergunta: E se eu quiser usar o mesmo campo Document para vários tipos de documento (digamos que RG e CPF)?

Podemos então mudar o tipo de Document para interface, e criar os tipos dos outros documentos que implementam esta nova interface. Melhor mostrando, né?

type Document interface {
    Validate() bool
}

type DocumentRG string
type DocumentCPF string

func (d DocumentRG) Validate() bool {
    log.Println("RG validation logic")
    return len(d) > 0
}

func (d DocumentCPF) Validate() bool {
    log.Println("CPF validation logic")
    return len(d) > 0
}
Enter fullscreen mode Exit fullscreen mode

Na Person nada muda, tá?!

Mas se liga aqui como fica o uso:

    person := Person{
        Name:     "John Doe",
        Document: DocumentCPF("123456789"),
        Email:    "johndoe@test.com",
    }

    person2 := Person{
        Name:     "Maike Doe",
        Document: DocumentRG("0987654321"),
        Email:    "maikedoe@test.com",
    }

    log.Printf("Person1: %+v", person)                                   // Person1: {Name:John Doe Document:123456789 Email:johndoe@test.com}
    log.Printf("Person1 Document valid: %t", person.Document.Validate()) // CPF validation logic // Person1 Document valid: true

    log.Printf("Person2: %+v", person2)                                   // Person2: {Name:Maike Doe Document:0987654321 Email:maikedoe@test.com}
    log.Printf("Person2 Document valid: %t", person2.Document.Validate()) //RG validation logic //Person2 Document valid: true
Enter fullscreen mode Exit fullscreen mode

Dessa forma, você pode ter vários tipos de documentos, e quem vai implementar é quem decide qual vai usar.

Utilizando enums

Para fecharmos, podemos fazer a pergunta: E se eu quiser ter um campo que indique o tipo de documento?

A gente pode usar Enums para isso. Espia:

type DocumentType uint

const (
    DocumentTypeRG DocumentType = iota
    DocumentTypeCPF
    DocumentTypeCNH
    DocumentTypePassaporte
)

func (d DocumentType) String() string {
    switch d {
    case DocumentTypeRG:
        return "RG"
    case DocumentTypeCPF:
        return "CPF"
    case DocumentTypeCNH:
        return "CNH"
    case DocumentTypePassaporte:
        return "Passaporte"
    default:
        return "Unknown"
    }
}
Enter fullscreen mode Exit fullscreen mode

Adicionamos então o campo DocumentType em Person:

type Person struct {
    Name         string
    Document     Document
    DocumentType DocumentType
    Email        Email
}
Enter fullscreen mode Exit fullscreen mode

E para usar também é bem simples:

    person := Person{
        Name:         "John Doe",
        Document:     DocumentCPF("123456789"),
        DocumentType: DocumentTypeCPF,
        Email:        "johndoe@test.com",
    }

    person2 := Person{
        Name:         "Maike Doe",
        Document:     DocumentRG("0987654321"),
        DocumentType: DocumentTypeRG,
        Email:        "maikedoe@test.com",
    }

    log.Printf("Person1: %+v", person)                 // Person1: {Name:John Doe Document:123456789 DocumentType:CPF Email:johndoe@test.com}
    log.Println("Document Type:", person.DocumentType) // Document Type: CPF

    log.Printf("Person2: %+v", person2)                 // Person2: {Name:Maike Doe Document:0987654321 DocumentType:RG Email:maikedoe@test.com}
    log.Println("Document Type:", person2.DocumentType) // Document Type: RG
Enter fullscreen mode Exit fullscreen mode

E voilà!

Vimos aqui então que podemos criar tipos customizados baseados em tipos primitivos, chamar métodos através deles, implementar interfaces e até utilizar Enums.

E aí, o que achou dessas dicas? Deixe aí nos comentários.

Obs. Cover image criada com IA.

Top comments (0)