← Voltar para o blog

Go Fix: A Revolução na Modernização de Código com Inlining Inteligente

O Go 1.26 introduziu uma nova implementação do comando `go fix`, projetada para auxiliar na atualização e modernização do código Go. Uma das...

O Go 1.26 introduziu uma nova implementação do comando go fix, projetada para auxiliar na atualização e modernização do código Go. Uma das funcionalidades mais notáveis é o “source-level inliner”, que permite a autores de pacotes expressarem migrações e atualizações de API de forma simples e segura. Este resumo detalha o funcionamento do inliner, seus casos de uso e a tecnologia por trás dele.

O Que é Source-Level Inlining?

O inlining, em geral, é uma técnica de otimização de compiladores que substitui uma chamada de função pelo corpo da função chamada, substituindo os argumentos pelos parâmetros. O “source-level inlining” faz isso diretamente no código fonte, modificando-o de forma duradoura. Isso difere do inlining tradicional feito pelo compilador, que opera em representações intermediárias e é descartado após a compilação.

Se você já usou o recurso “Inline call” do gopls (o servidor de linguagem Go), você já utilizou o source-level inliner. Ele está disponível em editores como VS Code, através do menu “Source Action…”. O inliner é uma ferramenta fundamental para outras refatorações, como “Change signature” e “Remove unused parameter”, pois lida com sutilezas que surgem ao refatorar chamadas de função.

A grande novidade é que este mesmo inliner agora faz parte do go fix, permitindo migrações e upgrades de API “self-service” através do uso de comentários com a diretiva //go:fix inline.

Exemplos Práticos de Uso

Renomeando ioutil.ReadFile

Em Go 1.16, a função ioutil.ReadFile foi depreciada em favor de os.ReadFile. Embora a promessa de compatibilidade do Go impeça a remoção da função antiga, o inliner permite que os usuários migrem para a nova função.

Primeiro, a função antiga é anotada com //go:fix inline:

package ioutil

import "os"

// ReadFile reads the file named by filename…
// Deprecated: As of Go 1.16, this function simply calls [os.ReadFile].
//go:fix inline
func ReadFile(filename string) ([]byte, error) {
    return os.ReadFile(filename)
}

Ao executar go fix em um arquivo que contenha chamadas a ioutil.ReadFile, o comando substituirá a chamada pela equivalente em os.ReadFile:

$ go fix -diff ./...
-import "io/ioutil"
+import "os"

-   data, err := ioutil.ReadFile("hello.txt")
+   data, err := os.ReadFile("hello.txt")

A principal vantagem é que a transformação preserva o comportamento do programa (exceto em casos de introspecção da pilha de chamadas). Isso o torna mais seguro do que ferramentas de reescrita arbitrária como gofmt -r.

Grandes empresas como o Google utilizam ferramentas semelhantes para eliminar milhões de chamadas a funções depreciadas em bases de código massivas. O inliner do Go já foi utilizado para preparar milhares de changelists internos.

Corrigindo Falhas de Design de API

O inliner também pode ser usado para corrigir falhas de design em APIs. Considere o seguinte pacote hipotético oldmath:

// Package oldmath is the bad old math package.
package oldmath

// Sub returns x - y.
func Sub(y, x int) int

// Inf returns positive infinity.
func Inf() float64

// Neg returns -x.
func Neg(x int) int

Este pacote tem vários problemas: a ordem dos parâmetros em Sub está invertida, Inf implicitamente prefere um dos infinitos, e Neg é redundante. Para migrar os usuários para um novo pacote newmath que corrige esses problemas, o primeiro passo é implementar a API antiga em termos da nova e depreciar as funções antigas. Em seguida, adicionam-se as diretivas //go:fix inline:

// Package oldmath is the bad old math package.
package oldmath

import "newmath"

// Sub returns x - y.
// Deprecated: the parameter order is confusing.
//go:fix inline
func Sub(y, x int) int {
    return newmath.Sub(x, y)
}

// Inf returns positive infinity.
// Deprecated: there are two infinite values; be explicit.
//go:fix inline
func Inf() float64 {
    return newmath.Inf(+1)
}

// Neg returns -x.
// Deprecated: this function is unnecessary.
//go:fix inline
func Neg(x int) int {
    return newmath.Sub(0, x)
}

Agora, ao executar go fix, as chamadas às funções antigas serão substituídas pelas novas:

