Kodumaro :: Sobre Golang

Publicado em 12 de Junho, 2017
As sombras da programação.
Golang

Recentemente começei a trabalhar em um projeto usando a linguagem Go do Google. Minha intenção é expor aqui minhas impressões sobre a linguagem enquanto plataforma de desenvolvimento.

Abordagem

A primeira coisa que percebi é que abordei a linguagem inicialmente com uma visão equivocada. Olhei para Go do ponto de vista de Python e Node.js. O resultado foi uma total decepção. Quando você tenta usar uma linguagem que oferece ferramentas mais poderosas no uso de recursos do sistema com mentalidade de linguagens poderosas no sentido de facilitar o resultado é frustração.

A partir do momento que chaveei meu pensamento para C++ e Objective-C tive uma epifania sobre Go: de repente ela se tornou um linguagem interessante, até divertida de programar.

Inclusive um amigo meu de muito boa vontade tentou me explicar ponteiros com analogias e relações ro vs rw – bastava dizer “são ponteiros“.

A única crítica que tenho nesse ponto a Go é que o Google poderia ter tentado menos reinventar a roda e aproveitado mais a sintaxe de C++, já amplamente experimentada e readaptada, como Vala fez com C#. Teria atraído mais bons programadores devido a uma menor curva de aprendizado (apesar de Linus Torvalds discordar).

Interfaces

Interfaces se parecem parte com interfaces de Java e parte com interfaces de Objective-C – na verdade structs se parecem com interfaces de Objetive-C. Porém, ao contrário de Java, as interfaces de Go seguem a filosofia goose typing, que é um aprimoramento do duck typing.

Ou seja, se seu objeto implementa todos os métodos descritos pela interface, ele é de fato uma instância daquela interface.

Por exemplo:

type Person interface {
  FirstName() string
  LastName() string
  FullName() string
  Birth() time.Time
}

type personType struct {
  firstName, lastName string
  birth time.Time
}

func NewPerson(firstName, lastName string, birth time.Time) Person {
  return personType{firstName, lastName, birth}
}

func (p personType) FirstName() string {
  return p.firstName
}

func (p personType) LastName() string {
  return p.lastName
}

func (p personType) FullName() string {
  return strings.Trim(fmt.Sprintf("%v %v", p.firstName, p.lastName), " ")
}

func (p personType) Birth() time.Time {
  return p.birth
}

No exemplo acima, instâncias da struct personType são automaticamente instâncias de Person, uma vez que personType implemente todos os métodos da interface, podendo ser retornadas por NewPerson como instâncias reais de Person.

Testes unitários

A biblioteca padrão de Go oferece recursos simples, porém poderosíssimos para testes unitários. Trata-se da lib testing.

Por exemplo, para testarmos nossa struct (sendo bastante prolixo):

func TestPerson(t *testing.T) {
  timeForm := "2000-01-31"
  birth, _ := time.Parse(timeForm, "2017-06-12")
  p := NewPerson("John", "Doe", birth)

  t.Run("primary methods", func(t *testing.T) {
    t.Run("#FirstName", func(t *test.T) {
      if got := p.FirstName(); got != "John" {
        t.Fatalf("expected John, got %v", got)
      }
    })

    t.Run("#LastName", func(t *test.T) {
      if got := p.LastName(); got != "Doe" {
        t.Fatalf("expected Doe, got %v", got)
      }
    })

    t.Run("#Birth", func(t *test.T) {
      expected, _ := time.Parse(timeForm, "2017-02-12")
      if got := p.Birth(); got != expected {
        t.Fatalf("expected %v, got %v", expected, got)
      }
    })
  })

  t.Run("secondary methods", func(t *test.T) {
    t.Run("#FullName", func(t *test.T) {
      if got := p.FullName(); got != "John Doe" {
        t.Fatalf("expected John Doe, got %v", got)
      }
    })
  })
}

Mais do que suficiente para os testes unitários do dia-a-dia.

Tratamento de exceções

A forma padrão de tratar exceções em Go é retornando a exceção como última resposta.

Por exemplo, você deve ter reparado no _ na linha que cria a data de nascimento:

birth, _ := time.Parse(timeForm, "2017-06-12")

O _ diz para Go descartar o retorno daquela posição. Um código mais honesto seria:

birth, err := time.Parse(timeForm, "2017-06-12")
if err != nil {
  // Faça alguma coisa com err
  ...
}

O segundo valor retornado é o erro, caso o parsing da data tenha falhado, ou nil se tudo deu certo e o objeto time.Time pode ser criado corretamente.

Porém há casos em que a exceção não pode ser tratada (ou não se deseja que seja). Nesses casos é levantado um panic, que é uma exceção destrutiva que derruba a gorrotina atual e cada uma que esteja conectada a ela.

A forma de tratar um panic é através de um defer.

defer é um bloco de código (não uma função!) que é executado incondicionalmente na saída da função atual.

Dentro do bloco defer é possível chamar recover(), que para a propagação do panic. Se houver algum panic sendo propagado, sua exceção será retornada pelo recover(), caso não haja, será retornado nil.

Por exemplo:

func DoSomethingDangerous(res chan<- error) {
  defer res <- recover()

  // Faz algo que possa levar a um panic
  ...
}

Podem ser registrados quantos defers forem necessários.

Conclusão

Esta foi uma avaliação bem superficial da linguagem, não entrei em detalhes importantes, como gorrotinas, ponteiros, chans, sync.WaitGroups, slices, loops, etc. Fica para uma próxima.

Quem estiver interessado em aventurar-se no mundo do Go, recomendo o tour pela linguagem, o livro Introdution to Programming in Go, The Go Blog e Going Go Programming. Para experimentar a linguagem, há disponível The Go Playground.

Golang | TDD