Pular para o conteúdo principal

victorstein.dev

100/100 Dias de Golang - Fim!

Table of Contents

# 100/100

Chegamos! Esse é o post 101, porque, como eu disse no dia 50, um blog de programação tinha que começar no índice 0.

No dia 0 a ideia era simples: “fazer um post todo dia e me forçar a estudar, nem que seja um pouco, de Golang todo dia”. E no dia 50 eu prometi que, no dia 100, faria de novo o índice de todos os posts agrupados por assunto. Promessa é dívida, então este post tem três partes: como foi a jornada, os números da série (calculados, claro, com um programa em Go) e o índice completo.

# Não foram 100 dias seguidos

Sendo honesto: “100 dias” virou mais um nome do que uma contagem. Olhando as datas dos posts, a série teve três fases bem claras:

  • Dias 0 a 83 (12 de março a 3 de junho): um post por dia, sem falhar. Foi a fase mais intensa: a base da linguagem, concorrência, testes, web, o projeto da previsão do tempo, o jogo em UDP, a TUI, o linter…
  • Dias 84 a 88 (junho e começo de julho): o ritmo diminuiu. Testcontainers, Fyne, POO, Goose e o bot do Discord saíram com alguns dias de intervalo entre eles.
  • Dias 89 a 100 (6 a 17 de dezembro): depois de cinco meses parado, voltei para terminar, de novo com um post por dia.

O resultado: 280 dias entre o primeiro e o último post. Não é o que estava no plano, mas a série terminou, e acho que isso vale mais do que a contagem perfeita.

# Do básico à produção

Relendo os posts em ordem, dá para ver a série mudando de assunto conforme o conhecimento foi aumentando.

Os fundamentos. Os primeiros posts são sobre a linguagem em si: tipos, slices, maps, structs, funções, interfaces, ponteiros, generics. Coisas que parecem simples, mas que têm detalhes importantes, como a diferença entre o tamanho e a capacidade de um slice, ou por que a ordem de um map muda a cada execução.

Concorrência e testes. Depois vieram as goroutines, os channels, o select e os mutexes, que são o motivo de muita gente escolher Go. E uma sequência inteira de testes: testes em tabela, benchmark, fuzzing, Testify, mocks, Testcontainers. O Go já vem com quase tudo pronto para testar, e isso faz diferença.

Construindo coisas. A partir do dia 40, a série ficou mais prática: CLI com Cobra, servidores web com a biblioteca padrão e com Gin, uma aplicação completa de previsão do tempo, um chat em TCP, um jogo multiplayer em UDP, um jogo no terminal com Bubble Tea, um linter que analisa código Go, um mini servidor de CI/CD, gráficos, interfaces gráficas e um bot para o Discord.

O ecossistema. No meio disso, várias bibliotecas e ferramentas: Zap, GORM, Colly, Faker, lo, go-echarts, Air, Docker…

Indo para produção. Os últimos posts fecharam com o que separa um programa que funciona de um programa pronto para produção: context, graceful shutdown, padrões de concorrência, profiling com pprof, como funciona o garbage collector, logs estruturados, métricas e tracing, SQL sem ORM, autenticação, gRPC, iterators e embed.

Um assunto foi aparecendo em quase todos os posts do final: o context.Context. O graceful shutdown depende dele, os logs pegam o request ID dele, os spans do OpenTelemetry passam por ele, as queries do banco são canceladas por ele e o gRPC envia o deadline através dele. Se eu tivesse que escolher um único conceito para recomendar estudar com calma depois da base, seria esse.

# Os números

No dia 50 eu criei um programa para gerar o índice, porque copiar 50 títulos à mão não dava. Agora, com 101 posts, é a vez de uma versão melhorada, usando várias coisas que aprendi no caminho.

package main

import (
	"bufio"
	"fmt"
	"io/fs"
	"iter"
	"log"
	"os"
	"path"
	"regexp"
	"slices"
	"strings"
	"time"
)

