Executando verificação de segurança...
18

Go vs C# – Ponteiros, Alocação de Memória, Arrays, Slices e Garbage Collector

Recentemente quis me desafiar e aprender uma nova linguagem de programação e por isso comecei a estudar Go com o objetivo de futuramente criar alguns projetos pessoais utilizando essa linguagem. Como trabalho há mais de 3 anos com C#, resolvi documentar os aprendizados em forma de comparativos, tanto para consolidar o conteúdo quanto para ajudar quem estiver passando pela mesma transição.

Hoje quero falar sobre um tema essencial em qualquer linguagem: memória. Bora lá?


🔹 Ponteiros

Não dá pra falar de memória sem tocar no assunto ponteiros — e aqui está uma das diferenças mais marcantes entre Go e C#.

No C#, lidamos com referências de forma implícita na maior parte do tempo, principalmente quando trabalhamos com objetos no heap. O uso explícito de ponteiros é raro, restrito a blocos unsafe e exige permissões específicas do compilador. Justamente por isso, muitos devs .NET passam a carreira inteira sem precisar usar ponteiros diretamente.

Já no Go, os ponteiros fazem parte do dia a dia de forma natural e segura. Eles são utilizados para otimizar o uso de memória e evitar cópias desnecessárias de valores — especialmente quando lidamos com funções que precisam alterar dados fora do seu escopo.

O mais interessante é que, diferente de linguagens como C ou C++, o Go não permite aritmética de ponteiros. Isso evita uma série de bugs e torna o uso de ponteiros mais seguro e previsível.

Exemplo básico de ponteiro em Go:

package main

import "fmt"

func main() {
    x := 10
    p := &x         // p é um ponteiro para x
    fmt.Println(*p) // Imprime 10 (valor apontado por p)
}

🔹 Alocação de Memória

Quando falamos sobre gerenciamento de memória, é importante entender como cada linguagem lida com a criação e o ciclo de vida dos dados.

🧠 Em C#

No C#, usamos o operador new praticamente para tudo: instanciar classes, arrays, listas e até structs (quando queremos trabalhar com eles como referências).

var pessoa = new Pessoa();
int[] numeros = new int[5];

Essa abordagem é simples e direta. O .NET Framework se encarrega de alocar a memória no heap (quando necessário) e o garbage collector cuida de liberar espaço quando os objetos não são mais utilizados.

🧠 Em Go

O Go também tem formas bem objetivas de alocar memória, mas com uma separação interessante entre duas funções principais: new e make.

  • new(T) aloca memória para um valor do tipo T e retorna um ponteiro para esse valor. O valor sempre vem inicializado com o zero value do tipo (por exemplo, 0 para inteiros, "" para strings, etc.).
p := new(int)  // p é do tipo *int, apontando para 0
  • make(T, args...) é usado especificamente para tipos que possuem uma estrutura interna mais complexa: slices, maps e channels. Esses tipos precisam de mais do que apenas espaço — eles exigem inicialização interna (como buffers, tabelas de hash etc.).
nums := make([]int, 5) // Cria um slice com 5 posições

🔹 Arrays e Slices

Embora Go e C# tenham estruturas que armazenam sequências de valores, a forma como cada linguagem lida com arrays e estruturas dinâmicas é bastante diferente — e entender isso ajuda muito na transição entre elas.

📌 C#

No C#, temos principalmente duas estruturas para armazenar coleções indexadas:

  • int[] → Um array de tamanho fixo.
  • List<int> → Uma lista dinâmica, baseada em array internamente, mas com métodos prontos e capacidade de redimensionamento automático.
int[] numeros = new int[3] { 1, 2, 3 };
// Array fixo. Não podemos adicionar mais elementos depois disso.

List<int> lista = new List<int> { 1, 2, 3 };
lista.Add(4);
// Lista dinâmica. Pode crescer conforme necessário.

📌 Go

No Go, também temos arrays, mas eles são menos usados no dia a dia, porque têm comportamentos mais restritivos:

var a [3]int = [3]int{1, 2, 3}
// Isso é um array. Seu tamanho faz parte do tipo.

Um array [3]int é diferente de um [4]int — mesmo que os dois armazenem inteiros, eles são tipos distintos.

Por isso, Go introduz uma estrutura muito mais comum: o slice.

slice := []int{1, 2, 3}
slice = append(slice, 4)

Os slices são "fatias" de arrays, com tamanho e capacidade dinâmicos. Internamente, um slice contém:

  • Um ponteiro para um array subjacente,
  • Um tamanho (len),
  • E uma capacidade (cap).
s := []int{1, 2, 3}
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 3 (pode variar)
s = append(s, 4)
fmt.Println(len(s)) // 4
fmt.Println(cap(s)) // Capacidade pode ter dobrado, dependendo do runtime

Alterações em um slice podem afetar outros slices que vieram do mesmo array base.


🔹 Garbage Collector

