Olá, mundo

Você pode encontrar todos os códigos para esse capítulo aqui

É comum o primeiro programa em uma nova linguagem ser um Olá, mundo.

  • Crie uma pasta onde quiser

  • Dentro da pasta, crie um arquivo chamado ola.go e coloque o seguinte código dentro dele

package main

import "fmt"

func main() {
    fmt.Println("Olá, mundo")
}

Para executá-lo, go run ola.go

Como isso funciona?

Quando você escreve um programa em Go, há um pacote main definido com uma função(func) main (principal) dentro dele. Os pacotes são maneiras de agrupar códigos Go relacionados.

A palavra reservada func é utilizada para que você defina uma função com um nome e um conteúdo.

Ao usar import "fmt", estamos importando um pacote que contém a função Println que será utilizada para imprimir (escrever) um valor na tela.

Como testar isso?

Como você testaria isso? É bom separar seu "domínio"(suas regras de negócio) do resto do mundo (efeitos colaterais). A função fmt.Println é um efeito colateral (que está imprimindo um valor no stdout [saída padrão do terminal]) e a string que estamos enviando para dentro dela é nosso domínio.

Então, vamos separar essas referências para ficar mais fácil para testarmos.

package main

import "fmt"

func Ola() string {
    return "Olá, mundo"
}

func main() {
    fmt.Println(Ola())
}

Criamos uma nova função usando func, mas dessa vez adicionamos outra palavra reservada string na sua definição. Isso significa que essa função terá como retorno uma string (cadeia de caracteres).

Agora, criaremos outro arquivo chamado ola_test.go onde iremos escrever um teste para a nossa função Ola.

package main

import "testing"

func TestOla(t *testing.T) {
	resultado := Ola()
	esperado := "Olá, mundo"

	if resultado != esperado {
		t.Errorf("resultado %q, esperado %q", resultado, esperado)
	}
}

Módulos Go?

A próxima etapa é executar os testes. Digite go test em seu terminal. Se os testes passarem, provavelmente você está usando uma versão anterior do Go. No entanto, se você estiver usando Go 1.16 ou posterior, os testes provavelmente não serão executados. Em vez disso, você verá uma mensagem de erro como esta no terminal:

Qual é o problema? Em uma palavra, módulos. Felizmente, o problema é fácil de resolver. Digite go mod init ola em seu terminal. Isso criará um novo arquivo com o seguinte conteúdo:

module ola

go 1.16 // a versão pode mudar dependendo da que você tem instalado na sua máquina

Este arquivo fornece às ferramentas go informações essenciais sobre o seu código. Se você planeja distribuir sua aplicação, você incluiria onde o código está disponível para download, bem como informações sobre dependências. Por enquanto, seu arquivo de módulo é mínimo e você pode deixá-lo assim. Para ler mais sobre os módulos, você pode verificar a referência na documentação do Golang. Podemos voltar a testar e aprender o Go agora, pois os testes devem ser executados, mesmo no Go 1.16.

Em capítulos futuros, você precisará executar go mod init ALGUM_NOME em cada nova pasta antes de executar comandos como go test ou go build.

De volta ao teste

Execute go test em seu terminal. Deve ter passado! Apenas para verificar, tente quebrar o teste, alterando a string esperado.

Observe como você não teve que escolher entre vários frameworks de teste e, em seguida, descobrir como instalar. Tudo o que você precisa está embutido na linguagem e a sintaxe é a mesma do resto do código que você escreverá.

Escrevendo testes

Escrever um teste é como escrever uma função, com algumas regras:

  • Precisa estar em um arquivo com um nome parecido com xxx_test.go

  • A função de teste precisa começar com a palavra Test

  • A função de teste recebe um único argumento, que é t *testing.T

  • Para usar o tipo *testing.T, você precisa importar "testing", como fizemos com fmt no outro arquivo

Por enquanto é o bastante para saber que o nosso t do tipo *testing.T é a nossa porta de entrada para a ferramenta de testes e assim você poderá utilizar o t.Fail() quando precisar relatar um erro.

