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 tipoT
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.