Criando ferramentas de linha de comando com flag e Cobra

Criando ferramentas de linha de comando com flag e Cobra

CLIs em Go com o pacote flag e o Cobra: subcomandos, grupos e o import que os põe no binário, flags obrigatórias, Viper com flag vinculada e variáveis de ambiente, erros sem ajuda repetida, autocompletar automático e a versão embutida com -X, que precisa do caminho do pacote.
Golang

• • 17 min de leitura

Go é a linguagem de fato para ferramentas de linha de comando. Docker, Kubernetes, Terraform, GitHub CLI, Hugo e centenas de outras ferramentas amplamente usadas foram escritas em Go. As razões são concretas: compilação para binário estático sem dependências, distribuição trivial, startup instantâneo, cross-compilation nativa e performance próxima ao C.

Este artigo cobre duas abordagens: o pacote flag da biblioteca padrão para ferramentas simples, e o Cobra para CLIs com subcomandos, flags complexas e help automático — o padrão da indústria para ferramentas profissionais.

O pacote flag: para CLIs simples

package main

import (
    "flag"
    "fmt"
    "os"
    "time"
)

func main() {
    // Definindo flags
    host := flag.String("host", "localhost", "endereço do servidor")
    porta := flag.Int("port", 8080, "porta do servidor")
    verbose := flag.Bool("verbose", false, "ativar saída detalhada")
    timeout := flag.Duration("timeout", 30*time.Second, "timeout das requisições")

    // Personalizar o texto de uso
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Uso: %s [opções]\n\n", os.Args[0])
        fmt.Fprintln(os.Stderr, "Opções:")
        flag.PrintDefaults()
    }

    flag.Parse()

    // Argumentos posicionais (após as flags)
    args := flag.Args()

    if *verbose {
        fmt.Printf("Conectando em %s:%d (timeout: %v)\n", *host, *porta, *timeout)
        fmt.Printf("Argumentos extras: %v\n", args)
    }

    fmt.Printf("Servidor: %s:%d\n", *host, *porta)
}
# Uso
./app -host=api.exemplo.com -port=443 -verbose arquivo.txt
./app --host api.exemplo.com --port 443
./app -help

O flag aceita um ou dois hífens e o valor com = ou separado por espaço, mas para de ler flags no primeiro argumento que não é flag. Em ./app arquivo.txt -verbose, o -verbose vira mais um argumento posicional, e o programa roda sem a saída detalhada e sem erro nenhum.

O flag funciona bem para ferramentas com um único conjunto de opções. Quando a CLI cresce e precisa de subcomandos como git commit, git push, docker run, o Cobra é a escolha certa.

Cobra: o framework profissional para CLIs

go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest  # para configuração

A estrutura de um projeto CLI com Cobra:

minha-cli/
├── go.mod
├── main.go
├── cmd/
│   ├── root.go       ← comando raiz
│   ├── serve.go      ← subcomando: minha-cli serve
│   ├── migrate.go    ← subcomando: minha-cli migrate
│   └── usuario/
│       ├── usuario.go    ← grupo: minha-cli usuario
│       ├── criar.go      ← minha-cli usuario criar
│       └── listar.go     ← minha-cli usuario listar
└── internal/
    └── config/
        └── config.go

Comando raiz

// cmd/root.go
package cmd

import (
    "fmt"
    "os"
    "strings"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"

    "minha-cli/cmd/usuario"
)

var (
    cfgFile string
    verbose bool
    formato string
)

var rootCmd = &cobra.Command{
    Use:   "minha-cli",
    Short: "Ferramenta de gerenciamento da aplicação",
    Long: `minha-cli é uma ferramenta completa para gerenciar
a aplicação, incluindo servidor, banco de dados e usuários.

Documentação completa em: https://docs.exemplo.com`,
    // Erro de execução não é erro de uso: sem isto, o Cobra imprime o
    // texto de ajuda inteiro junto de cada erro devolvido por um RunE
    SilenceUsage: true,
    // PersistentPreRunE executa antes de QUALQUER subcomando
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        return inicializarConfig()
    },
}

func Execute() {
    // O Cobra já imprime o erro ("Error: ..."); imprimir de novo aqui o duplicaria
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

func init() {
    cobra.OnInitialize(inicializarViper)

    // Flags persistentes — disponíveis em todos os subcomandos
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
        "arquivo de configuração (padrão: $HOME/.minha-cli.yaml)")
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v",
        false, "saída detalhada")
    rootCmd.PersistentFlags().StringVar(&formato, "format", "text",
        "formato de saída: text, json, yaml")

    // Sem o vínculo, viper.GetString("format") nunca enxerga o --format
    viper.BindPFlag("format", rootCmd.PersistentFlags().Lookup("format"))

    // O grupo é registrado aqui: o pacote usuario não importa cmd, e o
    // import acima é o que garante que ele entre no binário
    rootCmd.AddCommand(usuario.UsuarioCmd)
}

