CodeGym /Cursos /Go SELF /Ordenando antes de exibir: ...

Ordenando antes de exibir: sort.Slice, slices.SortFunc, criterios

Go SELF
Nível 51, Lição 3
Disponível

1. Por que ordenar antes de exibir

Quando voce escreve um CLI, parece que "a ordem das linhas e um detalhe". Mas isso vale so ate o momento em que alguem comeca a fazer parse da sua saida com um script, comparar resultados com testes ou simplesmente tentar encontrar com os olhos a "tarefa numero 7". Se a ordem oscila, voce tem a sensacao de que o programa vive por conta propria.

A ordenacao antes de exibir resolve duas tarefas praticas:

  • torna o resultado deterministico: os mesmos dados de entrada geram a mesma saida;
  • torna a saida conveniente: tarefas podem ser encontradas, comparadas e agrupadas.

E o mais importante: voce deve ordenar os dados, e nao as strings da tabela/JSON — porque a formatacao deve ser a ultima etapa, e nao o lugar onde "resolvemos a logica de negocio em paralelo".

Para fixar a ideia, vamos imaginar nosso pipeline (bem simplificado):

flowchart LR
    A[Dados do storage] --> B[Preparacao da lista]
    B --> C[Ordenacao]
    C --> D[Render table/json]
    D --> E[stdout]

A ordenacao fica exatamente antes do render: ja decidimos o que mostrar e agora decidimos em que ordem.

O que exatamente estamos ordenando

Antes de ordenar, e util parar por um segundo e combinar: nao estamos ordenando a "saida", e sim um slice de structs. Isso e importante porque a struct e a unica fonte da verdade para todos os formatos (tanto tabela quanto JSON). Nas aulas anteriores, ja nos apoiamos nessa ideia: um unico []Task → dois renders.

O modelo minimo de uma tarefa para o nosso CLI de estudo (vamos chamar o aplicativo de tasker) fica assim:

package main

import "time"

type Task struct {
	ID        int
	Title     string
	Done      bool
	CreatedAt time.Time
}

Se na sua versao atual do aplicativo voce ainda nao tem CreatedAt, tudo bem: tambem e possivel ordenar apenas por ID. Mas o campo de tempo e util para mostrar um criterio de ordenacao "real" tipico (e voce ja conhece time.Time do bloco sobre tempo).

2. sort.Slice: o jeito basico de ordenar

Quando voce precisa de um comeco rapido e claro, sort.Slice e uma excelente escolha. Ele ordena in-place, isto e, muda a ordem dos elementos no slice original. Isso e conveniente, mas as vezes surpreendente: voce pensava que estava "apenas exibindo", mas na verdade tambem alterou os dados.

Aqui esta a ordenacao mais honesta "por ID crescente":

package main

import "sort"

func sortByID(tasks []Task) {
	sort.Slice(tasks, func(i, j int) bool {
		return tasks[i].ID < tasks[j].ID
	})
}

Ha dois significados importantes aqui:

  • o comparador e uma funcao que responde a pergunta: "o elemento i deve vir antes do elemento j?" Ela retorna true se a resposta for sim;
  • se voce ordena antes de exibir, entao ordenar por ID e um bom default: simples, previsivel e facil de explicar ao usuario.

Voce pode verificar com um exemplo pequeno:

package main

import (
	"fmt"
	"sort"
)

func main() {
	tasks := []Task{{ID: 10, Title: "B"}, {ID: 2, Title: "A"}}

	sort.Slice(tasks, func(i, j int) bool { return tasks[i].ID < tasks[j].ID })

	fmt.Println(tasks[0].ID, tasks[1].ID) // 2 10
}

3. Como escolher um criterio de ordenacao

A palavra "criterio" parece algo de prova, mas em programacao isso e apenas uma regra: por qual campo (ou combinacao de campos) comparamos os elementos.

Ordem parcial: por que so Done nao basta

Imagine que voce quer exibir primeiro as tarefas nao concluidas e depois as concluidas. Parece que basta fazer assim:

sort.Slice(tasks, func(i, j int) bool {
	return !tasks[i].Done && tasks[j].Done
})

E aqui comeca a classica pegadinha. Se as duas tarefas estiverem Done=false, o comparador retorna false. Se as duas estiverem Done=true, tambem retorna false. Ou seja, para a maioria dos pares de elementos o comparador diz "nao sei". Como resultado, a ordem dentro do grupo pode ficar... "como der".

As vezes esse "como der" parece estavel na sua maquina, mas na pratica voce deixou uma brecha no contrato: a ordem nao foi definida completamente.

Ordem completa: chave secundaria

A solucao e simples: se os elementos sao iguais no campo principal, comparamos pelo secundario. Isso e a "chave secundaria".

