Manipulando data e hora com Go

Manipulando Data e Hora com GoLang

Durante a sua jornada como desenvolvedor, em algum momento você vai precisar trabalhar com datas e horas, seja para a criação de logs, ou para a comparação de um cupom de desconto em um período específico.

A manipulação de data e hora em projetos feitos com GoLang é essencial, pois uma hora ou outra, você pode acabar se esbarrando com alguma operação que requisite tal funcionalidade.

O ponto positivo, é que assim como toda linguagem de programação, o Go já conta com mecanismos e bibliotecas capazes de manipular datas e horas.

Posso começar a rodar o cronômetro? Mas não se sinta pressionado, ok? 😅

Criando seu projeto de testes

Dentro da pasta JornadaGoLang, nós iremos criar uma nova pasta chamada de 26-manipulando-data-e-hora-em-go, onde dentro dela, vamos criar o nosso arquivo main.go:

package main

func main(){

}

Feito isso, vamos aprender um pouco mais sobre a manipulação de data e hora em GoLang 😋

Conhecendo o pacote Time

A manipulação de datas e horas em Go é feita basicamente através do pacote padrão time. Com ele seremos capazes de fazer manipulações avançadas, tais como:

  • Adição e Subtração,
  • Comparações,
  • Formatações,
  • Parse de Tempo,
  • Time Zones,
  • etc...

Para usar essa biblioteca basta importá-la no seu projeto da seguinte forma:

import "time"

Caso você já possua outros imports, basta implementá-la da seguinte forma:

import (
    "fmt"
    "time"
)

E sim, ela é uma biblioteca padrão do Go, e você não precisar baixar nada para manipular o tempo 😁

Feito isso, já estamos prontos para executarmos um "Hello World" com essa biblioteca.

Retornando a data e hora atuais

Uma das coisas mais simples de serem feitas com a biblioteca time é retornar a data e hora atual, no momento em que o GoLang passar pelo código que faz isso, vejamos:

package main

import (
    "fmt"
    "time"
)

func main() {
    agora := time.Now()
    fmt.Println("Data e hora atuais:", agora)
}

O comando time.Now() retorna um valor do tipo time.Time representando o instante atual. A saída esperava no console deverá ser similar a esta:

Data e hora atuais: 2025-06-02 17:51:39.6069216 -0300 -03 m=+0.000000001

Dúvida: Beleza, mas isso tudo aí é uma data? E se eu quiser retornar apenas o ano? ou o mês e o dia? Ou a hora e o minuto?

Sim, tudo isso é possível com a biblioteca time, mas antes, você precisa entender que o tipo central que representa datas/horas é essa struct:

type Time struct { ... }

Ela conta com alguns métodos principais que podem ser usados para extrair uma instância de time.Time, vejamos:

t := time.Now()

ano := t.Year()           // ex: 2025
mes := t.Month()          // retorna time.Month (janeiro=1, fevereiro=2, ...)
dia := t.Day()            // dia do mês (1-31)
hora := t.Hour()          // hora (0-23)
minuto := t.Minute()      // minuto (0-59)
segundo := t.Second()     // segundo (0-59)
nanosegundo := t.Nanosecond() // nanosegundos dentro do segundo (0-999999999)
weekday := t.Weekday()    // dia da semana (time.Weekday)

Repare que, no comando acima, atribuímos à variável t o valor da data e hora atuais. A partir de t, podemos invocar métodos para obter o ano, o mês, o dia, a hora, o minuto, o segundo, o nanosegundo e até mesmo o dia da semana.

É importante destacar que esses métodos extraem informações referentes ao instante armazenado em t.

Ou seja, se você capturou a data e hora atuais no início do programa e guardou em t, e somente após 10 minutos acessa t.Minute(), o valor retornado continuará sendo o minuto correspondente ao momento em que t foi criado—não o minuto atual do sistema. Dessa forma, para sempre obter a hora “viva” do sistema, seria preciso chamar time.Now() novamente em vez de reutilizar a mesma variável.