func inicializarViper() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        cobra.CheckErr(err)

        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".minha-cli")
    }

    // Variáveis de ambiente sobrescrevem a config. O replacer faz a chave
    // server.port corresponder a SERVER_PORT, que um shell consegue definir
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil && verbose {
        fmt.Fprintln(os.Stderr, "Configuração:", viper.ConfigFileUsed())
    }
}

func inicializarConfig() error {
    if verbose {
        fmt.Printf("Usando banco: %s\n", viper.GetString("database.url"))
    }
    return nil
}

Subcomando: serve

// cmd/serve.go
package cmd

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var (
    serveHost string
    servePort int
)

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Inicia o servidor HTTP",
    Long:  `Inicia o servidor HTTP da aplicação na porta especificada.`,
    Example: `  minha-cli serve
  minha-cli serve --port 9090
  minha-cli serve --host 0.0.0.0 --port 443`,
    RunE: func(cmd *cobra.Command, args []string) error {
        // Lê pelo viper, e não pelas variáveis das flags: só assim
        // a config e o ambiente valem quando a flag não é passada
        return executarServidor(viper.GetString("server.host"), viper.GetInt("server.port"))
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)

    serveCmd.Flags().StringVar(&serveHost, "host", "0.0.0.0",
        "endereço de escuta")
    serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080,
        "porta de escuta")

    // Vincula flags com viper — permite sobrescrever via config ou env
    viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port"))
    viper.BindPFlag("server.host", serveCmd.Flags().Lookup("host"))
}

func executarServidor(host string, porta int) error {
    addr := fmt.Sprintf("%s:%d", host, porta)

    mux := http.NewServeMux()
    mux.HandleFunc("/saude", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })

    servidor := &http.Server{
        Addr:         addr,
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        fmt.Printf("Servidor iniciado em http://%s\n", addr)
        if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Fprintf(os.Stderr, "Erro: %v\n", err)
            os.Exit(1)
        }
    }()

    <-quit
    fmt.Println("\nEncerrando servidor...")

    ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelar()

    return servidor.Shutdown(ctx)
}

Grupo de subcomandos: usuario

// cmd/usuario/usuario.go
package usuario

import (
    "github.com/spf13/cobra"
)

// UsuarioCmd é exportado para o cmd/root.go registrá-lo. Este pacote
// não importa cmd: se importasse, e o root importasse este, seria um ciclo
var UsuarioCmd = &cobra.Command{
    Use:   "usuario",
    Short: "Gerencia usuários da aplicação",
    Long:  `Comandos para criar e listar usuários.`,
}

func init() {
    UsuarioCmd.AddCommand(criarCmd)
    UsuarioCmd.AddCommand(listarCmd)
}
// cmd/usuario/criar.go
package usuario

import (
    "fmt"
    "strings"

    "github.com/spf13/cobra"
)

var (
    criarNome  string
    criarEmail string
    criarPapel string
)

var criarCmd = &cobra.Command{
    Use:   "criar",
    Short: "Cria um novo usuário",
    Example: `  minha-cli usuario criar --nome "João Silva" --email joao@exemplo.com
  minha-cli usuario criar --nome "Admin" --email admin@exemplo.com --papel admin`,
    RunE: func(cmd *cobra.Command, args []string) error {
        return executarCriarUsuario(criarNome, criarEmail, criarPapel)
    },
}

func init() {
    criarCmd.Flags().StringVarP(&criarNome, "nome", "n", "", "nome completo (obrigatório)")
    criarCmd.Flags().StringVarP(&criarEmail, "email", "e", "", "email (obrigatório)")
    criarCmd.Flags().StringVar(&criarPapel, "papel", "usuario",
        "papel do usuário: usuario, admin, moderador")

    // Marca flags como obrigatórias
    criarCmd.MarkFlagRequired("nome")
    criarCmd.MarkFlagRequired("email")
}