Tanto Go quanto C# contam com coleta automática de lixo (Garbage Collector), o que facilita muito a vida do dev no dia a dia. Mas cada linguagem adota estratégias diferentes:

  • O GC do C# é generacional, ou seja, ele categoriza objetos em "gerações" (0, 1 e 2) com base no tempo de vida, o que melhora a performance em aplicações com muitos objetos temporários. Ele é bastante eficiente e ajustado para workloads empresariais pesados.

  • Já o GC do Go é concorrente, incremental e com pausas mínimas. Ele foi pensado para trabalhar ao lado da sua aplicação, sem travar tudo por longos períodos. Isso o torna muito eficiente em servidores web, APIs, e aplicações com alta concorrência e baixa latência.


🔹 O que podemos tirar desse comparativo?

No fim das contas, entender como cada linguagem lida com ponteiros, alocação de memória e garbage collector nos ajuda a fazer escolhas melhores no dia a dia de desenvolvimento.

Se você está desenvolvendo uma API de alta performance, que precisa lidar com milhares de requisições simultâneas e exige um consumo de memória mais previsível, o Go se destaca. Ele é enxuto, rápido, com um garbage collector pensado pra latência baixa e concorrência nativa com goroutines. Ideal pra microserviços, CLIs ou sistemas com footprint leve.

Por outro lado, se você está trabalhando em um sistema complexo, com muita lógica de negócio, integração com bancos de dados robustos, ORM, segurança, background jobs e um ecossistema maduro, o C# brilha. A plataforma .NET oferece ferramentas poderosas, uma base de código bem estruturada, suporte a LINQ, Entity Framework e tudo que ajuda a lidar com cenários mais empresariais.

Na prática:

  • Go brilha quando a prioridade é performance, simplicidade e concorrência.
  • C# é imbatível quando o foco é produtividade, robustez e integração com ferramentas corporativas.

Nenhuma linguagem é melhor que a outra — são apenas boas em coisas diferentes. O que importa é saber quando usar cada uma.


Carregando publicação patrocinada...
8

O ponteiro de Go na verdade é uma referência explícita, veja mais: https://pt.stackoverflow.com/q/56470/101. Não tem ponteiros reais. Go tem um jeito muito próprio de chamar as coisas. C# tem referência implícita ou ponteiro.

Em C# o new é usado até em tipos por valor para demonstrar que está chamando um construtor, e não é para criar o tipo por referência, isso é feito com ref. Eu considero isso um erro e perde uma das grandes vantagens do new explicito que tem em outras linguagens revelando que a alocação será no heap e portanto custa mais caro, mas em C# o new pode alocar na stack. Go acerta mais nessa.

"Ninguém" mais usa o .NET Framework, ele ainda tem suporte, mas está sem atualizações há vários anos.

O GC do .NET também é concorrente e tem pausas mínimas na maiorfia das situações. Dependedo da carga um pode se sair melhor que outro, algumas pessoas já passaram por grandes proglemas com o GC de Go, ele não é perfeito como algumas pessoas gostam de vender.

Não existe um cenário tão claro de uso de cada tecnologia e o programador pode fazer mais diferença do que a linguagem. Tem momento que uma linguagem passa em benchmarks mais rápido que outras, depois fica um pouco pior em classificação mesmo melhorando o desempenho (é com na Formula 1, você pode melhorar, mas se os outros melhorarma mais você fica pra trás).

C# pode tranquilamente ser usada em APIs de alta performance, não é automático e sempre que Go se dará melhor nisso. Go tende usar menos memória mesmo.

Para CLI geralmente qualquer coisa serve, até a linguagem mais lenta que existe.

Não acho que necessariamente C# tem mais produtividade, pode acontecer em alguns casos.

S2


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).

2

Fala maniero, muito obrigado pelo comentário!!

É exatamente esse tipo de troca que enriquece o aprendizado — principalmente quando a gente começa a explorar novas linguagens como é meu caso com Go.

Você trouxe ótimos pontos:

  • Realmente, o conceito de ponteiros em Go pode ser mais bem interpretado como referência explícita, já que não temos aritmética de ponteiros. Vou dar uma olhada nesse link do StackOverflow pra me aprofundar mais nesse detalhe técnico.

  • Sobre o new em C#, faz sentido o que você disse — ele não implica necessariamente alocação no heap, e sim a chamada de um construtor, inclusive para tipos por valor. Esse tipo de nuance é muito legal de entender melhor, e confesso que ainda não tinha parado pra olhar com essa profundidade.

  • E sim: o GC do .NET realmente evoluiu muito, inclusive com coleta concorrente. O que tentei trazer foi uma visão mais introdutória, mas essa contextualização de que nenhum GC é perfeito e que o cenário muda com carga e contexto é essencial.

  • A parte sobre “cenários ideais” de uso foi mais uma provocação didática do que uma verdade absoluta — e concordo plenamente com a analogia da Fórmula 1! 😄 No fim das contas, o dev faz a diferença com o que tem na mão.

Vou levar seus comentários como oportunidade de revisão e aprofundamento. Valeu demais por compartilhar sua visão com tanto conteúdo. Tamo junto 🙌