Abordando alguns novos tópicos:

if

Instruções if em Go são muito parecidas com as de outras linguagens.

Declarando variáveis

Estamos declarando algumas variáveis com a sintaxe nomeDaVariavel := valor, que nos permite reutilizar alguns valores nos nossos testes de maneira legível.

t.Errorf

Estamos chamando o método Errorf em nosso t que irá imprimir uma mensagem e falhar o teste. O sufixo f no final de Errorf representa que podemos formatar e montar uma string com valores inseridos dentro de valores de preenchimentos %s. Quando fazemos um teste falhar, devemos ser bastante claros com o que aconteceu.

Iremos explorar a diferença entre métodos e funções depois.

Go doc

Outra funcionalidade importante do Go é sua documentação. Você pode ver a documentação na sua máquina rodando godoc -http :8000. Se acessar localhost:8000/pkg no seu navegador, verá todos os pacotes instalados no seu sistema.

A vasta biblioteca padrão da linguagem tem uma documentação excelente com exemplos. Deve valer a pena dar uma olhada em http://localhost:8000/pkg/testing/ para verificar o que está disponível para você.

Se você não tiver o comando godoc, talvez esteja usando uma versão mais recente do Go (1.14 ou posterior), que não inclui mais o godoc. Você pode instalá-lo manualmente com go install golang.org/x/tools/cmd/godoc.

nota: Se você tiver usando Go 1.17, ou superior, você deve instalar o godoc com go install golang.org/x/tools/cmd/godoc, pois o comando go get para instalar executáveis ficou obsoleto.

Olá, VOCÊ

Agora que temos um teste, podemos iterar sobre nosso software de maneira segura.

No último exemplo, escrevemos o teste somente depois do código ser escrito apenas para que você pudesse ter um exemplo de como escrever um teste e declarar uma função. A partir de agora, escreveremos os testes primeiro.

Nosso próximo requisito é nos deixar especificar quem recebe a saudação.

Vamos começar especificando esses requisitos em um teste. Estamos praticando TDD (Desenvolvimento Orientado a Testes) de forma bastante simples e que nos permite ter certeza que nosso teste está testando o que precisamos. Quando você escreve testes retroativamente existe o risco que seu teste possa continuar passando mesmo que o código não esteja funcionando como esperado.

package main

import "testing"

func TestOla(t *testing.T) {
	resultado := Ola("Chris")
	esperado := "Olá, Chris"

	if resultado != esperado {
		t.Errorf("resultado %q, esperado %q", resultado, esperado)
	}
}

Agora, rodando go test, deve ter aparecido um erro de compilação:

./ola_test.go:6:18: too many arguments in call to Ola
    have (string)
    want ()
./ola_test.go:6:18: argumentos demais na chamada para Ola
    tem (string)
    quer ()

Quando estiver usando uma linguagem estaticamente tipada como Go, é importante dar atenção ao compilador. O compilador entende como seu código deve se encaixar, não delegando essa função para você.

Neste caso, o compilador está te falando o que você precisa fazer para continuar. Temos que mudar a nossa função Ola para receber um argumento.

Edite a função Ola para que um argumento do tipo string seja aceito:

func Ola(nome string) string {
    return "Olá, mundo"
}

Se tentar rodar seus testes novamente, seu arquivo ola.go irá falhar durante a compilação porque você não está passando um argumento. Passe "mundo" como argumento para fazer o teste passar.

func main() {
    fmt.Println(Ola("mundo"))
}

Agora, quando for rodar seus testes, você verá algo parecido com isso:

ola_test.go:10: resultado 'Olá, mundo', esperado 'Olá, Chris'

Finalmente temos um programa que compila, mas que não está satisfazendo os requisitos de acordo com o teste.

Vamos, então, fazer o teste passar usando o argumento nome e concatenar com Olá,

func Ola(nome string) string {
    return "Olá, " + nome
}

Quando você rodar os testes, eles irão passar. É comum como parte do ciclo do TDD refatorar o nosso código agora.