func executarCriarUsuario(nome, email, papel string) error {
    papeis := map[string]bool{"usuario": true, "admin": true, "moderador": true}
    if !papeis[strings.ToLower(papel)] {
        return fmt.Errorf("papel inválido: %s (aceitos: usuario, admin, moderador)", papel)
    }

    fmt.Printf("Criando usuário...\n")
    fmt.Printf("  Nome:  %s\n", nome)
    fmt.Printf("  Email: %s\n", email)
    fmt.Printf("  Papel: %s\n", papel)

    // Aqui viria a lógica real de criação
    fmt.Println("✓ Usuário criado com sucesso")
    return nil
}
// cmd/usuario/listar.go
package usuario

import (
    "encoding/json"
    "fmt"
    "os"
    "text/tabwriter"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var (
    listarLimite int
    listarPapel  string
)

var listarCmd = &cobra.Command{
    Use:   "listar",
    Short: "Lista usuários cadastrados",
    RunE: func(cmd *cobra.Command, args []string) error {
        return executarListarUsuarios(listarLimite, listarPapel)
    },
}

func init() {
    listarCmd.Flags().IntVar(&listarLimite, "limite", 20, "máximo de registros")
    listarCmd.Flags().StringVar(&listarPapel, "papel", "", "filtrar por papel")
}

type UsuarioListado struct {
    ID    int    `json:"id"`
    Nome  string `json:"nome"`
    Email string `json:"email"`
    Papel string `json:"papel"`
    Ativo bool   `json:"ativo"`
}

func executarListarUsuarios(limite int, papel string) error {
    // Simulação — em produção buscaria do banco
    usuarios := []UsuarioListado{
        {1, "Ricardo Matos", "ricardo@exemplo.com", "admin", true},
        {2, "Ana Silva", "ana@exemplo.com", "usuario", true},
        {3, "Carlos Lima", "carlos@exemplo.com", "moderador", false},
    }

    if papel != "" {
        filtrados := usuarios[:0]
        for _, u := range usuarios {
            if u.Papel == papel {
                filtrados = append(filtrados, u)
            }
        }
        usuarios = filtrados
    }

    if len(usuarios) > limite {
        usuarios = usuarios[:limite]
    }

    // Formato de saída configurável
    formato := viper.GetString("format")
    switch formato {
    case "json":
        return json.NewEncoder(os.Stdout).Encode(usuarios)
    default:
        w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
        fmt.Fprintln(w, "ID\tNOME\tEMAIL\tPAPEL\tATIVO")
        fmt.Fprintln(w, "--\t----\t-----\t-----\t-----")
        for _, u := range usuarios {
            ativo := "sim"
            if !u.Ativo {
                ativo = "não"
            }
            fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
                u.ID, u.Nome, u.Email, u.Papel, ativo)
        }
        return w.Flush()
    }
}

Subcomando: migrate

// cmd/migrate.go
package cmd

import (
    "fmt"
    "strconv"

    "github.com/spf13/cobra"
)

var migrateCmd = &cobra.Command{
    Use:   "migrate",
    Short: "Gerencia migrações do banco de dados",
}

var migrateUpCmd = &cobra.Command{
    Use:   "up",
    Short: "Executa todas as migrações pendentes",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("Executando migrações...")
        // golang-migrate aqui
        fmt.Println("✓ Migrações concluídas")
        return nil
    },
}

var migrateDownCmd = &cobra.Command{
    Use:   "down [n]",
    Short: "Reverte a última migração (ou N migrações)",
    Args:  cobra.MaximumNArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        n := 1
        if len(args) > 0 {
            var err error
            n, err = strconv.Atoi(args[0])
            if err != nil || n < 1 {
                return fmt.Errorf("n deve ser um inteiro positivo, recebido %q", args[0])
            }
        }
        fmt.Printf("Revertendo %d migração(ões)...\n", n)
        fmt.Println("✓ Concluído")
        return nil
    },
}

var migrateStatusCmd = &cobra.Command{
    Use:   "status",
    Short: "Exibe o status das migrações",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("Versão atual: 5")
        fmt.Println("Pendentes: 0")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(migrateCmd)
    migrateCmd.AddCommand(migrateUpCmd)
    migrateCmd.AddCommand(migrateDownCmd)
    migrateCmd.AddCommand(migrateStatusCmd)
}

main.go: ponto de entrada enxuto

// main.go
package main

import "minha-cli/cmd"

func main() {
    cmd.Execute()
}

Completions: autocompletar no shell