type Post struct {
	Arquivo    string
	Titulo     string
	Data       time.Time
	Rascunho   bool
	Palavras   int
	BlocosGo   int
	LinhasDeGo int
}

var nomeDoPost = regexp.MustCompile(`^\d{3}-golang\.pt\.md$`)

// Posts percorre a pasta e entrega cada post da série já processado
func Posts(fsys fs.FS) iter.Seq2[Post, error] {
	return func(yield func(Post, error) bool) {
		err := fs.WalkDir(fsys, ".", func(caminho string, d fs.DirEntry, err error) error {
			if err != nil {
				return err
			}
			if d.IsDir() || !nomeDoPost.MatchString(d.Name()) {
				return nil
			}

			post, err := lerPost(fsys, caminho)
			if !yield(post, err) {
				return fs.SkipAll
			}
			return nil
		})
		if err != nil {
			yield(Post{}, err)
		}
	}
}

func lerPost(fsys fs.FS, caminho string) (Post, error) {
	arquivo, err := fsys.Open(caminho)
	if err != nil {
		return Post{}, err
	}
	defer arquivo.Close()

	post := Post{Arquivo: path.Base(caminho)}
	scanner := bufio.NewScanner(arquivo)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

	separadores := 0
	dentroDoCodigo, codigoGo := false, false

	for scanner.Scan() {
		linha := scanner.Text()

		// front matter entre as duas linhas "---"
		if separadores < 2 {
			if linha == "---" {
				separadores++
				continue
			}
			chave, valor, _ := strings.Cut(linha, ":")
			valor = strings.Trim(strings.TrimSpace(valor), `'"`)
			switch strings.ToLower(chave) {
			case "title":
				post.Titulo = valor
			case "date":
				post.Data, _ = time.Parse(time.DateOnly, valor)
			case "draft":
				post.Rascunho = strings.EqualFold(valor, "true")
			}
			continue
		}

		if strings.HasPrefix(linha, "```") {
			if !dentroDoCodigo {
				codigoGo = strings.TrimPrefix(linha, "```") == "go"
				if codigoGo {
					post.BlocosGo++
				}
			}
			dentroDoCodigo = !dentroDoCodigo
			continue
		}

		if dentroDoCodigo {
			if codigoGo && strings.TrimSpace(linha) != "" {
				post.LinhasDeGo++
			}
			continue
		}
		post.Palavras += len(strings.Fields(linha))
	}
	return post, scanner.Err()
}

func main() {
	if len(os.Args) < 2 {
		log.Fatal("uso: go run . <pasta dos posts>")
	}

	var posts []Post
	for post, err := range Posts(os.DirFS(os.Args[1])) {
		if err != nil {
			log.Fatal(err)
		}
		if !post.Rascunho {
			posts = append(posts, post)
		}
	}

	slices.SortFunc(posts, func(a, b Post) int { return strings.Compare(a.Arquivo, b.Arquivo) })

	var palavras, blocos, linhas int
	for _, p := range posts {
		palavras += p.Palavras
		blocos += p.BlocosGo
		linhas += p.LinhasDeGo
	}

	maior := slices.MaxFunc(posts, func(a, b Post) int { return a.Palavras - b.Palavras })
	maisCodigo := slices.MaxFunc(posts, func(a, b Post) int { return a.LinhasDeGo - b.LinhasDeGo })
	primeiro, ultimo := posts[0], posts[len(posts)-1]

	fmt.Printf("posts publicados:   %d\n", len(posts))
	fmt.Printf("primeiro post:      %s\n", primeiro.Data.Format(time.DateOnly))
	fmt.Printf("último post:        %s\n", ultimo.Data.Format(time.DateOnly))
	fmt.Printf("dias corridos:      %.0f\n", ultimo.Data.Sub(primeiro.Data).Hours()/24)
	fmt.Printf("palavras (texto):   %d\n", palavras)
	fmt.Printf("blocos de Go:       %d\n", blocos)
	fmt.Printf("linhas de Go:       %d\n", linhas)
	fmt.Printf("maior post:         %s (%d palavras)\n", maior.Titulo, maior.Palavras)
	fmt.Printf("mais código:        %s (%d linhas)\n", maisCodigo.Titulo, maisCodigo.LinhasDeGo)
	fmt.Println()

	for _, p := range posts {
		fmt.Printf("- [%s](./%s)\n", p.Titulo, p.Arquivo)
	}
}