Vejamos um exemplo prático dos métodos vistos anteriormente:

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Printf("Ano: %d\n", t.Year())
    fmt.Printf("Mês: %s (%d)\n", t.Month(), t.Month())
    fmt.Printf("Dia: %d\n", t.Day())
    fmt.Printf("Hora: %d\n", t.Hour())
    fmt.Printf("Minuto: %d\n", t.Minute())
    fmt.Printf("Segundo: %d\n", t.Second())
    fmt.Printf("Dia da semana: %s\n", t.Weekday())
}

Criando datas de forma manual

Também é possível criarmos datas de forma manual, onde conseguimos informar o ano, mês, dia etc...

Isso é útil quando partes do nosso sistema precisa comparar datas com datas, ou quem sabe realizar adições e subtrações com as mesmas.

Para criar uma data e hora específica, você pode usar o seguinte comando:

// Exemplo com fuso horário de São Paulo
locSP, err := time.LoadLocation("America/Sao_Paulo")
if err != nil {
    panic(err)
}

tSP := time.Date(2025, time.June, 15, 14, 30, 0, 0, locSP)
fmt.Println("Data em horário de SP:", tSP)

O parâmetro locSP define o fuso horário, já o Time.June define o mês escolhido. Vejamos abaixo uma lista completa das constantes de time.Month em Go (cada uma mapeia para seu respectivo número de mês):  

  • time.January → janeiro (1)
  • time.February → fevereiro (2)
  • time.March → março (3)
  • time.April → abril (4)
  • time.May → maio (5)
  • time.June → junho (6)
  • time.July → julho (7)
  • time.August → agosto (8)
  • time.September → setembro (9)
  • time.October → outubro (10)
  • time.November → novembro (11)
  • time.December → dezembro (12)

Além disso, usamos o zero value de time.Time para representar a data "zero" (1 de janeiro do ano 1, em UTC). Pode servir como valor inicial para verificações de "data vazia", observe:

var z time.Time
fmt.Println(z) // "0001-01-01 00:00:00 +0000 UTC"

Criando datas de forma manual por meio de strings

Durante a construção do seu sistema, é bem provável que um determinado formulário HTML vá enviar datas para você, e obviamente, que na maior parte das vezes, essas datas virão em formato de strings (textos puros).

Como por exemplo: "2025-06-15" ou "15/06/2025 14:30".

Para transformar essas strings em valores do tipo time.Time (que o Go consegue manipular como datas), usamos as funções time.Parse ou time.ParseInLocation, definindo um layout que corresponda exatamente à estrutura da string recebida.

Em certos momentos, as strings retornadas pelo HTML não conterão informações relacionadas ao Fuso Horário, sendo assim o ideal é que façamos o Parse da seguinte forma:

t, err := time.Parse(layout, valorString)

Vejamos um exemplo completo:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Suponha que o formulário envie a data no formato "YYYY-MM-DD"
    dataBruta := "2025-06-15"

    // Definimos o layout que corresponde à string: "2006-01-02"
    const layoutISO = "2006-01-02"

    // Faz o parse (interpreta como UTC)
    t, err := time.Parse(layoutISO, dataBruta)
    if err != nil {
        fmt.Println("Erro ao converter a data:", err)
        return
    }

    fmt.Println("Data convertida (UTC):", t) // Impresso em UTC
    fmt.Println("Ano:", t.Year(), "Mês:", t.Month(), "Dia:", t.Day())
}

A saída possível seria:

Data convertida (UTC): 2025-06-15 00:00:00 +0000 UTC
Ano: 2025 Mês: June Dia: 15

Repare que, ao não especificar um fuso distinto, o Go pressupõe um UTC para a data resultante.

E sim, como dito em lições anteriores, o GoLang precisa de um layout que corresponda a string, e por isso usamos a data "2006-01-02".

Essa data é importante porque ela faz parte do "date reference" que o Go adotou como padrão: Mon Jan 2 15:04:05 MST 2006.

Cada componente dessa data de referência (ano 2006, mês 01, dia 02, hora 15, minuto 04, segundo 05) serve como template para indicar ao parser onde encontrar cada parte na string que queremos converter.

Quando escrevemos layout := "2006-01-02", estamos dizendo "procure uma string onde os quatro primeiros caracteres sejam o ano, seguidos de hífen, depois dois dígitos de mês, hífen e dois dígitos de dia".