O Cobra acrescenta sozinho um subcomando completion, com scripts para bash, zsh, fish e PowerShell: ele aparece no --help de qualquer CLI feita com o Cobra, sem nenhuma linha de código. Escrever o comando à mão só é necessário para mudar o seu comportamento; um comando próprio com o nome completion substitui o padrão:

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Gera script de autocompletar para o shell",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        switch args[0] {
        case "bash":
            return rootCmd.GenBashCompletionV2(os.Stdout, true)
        case "zsh":
            return rootCmd.GenZshCompletion(os.Stdout)
        case "fish":
            return rootCmd.GenFishCompletion(os.Stdout, true)
        case "powershell":
            return rootCmd.GenPowerShellCompletion(os.Stdout)
        default:
            return fmt.Errorf("shell não suportado: %s", args[0])
        }
    },
}
# Instalando o autocompletar no bash
minha-cli completion bash > /etc/bash_completion.d/minha-cli

# Ou temporariamente na sessão atual
source <(minha-cli completion bash)

Saída colorida e progress

import "github.com/fatih/color"

var (
    verde    = color.New(color.FgGreen).SprintFunc()
    vermelho = color.New(color.FgRed).SprintFunc()
    amarelo  = color.New(color.FgYellow).SprintFunc()
    azul     = color.New(color.FgCyan).SprintFunc()
)

func imprimirStatus(ok bool, mensagem string) {
    if ok {
        fmt.Printf("%s %s\n", verde("✓"), mensagem)
    } else {
        fmt.Printf("%s %s\n", vermelho("✗"), mensagem)
    }
}

func imprimirAviso(mensagem string) {
    fmt.Printf("%s %s\n", amarelo("⚠"), mensagem)
}

func imprimirInfo(mensagem string) {
    fmt.Printf("%s %s\n", azul("ℹ"), mensagem)
}

Compilando e distribuindo

# Build padrão
go build -o minha-cli .

# Build com versão embutida. O -X precisa do caminho completo do pacote
# onde a variável está; um nome que não existe é ignorado sem aviso
go build -ldflags="-X minha-cli/cmd.versao=1.2.0 -X minha-cli/cmd.build=$(date -u +%Y%m%d)" -o minha-cli .

# Cross-compilation para múltiplos sistemas
GOOS=linux   GOARCH=amd64 go build -o dist/minha-cli-linux-amd64 .
GOOS=darwin  GOARCH=arm64 go build -o dist/minha-cli-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o dist/minha-cli-windows-amd64.exe .
// cmd/version.go — versão embutida via ldflags
package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var (
    versao = "desenvolvimento"
    build  = "desconhecido"
)

var versaoCmd = &cobra.Command{
    Use:   "version",
    Short: "Exibe a versão da ferramenta",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("minha-cli v%s (build: %s)\n", versao, build)
    },
}

func init() {
    rootCmd.AddCommand(versaoCmd)
}

O flag resolve ferramentas de um comando só, e o Cobra organiza as maiores em subcomandos, com ajuda, validação de argumentos e autocompletar prontos. Quase todo o trabalho que sobra está nas ligações entre as peças: o pacote de um grupo de comandos só entra no binário se alguém o importar, uma flag só chega ao Viper se for vinculada, e uma configuração só vale se o comando ler pelo Viper, e não pela variável da flag. Nenhuma dessas falhas impede a compilação, e por isso vale testar a CLI pronta pelo terminal, com --help, flags, variáveis de ambiente e arquivo de configuração.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Na primeira versão deste projeto, o pacote cmd/usuario se registrava no próprio init, com cmd.RootCmd.AddCommand(UsuarioCmd), e o main.go importava só minha-cli/cmd. O go build passava, mas minha-cli usuario listar respondia unknown command "usuario". Por que o build não acusou nada, e qual é a forma certa de ligar os pacotes?

Ver resposta

✓ Resposta: O Go compila apenas os pacotes alcançáveis a partir do main pelos imports. Como ninguém importava cmd/usuario, o pacote ficava fora do build: o init dele nunca rodava, e nem os seus erros de compilação eram vistos. Num teste, go build . terminou sem erro e o --help listou completion, migrate e serve, sem usuario; só o go build ./... mostrou que o pacote nem compilava, com undefined: cmd.RootCmd, porque a variável era rootCmd, minúscula. Fazer o cmd importar usuario para resolver criaria um ciclo, já que usuario importava cmd, e o Go recusa ciclos de import. A forma certa é a dependência ir num sentido só: usuario exporta o comando e não sabe nada da raiz, e o cmd/root.go importa usuario e chama rootCmd.AddCommand(usuario.UsuarioCmd). Rodar go build ./... e go vet ./... no CI pega pacotes esquecidos.

Exercício 2

O serve vincula as flags com viper.BindPFlag("server.port", ...), e o arquivo de configuração diz server: port: 9999. Na versão antiga, o servidor subia sempre na 8080, com ou sem o arquivo, e SERVER_PORT=9998 também não mudava nada. Quais eram os dois defeitos?