Uma nota sobre versionamento de código

Nesse ponto, se você estiver usando um versionamento de código (que você deveria estar fazendo!) eu faria um commit do código no estado atual. Agora, temos um software funcional suportado por um teste.

No entanto, eu não faria um push para a branch principal, pois planejo refatorar em breve. É legal fazer um commit nesse ponto porque você pode se perder com a refatoração. Fazendo um commit você pode sempre voltar para a última versão funcional do seu software.

Não tem muita coisa para refatorar aqui, mas podemos introduzir outro recurso da linguagem: constantes.

Constantes

Constantes podem ser definidas como o exemplo abaixo:

const prefixoOlaPortugues = "Olá, "

Agora, podemos refatorar nosso código:

const prefixoOlaPortugues = "Olá, "

func Ola(nome string) string {
    return prefixoOlaPortugues + nome
}

Depois da refatoração, rode novamente os seus testes para ter certeza que você não quebrou nada.

Constantes devem melhorar a performance da nossa aplicação, assim como evitar que você crie uma string "Ola, " para cada vez que Ola é chamado.

Para esclarecer, o aumento de performance é incrivelmente insignificante para esse exemplo! Mas vale a pena pensar em criar constantes para capturar o significado dos valores e, às vezes, para ajudar no desempenho.

Olá, mundo... novamente

O próximo requisito é: quando nossa função for chamada com uma string vazia, ela precisa imprimir o valor padrão "Olá, mundo", ao invés de "Olá, ".

Começaremos escrevendo um novo teste que irá falhar

func TestOla(t *testing.T) {

    t.Run("diz olá para as pessoas", func(t *testing.T) {
        resultado := Ola("Chris")
        esperado := "Olá, Chris"

        if resultado != esperado {
            t.Errorf("resultado %q, esperado %q", resultado, esperado)
        }
    })

    t.Run("diz 'Olá, mundo' quando uma string vazia for passada", func(t *testing.T) {
        resultado := Ola("")
        esperado := "Olá, mundo"

        if resultado != esperado {
            t.Errorf("resultado %q, esperado %q", resultado, esperado)
        }
    })
}

Aqui nós estamos apresentando outra ferramenta em nosso arsenal de testes, os subtestes. Às vezes, é útil agrupar testes em torno de uma "coisa" e, em seguida, ter subtestes descrevendo diferentes cenários.

O benefício dessa abordagem é que você poderá construir um código que pode ser compartilhado por outros testes.

Há um código repetido quando verificamos se a mensagem é o que esperamos.

A refatoração não vale apenas para o código de produção!

É importante que seus testes sejam especificações claras do que o código precisa fazer.

Podemos e devemos refatorar nossos testes.

func TestOla(t *testing.T) {
    verificaMensagemCorreta := func(t testing.TB, resultado, esperado string) {
        t.Helper()
        if resultado != esperado {
            t.Errorf("resultado %q, esperado %q", resultado, esperado)
        }
    }

    t.Run("diz olá para as pessoas", func(t *testing.T) {
        resultado := Ola("Chris")
        esperado := "Olá, Chris"
        verificaMensagemCorreta(t, resultado, esperado)
    })

    t.Run("'Mundo' como padrão para string vazia", func(t *testing.T) {
        resultado := Ola("")
        esperado := "Olá, Mundo"
        verificaMensagemCorreta(t, resultado, esperado)
    })
}

O que fizemos aqui?

Refatoramos nossa asserção em uma função. Isso reduz a duplicação e melhora a legibilidade de nossos testes. No Go, você pode declarar funções dentro de outras funções e atribui-las a variáveis. Você pode chamá-las, assim como as funções normais. Precisamos passar t *testing.T como parâmetro para que possamos dizer ao código de teste que ele falhará quando necessário.

t.Helper() é necessário para dizermos ao conjunto de testes que este é um método auxiliar. Ao fazer isso, quando o teste falhar, o número da linha relatada estará em nossa chamada de função, e não dentro do nosso auxiliar de teste. Isso ajudará outros desenvolvedores a rastrear os problemas com maior facilidade. Se você ainda não entendeu, comente, faça um teste falhar e observe a saída do teste.

Agora que temos um teste bem escrito falhando, vamos corrigir o código usando um if.

const prefixoOlaPortugues = "Olá, "

func Ola(nome string) string {
    if nome == "" {
        nome = "Mundo"
    }
    return prefixoOlaPortugues + nome
}

Se executarmos nossos testes, veremos que ele satisfaz o novo requisito e não quebramos acidentalmente a outra funcionalidade.

De volta ao controle de versão

Agora estamos felizes com o código. Eu adicionaria mais um commit ao anterior para que possamos verificar o quão adorável ficou o nosso código com os testes.

Disciplina

Vamos repassar o ciclo novamente:

  • Escrever um teste

  • Compilar o código sem erros

  • Rodar o teste, ver o teste falhar e certificar que a mensagem de erro faz sentido

  • Escrever a quantidade mínima de código para o teste passar

  • Refatorar

Este ciclo pode parecer tedioso, mas se manter nesse ciclo de feedback é importante.

Ele não apenas garante que você tenha testes relevantes, como também ajuda a projetar um bom software refatorando-o com a segurança dos testes.

Ver a falha no teste é uma verificação importante porque também permite que você veja como é a mensagem de erro. Para quem programa, pode ser muito difícil trabalhar com uma base de código que, quando há falha nos testes, não dá uma ideia clara de qual é o problema.

Assegurando que seus testes sejam rápidos e configurando suas ferramentas para que a execução de testes seja simples, você pode entrar em um estado de fluxo ao escrever seu código.

Ao não escrever testes, você está comprometendo-se a verificar manualmente seu código executando o software que interrompe seu estado de fluxo, o que não economiza tempo, especialmente a longo prazo.

Continue! Mais requisitos

Caramba, temos mais requisitos. Agora precisamos suportar um segundo parâmetro, especificando o idioma da saudação. Se for passado um idioma que não reconhecemos, use como padrão o português.

Devemos ter certeza de que podemos usar o TDD para aprimorar essa funcionalidade facilmente!

Escreva um teste para um usuário, passando espanhol. Adicione-o ao conjunto de testes existente.

    t.Run("em espanhol", func(t *testing.T) {
        resultado := Ola("Elodie", "espanhol")
        esperado := "Hola, Elodie"
        verificaMensagemCorreta(t, resultado, esperado)
    })

Lembre-se de não trapacear! Primeiro os testes. Quando você tenta executar o teste, o compilador deve reclamar porque está chamando Ola com dois argumentos ao invés de um.

./ola_test.go:27:19: too many arguments in call to Ola
    have (string, string)
    want (string)

Acerte os problemas de compilação, adicionando um novo argumento do tipo string ao método Ola:

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }
    return prefixoOlaPortugues + nome
}

Quando você tentar executar o teste novamente, ele se queixará da função não ter recebido argumentos o suficiente para Ola nos seus outros testes em ola.go:

./ola.go:15:19: not enough arguments in call to Ola
    have (string)
    want (string, string)

Corrija-os passando strings vazia. Agora todos os seus testes devem compilar e passar, além do nosso novo cenário:

ola_test.go:29: resultado 'Olá, Elodie', esperado 'Hola, Elodie'

Podemos usar if aqui para verificar se o idioma é igual a "espanhol" e, em caso afirmativo, alterar a mensagem:

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }

    if idioma == "espanhol" {
        return "Hola, " + nome
    }

    return prefixoOlaPortugues + nome
}

Os testes devem passar agora.

Agora é hora de refatorar. Você verá alguns problemas no código, sequências de caracteres "mágicas", algumas das quais são repetidas. Tente refatorar você mesmo, a cada alteração, execute novamente os testes para garantir que sua refatoração não esteja quebrando nada.

const espanhol = "espanhol"
const prefixoOlaPortugues = "Olá, "
const prefixoOlaEspanhol = "Hola, "

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }

    if idioma == espanhol {
        return prefixoOlaEspanhol + nome
    }

    return prefixoOlaPortugues + nome
}

Francês

  • Escreva um teste que verifique que quando passamos o idioma "francês", obtemos "Bonjour, "

  • Veja o teste falhar, verifique se a mensagem de erro é fácil de ler

  • Faça a mínima alteração de código o suficiente para que o teste passe

Você pode ter escrito algo parecido com isso:

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }

    if idioma == espanhol {
        return prefixoOlaEspanhol + nome
    }

    if idioma == frances {
        return prefixoOlaFrances + nome
    }

    return prefixoOlaPortugues + nome
}

switch

Quando você tem muitas instruções if verificando um valor específico, é comum usar uma instrução switch. Podemos usar o switch para refatorar o código facilitando a leitura e a sua extensão, caso desejarmos adicionar suporte a mais idiomas posteriormente.

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }

    prefixo := prefixoOlaPortugues

    switch idioma {
    case frances:
        prefixo = prefixoOlaFrances
    case espanhol:
        prefixo = prefixoOlaEspanhol
    }

    return prefixo + nome
}

Faça um teste para incluir agora uma saudação no idioma de sua escolha e você deve ver como é simples estender nossa fantástica função.

Uma...última...refatoração?

Você pode achar que talvez nossa função esteja ficando um pouco grande. A refatoração mais simples para isso seria extrair algumas funcionalidades para outra função.

func Ola(nome string, idioma string) string {
    if nome == "" {
        nome = "Mundo"
    }

    return prefixodeSaudacao(idioma) + nome
}

func prefixodeSaudacao(idioma string) (prefixo string) {
    switch idioma {
    case frances:
        prefixo = prefixoOlaFrances
    case espanhol:
        prefixo = prefixoOlaEspanhol
    default:
        prefixo = prefixoOlaPortugues
    }
    return
}

Alguns novos conceitos:

  • Em nossa assinatura de função, criamos um valor de retorno chamado (prefixo string).

  • Isso criará uma variável chamada prefixo na nossa função.

    • Lhe será atribuído o valor "zero". Isso dependendo do tipo, por exemplo, para int será 0 e para strings será "".

      • Você pode retornar o que quer que esteja definido, apenas chamando return ao invés de return prefixo.

    • Isso será exibido no go doc para sua função, para que possa tornar a intenção do seu código mais clara.

  • default será escolhido caso o valor recebido não corresponda a nenhuma das outras instruções case do switch.

  • O nome da função começa com uma letra minúscula. As funções públicas em Go começam com uma letra maiúscula e as privadas, com minúsculas. Não queremos que as partes internas do nosso algoritmo sejam expostas ao mundo, portanto tornamos essa função privada.

Resumindo

Quem imaginaria que você poderia tirar tanto proveito de um Olá, mundo?

Até agora você deve ter alguma compreensão de:

Algumas das sintaxes da linguagem Go para:

  • Escrever testes

  • Declarar funções, com argumentos e tipos de retorno

  • if, const e switch

  • Declarar variáveis e constantes

O processo TDD e por que as etapas são importantes

  • Escreva um teste que falhe e veja-o falhar, para que saibamos que escrevemos um teste relevante para nossos requisitos e vimos que ele produz uma descrição da falha fácil de entender

  • Escrever a menor quantidade de código para fazer o teste passar, para que saibamos que temos um software funcionando

  • Em seguida, refatorar, tendo a segurança de nossos testes para garantir que tenhamos um código bem feito e fácil de trabalhar

No nosso caso, passamos de Ola() para Ola("nome"), para Ola ("nome"," Francês ") em etapas pequenas e fáceis de entender.

Naturalmente, isso é trivial comparado ao software do "mundo real", mas os princípios ainda permanecem. O TDD é uma habilidade que precisa de prática para se desenvolver. No entanto, você terá muito mais facilidade em escrever software sendo capaz de dividir os problemas em pedaços menores que possa testar.

Last updated