Ordenacao "UNDONE primeiro, dentro dos grupos por ID":

package main

import "sort"

func sortByDoneThenID(tasks []Task) {
	sort.Slice(tasks, func(i, j int) bool {
		a, b := tasks[i], tasks[j]

		if a.Done != b.Done {
			return !a.Done && b.Done // false < true, ou seja, UNDONE vem antes de DONE
		}
		return a.ID < b.ID
	})
}

Esse ja e um contrato de verdade: para quaisquer duas tarefas, conseguimos dizer de forma unica qual vem antes.

Colinha: como comparar campos

As vezes e dificil para iniciantes lembrar como comparar time.Time ou se e possivel comparar strings com <. Aqui vai uma colinha rapida:

Campo Tipo Como comparar em Go Comentario
ID
int
a.ID < b.ID
O criterio mais simples
Title
string
a.Title < b.Title
Lexicograficamente, sensivel a maiusculas/minusculas
Done
bool
if a.Done != b.Done { ... }
Melhor explicitar para ficar legivel
CreatedAt
time.Time
a.CreatedAt.Before(b.CreatedAt)
Nao comparar com <

Exemplo de ordenacao por tempo de criacao:

package main

import "sort"

func sortByCreatedAt(tasks []Task) {
	sort.Slice(tasks, func(i, j int) bool {
		return tasks[i].CreatedAt.Before(tasks[j].CreatedAt)
	})
}

4. sort.Slice e sort.SliceStable

A estabilidade da ordenacao significa: se dois elementos sao "iguais" pela chave de ordenacao, a ordem relativa entre eles e preservada como estava antes da ordenacao. Isso e util quando voce faz uma ordenacao "em varios passos" ou quando a propria ordem original tambem carrega significado.

No pacote sort, existe sort.SliceStable para isso. O uso e parecido com sort.Slice, so que ele garante estabilidade:

package main

import "sort"

func sortByDoneStable(tasks []Task) {
	sort.SliceStable(tasks, func(i, j int) bool {
		return !tasks[i].Done && tasks[j].Done
	})
}

Um detalhe importante: mesmo uma ordenacao estavel nao vai salvar voce se a ordem inicial for "aleatoria" (por exemplo, se voce reuniu tarefas de um map sem ordenar as chaves). Por isso, para o contrato do CLI, muitas vezes e mais simples e mais confiavel fazer uma ordem completa (chave principal + secundaria) do que confiar na estabilidade e no "jeitinho de como estava antes".

5. O pacote slices e slices.SortFunc — Go 1.21+

Na biblioteca padrao do Go surgiu o pacote slices, que traz operacoes prontas para slices, inclusive ordenacao. Ele foi adicionado como uma solucao padrao para tarefas tipicas com slices, e la tambem existem funcoes de ordenacao mais convenientes.

Para casos simples (quando os elementos sao ordenaveis), existe slices.Sort; para comparacao customizada, existe slices.SortFunc. Nos materiais sobre o pacote slices, costuma-se mostrar slices.Sort e slices.Clone como "ferramentas normais", que agora ficam mais legiveis do que solucoes improvisadas.

Por que slices.SortFunc e mais facil de ler

sort.Slice trabalha com os indices i e j. Ja slices.SortFunc traz a comparacao no nivel dos valores (por meio de uma funcao comparadora). Isso muitas vezes e mais legivel: comparamos a e b, e nao "os elementos sob os numeros i e j".

O padrao tipico fica assim (o comparador retorna int: negativo/0/positivo):

package main

import (
	"cmp"
	"slices"
)

func sortByID2(tasks []Task) {
	slices.SortFunc(tasks, func(a, b Task) int {
		return cmp.Compare(a.ID, b.ID)
	})
}

O pacote cmp aqui e a maneira padrao de comparar valores ordenaveis (numeros, strings) por meio de cmp.Compare, para nao precisar escrever na mao "se for menor, retorne -1...". Isso deixa o codigo mais uniforme no estilo.

Criterio complexo com chave secundaria

Vamos fazer o mesmo contrato de antes: UNDONE primeiro, depois DONE; dentro dos grupos, ordenamos por ID.

package main

import (
	"cmp"
	"slices"
)

func sortByDoneThenID2(tasks []Task) {
	slices.SortFunc(tasks, func(a, b Task) int {
		if a.Done != b.Done {
			if !a.Done {
				return -1
			}
			return 1
		}
		return cmp.Compare(a.ID, b.ID)
	})
}

Sim, aqui ha um pouco mais de linhas do que em sort.Slice, mas a logica fica bem "tabular": quase como se estivessemos escrevendo regras de comparacao.

Ordenacao in-place e copia do slice