Ver resposta

✓ Resposta: O BindPFlag faz o Viper consultar a flag ao responder viper.GetInt("server.port"), numa ordem de precedência: flag passada, variável de ambiente, arquivo de configuração e, por fim, o valor padrão da flag. Mas a versão antiga nunca perguntava ao Viper: o RunE passava a variável servePort, preenchida só pela flag, e o arquivo e o ambiente eram ignorados. Num teste, com o arquivo e com a variável, o servidor subiu em 0.0.0.0:8080. O segundo defeito estava no nome da variável de ambiente: o AutomaticEnv transforma a chave server.port em SERVER.PORT, com ponto, um nome que os shells não deixam definir. Com o RunE lendo pelo Viper e o SetEnvKeyReplacer(strings.NewReplacer(".", "_")), a mesma CLI subiu na 9999 com o arquivo, na 9998 com SERVER_PORT, e na 9997 com -p 9997 além da variável, porque a flag passada vence.

Exercício 3

O pipeline de release compila com -ldflags="-X main.versao=1.2.0", e minha-cli version continua respondendo vdesenvolvimento. O build não mostra aviso nenhum. O que está errado?

Ver resposta

✓ Resposta: O -X do linker recebe o caminho completo do pacote mais o nome da variável, e a variável versao está no pacote minha-cli/cmd, junto do comando version, e não no main. Quando o símbolo indicado não existe, o linker simplesmente não faz nada: num teste, o mesmo build com -X minha-cli/cmd.versao=1.2.0 -X main.versao=x terminou sem aviso, e o binário respondeu minha-cli v1.2.0, ignorando a parte errada. O -X também só funciona com variáveis do tipo string que não sejam constantes nem inicializadas por chamada de função. Para não depender disso, o Go grava sozinho no binário a versão do módulo e os dados do controle de versão, legíveis com debug.ReadBuildInfo() ou com go version -m minha-cli; o -X fica para o que não está lá, como o número de uma release.

Exercício 4

Um usuário digita minha-cli migrate down tres numa versão antiga do comando, que lia o argumento com fmt.Sscan(args[0], &n). O que acontece com o banco, e por que esse tipo de defeito é mais grave numa CLI?

Ver resposta

✓ Resposta: O fmt.Sscan devolve um erro quando o texto não é um número, mas o código antigo descartava o retorno, e o n ficava com o valor inicial, 1. Num teste, down abc imprimiu Revertendo 1 migração(ões)... e terminou com sucesso: o usuário pediu algo impossível e o comando reverteu uma migração mesmo assim. Com o strconv.Atoi e o erro devolvido pelo RunE, o comando parou com Error: n deve ser um inteiro positivo, recebido "abc" e código de saída 1, sem tocar o banco. Numa CLI, o defeito pesa mais porque o comando costuma rodar dentro de scripts e pipelines, que só olham o código de saída: um sucesso falso deixa o script seguir em frente. Comandos destrutivos ainda merecem uma camada a mais, como pedir confirmação ou exigir uma flag --force quando não há terminal interativo.

Exercício 5

Um script chama ./app relatorio.csv -verbose, usando o programa da primeira seção, feito com o pacote flag. Não aparece a saída detalhada, e não há erro. Com ./app -verbose relatorio.csv, funciona. Por quê, e o que muda com o Cobra?

Ver resposta

✓ Resposta: O flag.Parse lê flags até encontrar o primeiro argumento que não começa com hífen e para ali: tudo o que vem depois, inclusive o -verbose, vai para flag.Args() como argumento posicional. Num teste, go run . arquivo.txt -verbose -port 9 imprimiu Servidor: localhost:8080, ignorando as duas flags, sem erro. É o comportamento tradicional das ferramentas Unix e permite passar argumentos que começam com hífen, mas surpreende quem está acostumado a outros programas. O Cobra usa o pacote pflag, que aceita flags misturadas aos argumentos em qualquer posição e só para de interpretá-las depois de um -- isolado. Com o flag padrão, a saída é documentar a ordem no texto de uso ou verificar flag.Args() e recusar argumentos que pareçam flags.

Comentários

Mais em Golang

Introdução ao database/sql e drivers em Go
Introdução ao database/sql e drivers em Go

O pacote database/sql em Go: drivers e import em branco, o pool de conexões e…

Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run
Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run

Deploy de uma API Go no Railway, no Fly.io e no Google Cloud Run: CLIs…

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Tour pela biblioteca padrão do Go: fmt, strings, strconv, time, os, io e…