Veja um exemplo prático:

package main

import (
    "fmt"
    "time"
)

func main() {
	// Exemplo 1: converter "2025-06-15" para time.Time
	layout := "2006-01-02"
	strData := "2025-06-15"
	t, err := time.Parse(layout, strData)
	if err != nil {
		fmt.Println("Erro ao converter:", err)
		return
	}
	fmt.Println("Data convertida:", t) // Resultado em UTC: 2025-06-15 00:00:00 +0000 UTC
}

Selecionando o instante atual junto com transformações

Como você já sabe, o time.Now() é usado para retornar uma struct de time.Time no fuso horário local da máquina onde o sistema está instalado.

Mas além dele, você consegue retornar também o fuso UTC e realizar conversões do tempo atual para outra localização por meio do In(loc), vejamos:

package main

import (
    "fmt"
    "time"
)

func main() {
	agoraLocal := time.Now()
	agoraUTC := agoraLocal.UTC()
	locSP, _ := time.LoadLocation("America/Sao_Paulo")
	agoraSP := agoraUTC.In(locSP)

	fmt.Println("Now (local):", agoraLocal)
	fmt.Println("Now (UTC):", agoraUTC)
	fmt.Println("Now (Sao Paulo):", agoraSP)
}

O comando time.Now().UTC() retorna time.Time no fuso UTC.

Já o comando time.Now().In(loc) converte o tempo atual para outra localização.

Realizando operações aritméticas com datas

Quase sempre, você vai precisar realizar algumas operações com datas, tais como adição e subtração de duração, além de poder encontrar diferenças entre duas datas.

Vejamos como isso tudo pode ser feito 😀

Para realizar adição e subtração de duração (em horas, minutos e segundos) usamos o t.Add(d time.Duration) para isso:

package main

import (
    "fmt"
    "time"
)

func main() {
	t := time.Now()
	maisUmaHora := t.Add(time.Hour * 1)
	menosTrintaMin := t.Add(-30 * time.Minute)
	fmt.Println("Agora +1h:", maisUmaHora)
	fmt.Println("Agora -30m:", menosTrintaMin)
}

Suas durations podem ser construídas via operações com constantes, por exemplo:

  • time.Nanosecond
  • time.Microsecond
  • time.Millisecond
  • time.Second
  • time.Minute
  • time.Hour

Além disso, é possível somar várias durações ao mesmo tempo, da seguinte forma:

package main

import (
    "fmt"
    "time"
)

func main() {
	// 2 horas + 45 minutos + 30 segundos
	t := time.Now()
	d := 2*time.Hour + 45*time.Minute + 30*time.Second
	tNovo := t.Add(d)
	fmt.Println("Depois de 2h45m30s:", tNovo)
}

Já para fazer isso usando anos, meses ou dias, fazemos o uso do t.AddDate(years, months, days), da seguinte forma:

package main

import (
    "fmt"
    "time"
)

func main() {
	t:= time.Now()

	// Adiciona 1 ano, 2 meses e 10 dias
	tFuturo := t.AddDate(1, 2, 10)

	// Subtrai 0 anos, 3 meses e 5 dias
	tPassado := t.AddDate(0, -3, -5)

	fmt.Println("Agora:", t)
	fmt.Println("Futuro (+1a+2m+10d):", tFuturo)
	fmt.Println("Passado (-3m-5d):", tPassado)
}

Se o dia resultante não existir no mês (ex.: 31 de novembro), a própria biblioteca ajusta para o último dia válido do mês. Além disso, os horários, fuso e nanosegundos são mantidos.

Por fim, para identificar as diferenças entre duas datas, usamos o comando t2.Sub(t1) para obter a diferença com time.Duration:

package main

import (
    "fmt"
    "time"
)

func main() {
	t1 := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC)
	t2 := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)

	d := t2.Sub(t1) // time.Duration representando 14 dias e 12 horas
	fmt.Println("Diferença em horas:", d.Hours())
	fmt.Println("Diferença em dias (aprox):", d.Hours()/24)
}

Realizando comparações entre datas

