Como usar tipos customizados em Golang
Renan de Andrade

Renan de Andrade @renandotcorrea

About: An passionate software developer that likes do interesting things.

Location:
Maranhão, Brazil
Joined:
Mar 8, 2023

Como usar tipos customizados em Golang

Publish Date: Jun 19
1 0

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.

Comments 0 total

    Add comment