Leonardo Gregianin
Posted on December 12, 2023
Tradução livre do artigo “Python is Easy. Go is Simple. Simple != Easy” escrito por Preslav Rachev no seu blog. O artigo original pode ser lido em: https://preslav.me/2023/11/27/python-is-easy-golang-is-simple-simple-is-not-easy/
Existe um equívoco comum de que simples e fácil se referem à mesma coisa. Afinal, se algo é fácil de usar, seu funcionamento interno deve ser simples de entender, certo? Ou vice-versa? Na verdade, é exatamente o oposto. Embora os dois conceitos apontem para o mesmo resultado, fazer algo parecer fácil requer uma enorme complexidade.
Tomemos como exemplo o Python, uma linguagem conhecida por sua baixa barreira de entrada e, portanto, uma escolha favorita para linguagem de programação de entrada. Escolas, universidades, centros de pesquisa e um grande número de empresas em todo o mundo escolheram Python precisamente por causa de sua acessibilidade a qualquer pessoa, independentemente do seu nível de educação ou formação acadêmica (ou total falta dela). Raramente é necessária muita teoria de tipos ou compreensão de como e onde as coisas são armazenadas na memória, em quais threads algum trecho de código está sendo executado, etc. Além disso, Python é a porta de entrada para algumas das mais profundas bibliotecas científicas e de nível de sistema. Ser capaz de controlar essa quantidade de poder com uma única linha de código fala muito a favor de que ela se torne uma das linguagens de programação mais populares do planeta.
E aí vem o problema: a facilidade de expressar coisas em código Python tem um custo. Por baixo dos panos, o interpretador Python é enorme e muitas operações devem ocorrer para que até mesmo uma única linha de código seja executada. Quando você ouve alguém se referindo ao Python como uma linguagem “lenta”, grande parte da “lentidão” percebida vem do número de decisões que o interpretador toma em tempo de execução. Mas esse nem é o maior problema, na minha opinião. A complexidade do ecossistema de tempo de execução do Python, juntamente com algumas decisões liberais de design em torno do gerenciamento de pacotes, cria um ambiente muito frágil, e as atualizações geralmente levam a incompatibilidades e travamentos de tempo de execução. Não é incomum deixar um aplicativo Python para voltar a ele depois de alguns meses, apenas para perceber que o ambiente mudou o suficiente para que não seja mais possível nem mesmo iniciar o aplicativo.
Claro, isso é uma simplificação grosseira, e até as crianças de hoje em dia sabem que existem contêineres para resolver problemas como esse. Na verdade, graças ao Docker e similares, é possível “congelar” as dependências de uma base de código Python no tempo para que ela possa praticamente rodar para sempre. No entanto, isso tem o custo de transferir a responsabilidade e a complexidade para a infraestrutura do sistema operacional. Não é o fim do mundo, mas também não é algo que deva ser subestimado e ignorado.
Da facilidade à simplicidade
Se resolvêssemos os problemas com Python, acabaríamos com algo como Rust — extremamente performático, mas com uma barreira de entrada notoriamente alta. A ferrugem, na minha opinião, não é fácil de usar e, o que é mais importante, não é simples. Embora esteja em alta hoje em dia, apesar de 20 anos de programação e de ter dado meus primeiros passos em C e C++, não posso olhar para um pedaço de código Rust e dizer com certeza que entendo o que está acontecendo lá.
Descobri Go há cerca de cinco anos, enquanto trabalhava em um sistema baseado em Python. Embora tenha levado algumas tentativas para gostar da sintaxe, imediatamente caí na ideia de simplicidade. O objetivo do Go é ser simples de entender por qualquer pessoa em uma organização — desde o desenvolvedor júnior recém-saído da escola até o gerente de engenharia de nível sênior que apenas ocasionalmente olha o código. Além do mais, sendo uma linguagem simples, Go raramente recebe atualizações de sintaxe — a última significativa foi a adição de genéricos na v1.18, o que ocorre somente após uma década de discussões sérias. Na maior parte, quer você observe o código Go escrito há cinco dias ou cinco anos, ele é basicamente o mesmo e deve funcionar.
A simplicidade requer disciplina. Pode parecer limitante e até um tanto retrógrado no início. Especialmente quando comparado a uma expressão sucinta, como uma lista ou compreensão de dicionário em Python:
temperatures = [
{"city": "City1", "temp": 19},
{"city": "City2", "temp": 22},
{"city": "City3", "temp": 21},
]
filtered_temps = {
entry["city"]: entry["temp"] for entry in temperatures if entry["temp"] > 20
}
O mesmo código em Go requer mais código, mas idealmente deve ser uma ideia mais próxima do que o interpretador Python está fazendo por baixo dos panos:
type CityTemperature struct {
City string
Temp float64
}
// ...
temperatures := []CityTemperature{
{"City1", 19},
{"City2", 22},
{"City3", 21},
}
filteredTemps := make(map[string]float64)
for _, ct := range temperatures {
if ct.Temp > 20 {
filteredTemps[ct.City] = ct.Temp
}
}
Embora você possa escrever código equivalente em Python, uma regra não escrita em programação diz que se a linguagem fornecer uma opção mais fácil (mais concisa, mais elegante), os programadores irão gravitar em torno dela. Mas fácil é subjetivo e simples deve ser igualmente aplicável a todos. A disponibilidade de alternativas para executar a mesma ação leva a diferentes estilos de programação, e muitas vezes é possível encontrar vários estilos na mesma base de código.
Como Go é prolixo e “chato”, ele naturalmente marca outra caixa — o compilador Go tem muito menos trabalho a fazer ao compilar um executável. Compilar e executar um aplicativo Go costuma ser tão rápido, ou até mais rápido, do que carregar o interpretador Python ou a máquina virtual Java antes mesmo de executar o aplicativo real. Não é de surpreender que ser um executável nativo seja tão rápido quanto um executável pode ser. Não é tão rápido quanto seus equivalentes em C/C++ ou Rust, mas com uma fração da complexidade do código. Estou disposto a negligenciar esta pequena “desvantagem” do Go. Por último, mas não menos importante, os binários Go são vinculados estaticamente, o que significa que você pode construir um em qualquer lugar e executá-lo no host de destino — sem quaisquer tempos de execução ou dependências de biblioteca. Por uma questão de conveniência, ainda agrupamos nossos aplicativos Go em contêineres Docker. Ainda assim, eles são significativamente menores e têm uma fração do consumo de memória e CPU de seus equivalentes Python ou Java.
Como usamos Python e Go a nosso favor
A solução mais pragmática que encontramos em nosso trabalho é combinar os poderes da facilidade do Python e da simplicidade do Go. Para nós, Python é um ótimo playground de prototipagem. É onde nascem as ideias e onde as hipóteses científicas são aceitas e rejeitadas. Python é uma escolha natural para ciência de dados e aprendizado de máquina e, como lidamos com muitas dessas coisas, não faz sentido tentar reinventar a roda com outra coisa. Python também tem o Django, que atende ao seu lema de permitir o desenvolvimento rápido de aplicativos como poucas outras ferramentas (é claro, Ruby on Rails e Elixir’s Phoenix merecem uma menção digna de nota aqui).
Suponha que um projeto precise de um mínimo de gerenciamento de usuários e administração interna de dados (como a maioria dos nossos projetos). Nesse caso, começaríamos com um esqueleto do Django por causa do seu Admin integrado, o que é fantástico. Uma vez que a prova de conceito com Django começa a se assemelhar a um produto, identificamos o quanto dele pode ser reescrito em Go. Como o aplicativo Django já definiu a estrutura do banco de dados e a aparência dos modelos de dados, escrever o código Go que o complementa é bastante fácil. Depois de algumas iterações, chegamos a uma simbiose, onde os dois lados coexistem pacificamente no mesmo banco de dados e usam mensagens básicas para se comunicarem. Eventualmente, o “shell” do Django se torna um orquestrador — ele serve aos nossos propósitos de administração e aciona tarefas que são então gerenciadas pela sua contraparte Go. A parte Go atende a todo o resto, desde APIs e endpoints até a lógica de negócios e processamento de trabalhos em background.
É uma simbiose que tem funcionado bem até agora e espero que continue assim no futuro. Em um post futuro, descreverei mais alguns detalhes sobre a arquitetura em si.
Obrigado por ler!
Posted on December 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.