Supondo que você tenha um serviço de plano de assinatura, como você faz para validar se aquele usuário ainda pode usar o seu sistema ou não?

Obviamente que a resposta estará na comparação entre duas datas, a data de acesso atual, e a data limite de acesso que está armazenada em algum canto do seu sistema.

Um time.Time pode ser comparado de 3 formas diferentes:

  • t1.Before(t2): true se t1 < t2
  • t1.After(t2): true se t1 > t2
  • t1.Equal(t2): true se têm o mesmo instante (considera também o mesmo local/fuso)

Vejamos um exemplo prático:

package main

import (
    "fmt"
    "time"
)

func main() {
	t1 := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC)
	t2 := time.Now().UTC()

	if t1.Before(t2) {
		fmt.Println("t1 é anterior a t2")
	} else if t1.After(t2) {
		fmt.Println("t1 é posterior a t2")
	} else {
		fmt.Println("t1 e t2 são iguais")
	}
}

Junto a isso, podemos validar se uma data já passou ou não:

package main

import (
    "fmt"
    "time"
)

func checkVencimento(dataVencimento time.Time) {
    agora := time.Now()
    if agora.After(dataVencimento) {
        fmt.Println("A data de vencimento já passou!")
    } else if agora.Equal(dataVencimento) {
        fmt.Println("Hoje é o dia do vencimento!")
    } else {
        fmt.Println("Ainda falta tempo para vencimento.")
    }
}

func main() {
    venc := time.Date(2025, 6, 10, 0, 0, 0, 0, time.Local)
    checkVencimento(venc)
}

Vejamos um outro exemplo que foi usado para calcular a idade a partir da data de nascimento do individuo:

package main

import (
    "fmt"
    "time"
)

func calcularIdade(nascimento time.Time) int {
    hoje := time.Now()
    idade := hoje.Year() - nascimento.Year()

    // Se ainda não chegou ao mês/dia de aniversário neste ano, subtrai 1
    if hoje.Month() < nascimento.Month() ||
        (hoje.Month() == nascimento.Month() && hoje.Day() < nascimento.Day()) {
        idade--
    }
    return idade
}

func main() {
    // Supondo data de nascimento: 15 de agosto de 1990
    nascimento := time.Date(1990, time.August, 15, 0, 0, 0, 0, time.UTC)
    fmt.Println("Idade:", calcularIdade(nascimento)) // Ex.: 34 (em jun/2025)
}

Trabalhando com Fusos Horários (Times Zones)

Aprendemos na escola que cada pais, cidade e estado pode contar com fusos horários diferentes. Aqui no Brasil usamos bastante o America/Sao_Paulo, que equivale ao horário de Brasília (UTC -03:00).

Além dele, temos outros fusos, tais como:

UTC: empo Universal Coordenado, ponto de referência básico (equivalente a GMT).

America/New_York: Leste dos EUA (Eastern Time, normalmente UTC−05:00 ou UTC−04:00 no horário de verão).

Europe/London: Reino Unido (UTC+00:00 ou UTC+01:00 no horário de verão).

Asia/Shanghai: China (UTC+08:00).

Africa/Johannesburg: África do Sul (UTC+02:00, sem horário de verão).

Caso desejar, você pode encontrar a lista completa de todos os fusos horários (incluindo abreviações, offsets e regras de horário de verão) em: https://www.iana.org/time-zones

No GoLang usamos o comando time.Now().UTC() para converter para UTC, para posteriormente fazermos a manipulação dos nosso fusos horários.

Observe um exemplo abaixo onde estamos carregando uma localização específica:

package main

import (
    "fmt"
    "time"
)

func main() {
	locSP, err := time.LoadLocation("America/Sao_Paulo")
	if err != nil {
		panic(err)
	}

	tSP := time.Date(2025, 6, 2, 12, 0, 0, 0, locSP)
	fmt.Println("Horário SP:", tSP.Format(time.RFC3339))
}

Supondo que você queria fazer conversões entre fusos diferentes, você pode usar a seguinte estratégia:

package main

import (
    "fmt"
    "time"
)