Tanto sort.Slice quanto slices.SortFunc ordenam in-place. Isso e uma semantica normal e esperada: voce reordena os elementos, e nao cria "um novo universo de tarefas".

Alias, o pacote slices tem ferramentas para fazer uma copia antes da ordenacao, caso voce precise preservar a ordem original. O padrao tipico e: s2 := slices.Clone(s) → ordenamos s2, e o s original continua como estava.

6. Integrando a ordenacao ao pipeline do CLI

Agora vamos juntar isso em um trecho completo do nosso aplicativo. Imagine que ja temos o comando list, que recebe as tarefas do armazenamento e depois chama o render (tabela ou JSON — isso ja foi feito nas aulas anteriores).

Vamos adicionar uma funcao: prepareForOutput, que faz a ordenacao. Os filtros aqui nao serao tratados de proposito: isso e outro tema, e a proxima aula e dedicada a ele.

package main

func prepareForOutput(tasks []Task) []Task {
	// Se nao quisermos alterar o slice original, fazemos uma copia.
	out := make([]Task, len(tasks))
	copy(out, tasks)

	sortByDoneThenID(out) // ou sortByDoneThenID2(out)
	return out
}

Agora, no handler do comando list, a logica fica linear e calma:

package main

import "os"

func runList() error {
	tasks, err := loadTasks() // suponha que isso ja exista
	if err != nil {
		return err
	}

	tasks = prepareForOutput(tasks)

	return render(os.Stdout, "table", tasks) // o formato ja foi normalizado antes
}

Aqui a filosofia e importante: render nao precisa mais pensar em ordem. Ele recebe uma lista pronta e simplesmente a imprime com honestidade. Isso simplifica muito a manutencao: quando surgir um segundo formato de saida ou testes (e eles vao surgir), a ordenacao continuara sendo uma linha em um unico lugar, e nao espalhada por todo o projeto.

7. Erros tipicos ao ordenar antes de exibir

Erro numero 1: ordenar apenas pela chave principal sem chave secundaria, fazendo a ordem "tremer".
Esse e o bug mais traiçoeiro: voce ordena por Done, a saida "parece" normal, mas de repente em uma execucao as tarefas dentro do grupo aparecem em uma ordem, e em outra execucao, em outra. O motivo e que voce nao definiu uma ordem completa. A correcao vem com a adicao de uma chave secundaria (por exemplo, ID), para que a comparacao fique definida para quaisquer dois elementos.

Erro numero 2: ordenar dentro do render (tabela/JSON), e nao antes dele.
No inicio parece conveniente: "ja que eu vou passar por um loop de qualquer forma, eu ordeno ali". Na pratica, isso mistura duas camadas: preparacao de dados e formatacao. O resultado e que a tabela e ordenada, o JSON foi esquecido, os testes falham e o usuario fica irritado. O correto e manter a ordenacao na etapa de preparacao dos dados e chamar o render ja com um []Task pronto.

Erro numero 3: o slice original muda inesperadamente (ordenacao in-place), e a logica depois comeca a se comportar de modo estranho.
Ambas as ordenacoes (sort.Slice e slices.SortFunc) mudam a ordem dos elementos no slice recebido. Se mais abaixo voce contava com a ordem original (ou reutiliza tasks), voce acaba com um "efeito colateral". Se for preciso preservar a ordem, faca uma copia com copy ou slices.Clone, e ordene a copia.

Erro numero 4: o comparador e escrito de um jeito que viola o "bom senso" da comparacao.
As vezes o comparador fica contraditorio: para tres elementos A, B, C ele pode afirmar que A < B, B < C, mas ao mesmo tempo C < A. A ordenacao nao precisa "cair bonito" por causa disso — ela simplesmente deixa de ser previsivel. Se o criterio for complexo, escreva a comparacao em passos: primeiro o campo principal, depois o secundario e so entao, se precisar, o terceiro.

Erro numero 5: esperar que "o JSON se estabilize sozinho".
A saida em JSON e estavel apenas na medida em que o seu []Task for estavel. Se a ordem no slice oscila, o array no JSON tambem vai oscilar. Por isso, a ordenacao e uma etapa obrigatoria para ambos os formatos: tanto para table quanto para json. O pacote slices e suas ordenacoes ajudam justamente a manter essa etapa curta e legivel.

1
Tarefa
Go SELF, nível 51, lição 3
Bloqueado
Lista ordenada
Lista ordenada
1
Tarefa
Go SELF, nível 51, lição 3
Bloqueado
Estoque por ID
Estoque por ID
1
Tarefa
Go SELF, nível 51, lição 3
Bloqueado
Ordem das tarefas
Ordem das tarefas
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION