[Go e a promessa de retrocompatibilidade]

Quando a versão 1.0 da linguagem Go foi lançada, os desenvolvedores assumiram um compromisso de manter retrocompatibilidade conforme novas versões fossem surgindo.

Esse compromisso traz vários resultados interessantes, por exemplo, o código que você aprender não fica desatualizado, além disso, manter a retrocompatibilidade da linguagem reforça outro compromisso importante, o que se refere a simplicidade e legibilidade do código. Aquele projeto que ficou esperando por meses, talvez anos continua compilando de primeira, eu experimentei isso pessoalmente com código que ficou na gaveta por três anos. 

Apesar dos esforços, nem sempre é possível manter compatibilidade absoluta por tantos anos, por exemplo, falhas de segurança às vezes precisam ser refletidas no código, e pequenas modificações podem ser inseridas. Para essas raras situações, existe o comando go fix. E mesmo assim ele é raramente usado, eu me lembro de ter usando o go fix apenas uma vez.

Por exemplo, o comando go fix subistitui a antiga buildtag +build pela nova go:build. No arquivo a linha // +build !windows é substituida por //go:build !windows.

Exemplo de uso

go fix ./...
luaengine/setwinsize_unix.go: fixed buildtag
luaengine/setwinsize_windows.go: fixed buildtag
term/resizeterm_unix.go: fixed buildtag
term/resizeterm_windows.go: fixed buildtag

Limitações

Alem das correções de segurança, a compatibilidade é de código, não binária, então apesar do código continuar compilando e se comportando como esperado, ele pode produzir binários diferentes.

Como incompatibilidades são descobertas

O time do Go faz algumas coisas interessantes para garantir que a compatibilidade com versões anteriores, primeiro eles mantêm uma lista com todas as funções exportadas pela biblioteca padrão. Quando eles testam uma nova versão eles comparam a lista antiga com a nova e com isso sabem se houve alguma quebra de compatibilidade.

Outra forma interessante de como a compatibilidade é validada são testes. Antes de uma nova versão ser lançada, ela é testada contra toda a base de código Go do Google, e eles assumem que se a compatibilidade quebrar no Google quebrará para os outros também.

Mantendo o código expansível.

Go toma cuidado para não quebrar a API com relação a estruturas e interfaces. Uma maneira interessante, por exemplo, é usar parâmetros nomeados sempre que for popular uma struct assim se você adicionar novos campos o código antigo que for populada com campos nomeados continuará compilando.

Por exemplo, considere a seguinte estrutura:

type valores struct {
	a int
	b int
}

O código abaixo compilará normalmente.

val1 := valores{a: 1, b: 2}
val2 := valores{1, 2}

Se no futuro você decidir que precisa de mais um campo e alterar a struct como abaixo:

type valores struct {
	a int
	b int
	c int
}

A linha de val1 continuará compilando e funcionando como esperado, mas val2 não vai mais compilar.

Como Go inicializa automaticamente os valores, campos não populados tem um valor previsível, mesmo ponteiros terão valor nil e será fácil tratar eles no código.

Fork o Go (ou partes)

Em último caso, uma alternativa para estender a longevidade do seu programa é fazer um fork de partes que por algum motivo perderam a compatibilidade com a versão que você está usando.

Essa é uma opção um tanto radical, mas às vezes necessária ainda mais se você estiver usando bibliotecas de terceiros. O problema com isso é que seu código continua compilando, mas você não receberá atualizações de segurança e melhorias, precisará assumir a responsabilidade de manter esse novo trecho você mesmo.

Referemcias


Cesar Gimenes