func main() {
	// t1 em UTC
	t1 := time.Now().UTC()

	// converter para horário de São Paulo
	locSP, _ := time.LoadLocation("America/Sao_Paulo")
	tSP := t1.In(locSP)
	fmt.Println("Em SP:", tSP)
}

Trabalhando com durações

Imagine que você queria rodar um processo longo, e entender quanto tempo aquele processo levou, como você poderia fazer isso?

A primeira alternativa seria realizar um calculo matemático, porém, você pode ganhar tempo usando uma função específica do Go chamada de Since, que é capaz de identificar o intervalo entre dois tempos, vejamos um exemplo:

package main

import (
    "fmt"
    "time"
)

func main() {
	inicio := time.Now()
	// ...faz algo demorado...
	duracao := time.Since(inicio)
	fmt.Println("Levantamento levou:", duracao) // ex: "2.3456789s"
}

Obviamente que a execução do comando acima durará 0 segundos, pois não criamos uma função que fazia algo demorado.

Além disso, você pode extrair componentes de uma Duration da seguinte forma, por exemplo:

d := time.Duration(5*time.Hour + 30*time.Minute + 10*time.Second)
fmt.Println("Horas totais:", d.Hours())        // 5.502777...
fmt.Println("Minutos totais:", d.Minutes())    // 330.1666...
fmt.Println("Segundos totais:", d.Seconds())   // 19810.0
fmt.Println("Milissegundos:", d.Milliseconds()) // 19810000

Formatando diferentes padrões regionais

Se precisar de formatação em português ou outro idioma, é necessário criar manualmente o layout ou usar bibliotecas de terceiros.

No nosso exemplo, eu irei criar o nome dos meses de forma manual em português (meses por extenso):

package main

import (
    "fmt"
    "time"
)

func main() {
	meses := []string{
		"janeiro", "fevereiro", "março", "abril", "maio", "junho",
		"julho", "agosto", "setembro", "outubro", "novembro", "dezembro",
	}

	t := time.Now()
	dia := t.Day()
	mesExtenso := meses[int(t.Month())-1]
	ano := t.Year()

	fmt.Printf("%02d de %s de %d\n", dia, mesExtenso, ano)
	// Ex.: "02 de junho de 2025"
}

Como resultado, a data no console ficará em português do brasil, incrível não? 😌

Lidando com timestamps em Unix

Veja um exemplo onde estamos convertendo Unix/UnixNano de volta para time.Time:

package main

import (
    "fmt"
    "time"
)

func main() {
	// Exemplo: converter timestamp Unix para Time
	ts := time.Now().Unix()
	tFromUnix := time.Unix(ts, 0) // 0 nanosegundos extra
	fmt.Println("De volta:", tFromUnix)

	// Mesmo com nanosegundos
	tsNano := time.Now().UnixNano()
	tFromNano := time.Unix(0, tsNano)
	fmt.Println("De volta (nano):", tFromNano)
}

Se você quiser trabalhar diretamente com o padrão timestamp (isto é, o número de segundos, milissegundos ou nanosegundos desde 1º de janeiro de 1970), o Go fornece métodos para gerar, converter e interpretar esses valores. Em geral, existem três "granularidades" comuns:

  • Segundos (10 dígitos)
  • Milissegundos (13 dígitos)
  • Nanosegundos (19 dígitos)

Vejamos um exemplo:

package main

import (
    "fmt"
    "time"
)

func main() {
	now := time.Now()

	// 1. Em segundos (Unix epoch, 10 dígitos aprox.)
	tsSegundos := now.Unix()                
	fmt.Println("Timestamp (segundos):", tsSegundos)

	// 2. Em nanosegundos (Unix epoch, 19 dígitos aprox.)
	tsNano := now.UnixNano()                 
	fmt.Println("Timestamp (nanosegundos):", tsNano)

	// 3. Em milissegundos (Unix epoch, 13 dígitos)
	// Basta dividir o UnixNano por 1_000_000
	tsMillis := now.UnixNano() / int64(time.Millisecond)
	fmt.Println("Timestamp (milissegundos):", tsMillis)
}

Principais erros que podem ocorrer durante a formatação de datas