import "oldmath"

var nine = oldmath.Sub(1, 10) // diagnostic: "call to oldmath.Sub should be inlined"

será transformado em:

import "newmath"

var nine = newmath.Sub(10, 1)

Note que a ordem dos argumentos em Sub foi corrigida.

O inliner também funciona com tipos e constantes. Por exemplo:

package oldmath

//go:fix inline
type Rational = newmath.Rational

//go:fix inline
const Pi = newmath.Pi

Desafios e Considerações Técnicas

O inlining de código fonte não é uma tarefa trivial e apresenta vários desafios:

  • Escopo de Variáveis: O inliner deve garantir que as variáveis introduzidas pelo corpo da função inlinada não colidam com variáveis existentes no escopo da chamada. A solução envolve renomear variáveis, se necessário, para evitar conflitos.
  • Retornos Múltiplos: Funções com múltiplos retornos exigem cuidado para garantir que todos os valores de retorno sejam corretamente atribuídos após o inlining.
  • defer statements: Quando uma função contem defer statements, o inlining deve garantir que estes statements sejam executados no momento correto.
  • go statements (goroutines): Inlining de funções que iniciam goroutines pode ser problematico e requer tratamento especial.
  • recover: A semântica do recover pode ser afetada pelo inlining, e o inliner precisa estar ciente destas nuances.
  • goto statements: goto statements podem tornar o inlining complexo, especialmente se o alvo do goto estiver fora da função inlinada.
  • break e continue em loops: O inliner deve garantir que os statements break e continue continuem a funcionar corretamente após o inlining, o que pode exigir ajustes na estrutura do código.
  • return statements: O inliner deve garantir que os return statements na função inlinada retornem corretamente do contexto da chamada. Isso pode envolver a introdução de novos return statements ou a modificação dos existentes.

O artigo original não entra em detalhes sobre como cada um desses casos é tratado, mas implica que o inliner do Go possui mecanismos para lidar com essas complexidades de forma robusta.

Implementação e Algoritmo

O inliner do Go é implementado usando a biblioteca golang.org/x/tools/go/analysis. Ele opera em duas fases:

  1. Análise: A primeira fase analisa o código fonte para identificar chamadas de função que podem ser inlinadas. Ele também verifica se a função chamada está marcada com a diretiva //go:fix inline.
  2. Transformação: A segunda fase realiza a transformação do código, substituindo a chamada de função pelo corpo da função. Esta fase lida com os desafios mencionados acima, como escopo de variáveis, retornos múltiplos e defer statements.

O algoritmo é projetado para ser seguro e preservar o comportamento do programa. Ele evita transformações que possam introduzir erros ou alterar a semântica do código.

Implicações Práticas e Benefícios

O source-level inliner do Go oferece vários benefícios:

  • Modernização de Código: Facilita a migração de APIs antigas para novas, permitindo que os desenvolvedores atualizem seu código de forma incremental e segura.
  • Correção de Falhas de Design: Permite corrigir falhas de design em APIs, como a ordem incorreta de parâmetros ou a falta de clareza em constantes.
  • Automatização de Refatorações: Automatiza tarefas de refatoração que seriam tediosas e propensas a erros se feitas manualmente.
  • Melhora na Manutenibilidade: Simplifica o código, eliminando a necessidade de manter APIs antigas e corrigindo falhas de design.
  • Redução de Dependências: Pode permitir a remoção de dependências, uma vez que as chamadas a funções de um pacote podem ser inlineadas, eliminando a necessidade do pacote original.

Conclusão

O source-level inliner é uma adição poderosa ao ecossistema Go, oferecendo uma maneira segura e automatizada de modernizar o código, corrigir falhas de design e simplificar a manutenção. A combinação da diretiva //go:fix inline com o comando go fix permite que os autores de pacotes forneçam migrações de API “self-service”, reduzindo o esforço necessário para manter o código atualizado e em conformidade com as melhores práticas. Apesar dos desafios técnicos envolvidos, o inliner do Go parece lidar com as complexidades do código Go de forma robusta, oferecendo uma ferramenta valiosa para os desenvolvedores Go.


Artigo Original

Este e um resumo em português do artigo original publicado no blog oficial do Go.

Titulo original: //go:fix inline and the source-level inliner

Leia o artigo completo em ingles no Go Blog

Autor original: Alan Donovan