Comparando com o programa do dia 50, que usava filepath.Walk e pegava só o título da segunda linha:

  • os.DirFS + fs.WalkDir: o programa lê de um fs.FS, a mesma interface do embed do dia 99. Num teste, dá para passar um fstest.MapFS com posts falsos em vez da pasta de verdade;
  • Um iterator (iter.Seq2[Post, error]) esconde a caminhada pelas pastas: o main só faz um for range, igual ao exemplo de paginação. E se o main parasse no meio, o fs.SkipAll interromperia a caminhada;
  • O front matter é lido de verdade, com strings.Cut, e os rascunhos (draft: true) ficam de fora. Tenho alguns rascunhos parados que não contam como post;
  • O bufio.Scanner com buffer maior: o buffer padrão é de 64KB por linha, e uma linha maior que isso faria o Scan parar com erro;
  • slices.SortFunc e slices.MaxFunc para ordenar e encontrar os maiores posts, em vez de loops manuais.

Rodando na pasta dos posts:

go run . content/posts/golang
posts publicados:   101
primeiro post:      2025-03-12
último post:        2025-12-17
dias corridos:      280
palavras (texto):   39945
blocos de Go:       457
linhas de Go:       6545
maior post:         100/100 Dias de Golang - Fim! (2205 palavras)
mais código:        99/100 Dias de Golang - Iterators e embed (314 linhas)

As “palavras” são as do texto, sem contar os blocos de código, e as “linhas de Go” contam só as linhas não vazias dentro de blocos marcados como Go. E sim, o maior post da série é este aqui, mas é trapaça: são os 101 links do índice lá embaixo.

# O que fica

Algumas coisas que levo da série:

  • A biblioteca padrão é enorme e boa. Servidor HTTP, JSON, testes, benchmarks, profiling, logs estruturados, templates, embed… Muitas vezes a primeira pergunta deveria ser “a biblioteca padrão já faz isso?”, e a resposta costuma ser sim;
  • Simplicidade dá trabalho. Go tem poucas funcionalidades de propósito. No começo isso parece limitação (cadê o map e o filter?), mas o código fica fácil de ler, inclusive o código dos outros;
  • Erros são valores. Tratar o err em cada chamada parece repetitivo, mas força a pensar no que fazer quando algo dá errado, em vez de deixar uma exceção subir sem ninguém ver;
  • Escrever ensina. Para explicar um assunto num post, eu precisava entender de verdade, rodar os exemplos e conferir as saídas. Muitas das coisas que achava que sabia, só aprendi direito na hora de escrever.

# E agora?

A série acabou, mas o blog não. Ficaram alguns rascunhos pelo caminho, sobre arquitetura limpa em Go e testes com WireMock, que podem virar posts. E tem um rascunho sobre Sliding Window que pode ser o começo de uma série sobre estruturas de dados e algoritmos, implementados em Go.

Obrigado a quem acompanhou, nem que seja um post. Até a próxima!

# Todos os posts

O índice completo, agrupado por assunto como no dia 50 (inspirado no roadmap.sh):

## Fundamentos

## Indo mais fundo

## Concorrência

## Testes

## Web e APIs

## Redes e tempo real

## Banco de dados

## Logging e observabilidade

## CLIs, TUIs e interfaces gráficas

## Deploy e produção

## Ecossistema e ferramentas

## Projetos

## Marcos

# Referências

  1. Documentação Go
  2. Roadmap Go - roadmap.sh
  3. Documentação io/fs
  4. Documentação iter