Para te antecipar aos possíveis erros que podem acontecer durante a geração de datas no seu sistema, separei algumas situações que podem acontecer, e as medidas que você precisa tomar para resolvê-las 🫡

Situação 1) Layout de formatação errado:

Se o layout de uma de suas datas não corresponder exatamente ao formato da string, o comando time.Parse retornará erro. Por exemplo:

layout := "2006-01-02"
str := "15/06/2025"

// time.Parse falhará, pois a string não tem o mesmo padrão

Vamos ver um exemplo mais completo de layout incompatível:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Situação inicial: queremos converter a string "15/06/2025"
    // usando o layout "2006-01-02":
    layoutErrado := "2006-01-02"
    str := "15/06/2025"

    // 1) Tentativa de fazer o parse com layout incompatível
    t1, err1 := time.Parse(layoutErrado, str)
    if err1 != nil {
        fmt.Println("Erro ao fazer Parse com layout “2006-01-02” →", err1)
    } else {
        // Esse bloco não será alcançado, pois o Parse falha
        fmt.Println("Data convertida (inacreditável!):", t1)
    }

    // Saída esperada:
    // Erro ao fazer Parse com layout “2006-01-02” → parsing time "15/06/2025" as "2006-01-02": cannot parse "15/06/2025" as "2006"

    // 2) Correção: usar o layout que corresponde exatamente à string "15/06/2025".
    // No formato brasileiro, dia/mês/ano com barras, o layout é "02/01/2006".
    layoutCorreto := "02/01/2006"

    // Se quisermos considerar o fuso local (por exemplo, hora=meia-noite em UTC ou local),
    // basta usar ParseInLocation. Aqui, vamos interpretar a data como UTC para simplificar:
    t2, err2 := time.Parse(layoutCorreto, str)
    if err2 != nil {
        fmt.Println("Ainda deu erro:", err2)
        return
    }
    // Agora t2 contém 2025-06-15 00:00:00 +0000 UTC
    fmt.Println("Parse bem-sucedido com layout “02/01/2006” →", t2)

    // 3) Se quisermos forçar um fuso específico (ex.: São Paulo), usamos ParseInLocation:
    locSP, errLoc := time.LoadLocation("America/Sao_Paulo")
    if errLoc != nil {
        // Caso não consiga carregar o fuso, cria um fixo de UTC-3:
        locSP = time.FixedZone("BRT", -3*3600)
    }
    t3, err3 := time.ParseInLocation(layoutCorreto, str, locSP)
    if err3 != nil {
        fmt.Println("Erro em ParseInLocation:", err3)
        return
    }
    // Agora t3 = 2025-06-15 00:00:00 -03:00 BRT
    fmt.Println("ParseInLocation com São Paulo →", t3)

    // 4) Demonstrando uso da data convertida:
    fmt.Printf(
        "Ano: %d, Mês: %02d, Dia: %02d, Timestamp (segundos): %d\n",
        t3.Year(), t3.Month(), t3.Day(), t3.Unix(),
    )
}

Solução: descobrir o formato exato da string e construir o layout correspondente, por exemplo "02/01/2006" para "dia/mês/ano com barras".

Situação 2) Time zone não encontrado:

Ao usar o comando time.LoadLocation("America/Sao_Paulo"), se o SO (seu sistema operacional) não tiver informações de fuso (por exemplo, em alguns containers), tal código pode falhar.

Nesse caso, opte por utilizar o time.FixedZone("BST",-3*3600), e como alternativa você pode resolver isso da seguinte forma:

locSP := time.FixedZone("BRT", -3*3600) // BRT = UTC-3h

Situação 3) Comparações sem normalizar fuso:

Duas time.Time podem representar o mesmo instante, mas se tiverem fusos diferentes, Equal poderá retornar false. Portanto, faça sempre o uso do .UTC() ou o mesmo fuso antes de fazer as devidas comparações.

Repositório da lição

Todos os arquivos relacionados com esta lição, podem ser encontrados nos seguintes repositórios abaixo:

Conclusão

Nesta lição, você aprendeu a fazer diversas manipulações de data e hora em GoLang, explorando ao máximo a biblioteca time.

Até a próxima lição 😆

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.