28장. 시간 다루기
시간은 프로그램 어디에나 등장한다. 로그 타임스탬프, 만료 검사, 타임아웃, 주기적인 작업, 기록 정렬…
Go 는 time 패키지 하나로
대부분의 시간 작업을 처리한다.
이번 장에서 그 사용법을 둘러본다.
목표:
time.Time으로 시각을 표현하기- 현재 시각을 얻고 임의의 시각을 만들기
- 포맷팅과 파싱 (Go 의 독특한 reference time 이해)
- 시간 사이의 차이와 연산
- 타이머/Ticker 와 동시성 조합
- 시간대(timezone) 다루기
28.1 time.Time
time.Time 은 “시간 위의 한 점” 을 나타내는 타입이다.
일상 언어로 말하면 “어떤 순간” 이다.
예를 들어 “2026년 1월 1일 오전 9시 0분 0초” 같은 값이
하나의 time.Time 이 된다.
내부에는 두 가지가 함께 들어 있다.
- 그 순간 자체 (몇 년 몇 월 며칠 몇 시…)
- 어떤 시간대(time zone) 기준인지
같은 순간이라도 시간대가 다르면 표시는 달라진다.
| 순간 | 한국(KST) | UTC |
|---|---|---|
| 같은 순간 | 09:00 | 00:00 |
time.Time 은 이 모든 정보를 함께 들고 있다.
비교나 빼기 같은 연산은 시간대와 무관하게 “같은 순간인지” 를 기준으로 동작한다.
28.2 현재 시각 / 만들기
현재 시각
now := time.Now()
fmt.Println(now)
// 2026-05-24 13:00:00.123 +0900 KST
time.Now() 는 시스템 시계의 현재 시각을 반환한다.
보통 가장 자주 쓰는 함수다.
임의 시각 만들기
time.Date 로 직접 만들 수 있다.
t := time.Date(
2026, time.May, 24,
13, 0, 0, 0,
time.Local,
)
인자는 순서대로 다음을 뜻한다.
| 인자 | 의미 |
|---|---|
| year | 연 |
| month | 월 (time.Month 상수) |
| day | 일 |
| hour | 시 (0~23) |
| min | 분 |
| sec | 초 |
| nsec | 나노초 |
| loc | 시간대 (*time.Location) |
월은 time.January 부터 time.December 까지의 상수를 쓴다.
1 같은 정수를 그대로 써도 동작한다 (time.Month(1)).
28.3 포맷팅과 파싱
Go 의 독특한 reference time
다른 언어는 보통 YYYY-MM-DD HH:mm:ss 같은 형식 문자열을 쓴다.
Go 는 좀 다르다.
기준 시각 자체를 형식 문자열로 쓴다.
기준 시각은 외워야 한다.
2006-01-02 15:04:05 -0700 MST
각 자리는 이런 의미다.
| 부분 | 의미 |
|---|---|
2006 | 연 |
01 | 월 |
02 | 일 |
15 | 시 (24시간) |
04 | 분 |
05 | 초 |
-0700 | 시간대 오프셋 |
MST | 시간대 이름 |
순서대로 외우면 1 2 3 4 5 6 -7.
“2006년 1월 2일 3시 4분 5초, -7 오프셋”
이라는 식으로 기억하면 좋다.
Format
t := time.Now()
t.Format("2006-01-02")
// "2026-05-24"
t.Format("2006/01/02 15:04")
// "2026/05/24 13:00"
t.Format("2006-01-02T15:04:05Z07:00")
// RFC3339 형식
기준 시각의 자리에 원하는 자리 표시자를 끼워 넣는 식이다.
Parse
문자열을 time.Time 으로 되돌리는 함수다.
t, err := time.Parse(
"2006-01-02",
"2026-05-24",
)
첫 번째 인자가 형식, 두 번째가 실제 값.
자주 쓰는 포맷 상수
표준 라이브러리가 자주 쓰는 형식을 상수로 제공한다.
time.RFC3339 // "2006-01-02T15:04:05Z07:00"
time.RFC1123 // "Mon, 02 Jan 2006 15:04:05 MST"
time.DateOnly // "2006-01-02"
time.TimeOnly // "15:04:05"
time.DateTime // "2006-01-02 15:04:05"
t.Format(time.RFC3339)
t.Format(time.DateTime)
한국 포맷 예시
t.Format("2006년 1월 2일 15시 04분")
// "2026년 5월 24일 13시 00분"
한글이 섞여도 문제없다. 자리 표시자 부분만 정확하면 된다.
28.4 시간 연산
time.Duration
“얼마만큼의 시간 길이” 를 나타내는 타입이다. 나노초 단위의 정수다.
time.Second // 1초
3 * time.Second // 3초
500 * time.Millisecond // 0.5초
time.Hour + 30*time.Minute // 1시간 30분
Duration 값을 그대로 출력하면 사람 친화적으로 나온다.
fmt.Println(2 * time.Hour) // "2h0m0s"
Add / Sub
t := time.Now()
later := t.Add(2 * time.Hour) // 2시간 뒤
diff := later.Sub(t) // Duration
Add(d)는Time을 반환Sub(other)는Duration을 반환
Before / After / Equal
t1.Before(t2) // t1 이 t2 보다 이전인가
t1.After(t2) // t1 이 t2 보다 이후인가
t1.Equal(t2) // 같은 순간인가
== 가 아니라 Equal 을 쓰는 게 안전하다.
시간대가 다른 같은 순간을 비교할 때
== 는 false 가 나올 수 있지만
Equal 은 true 를 돌려준다.
Since / Until
자주 쓰는 짧은 도우미다.
start := time.Now()
doWork()
elapsed := time.Since(start)
// Now() - start 와 같다
deadline := someTime
remaining := time.Until(deadline)
// deadline - Now() 와 같다
Since 는 “그 뒤로 얼마나 지났나”,
Until 은 “그때까지 얼마나 남았나”.
28.5 타이머와 Ticker
time.Sleep
가장 단순한 도구. 지정한 시간 동안 현재 고루틴을 멈춘다.
time.Sleep(2 * time.Second)
time.After
지정 시간 뒤에 값을 넣어 주는 채널을 반환한다.
select 에서 타임아웃을 만들 때 자주 쓴다.
select {
case msg := <-ch:
fmt.Println("받음:", msg)
case <-time.After(3 * time.Second):
fmt.Println("3초 안에 못 받음")
}
22장의 select 와 자연스럽게 결합한다.
time.NewTimer
한 번만 울리는 타이머가 필요하면 NewTimer 를 쓴다.
중간에 멈추거나 재설정할 수 있다는 점이 After 와 다르다.
t := time.NewTimer(5 * time.Second)
defer t.Stop()
select {
case <-t.C:
fmt.Println("5초 경과")
case <-cancel:
// 취소된 경우, Stop 으로 깔끔히 정리
}
time.NewTicker
주기적으로 신호를 보내는 도구. 1초마다, 10초마다 등의 정기 작업에 쓴다.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 3; i++ {
t := <-ticker.C
fmt.Println("틱:", t)
}
다 쓴 Ticker 는 반드시
Stop()한다. 안 그러면 내부 고루틴이 살아 있어 누수 원인이 된다.
9부 동시성과의 조합
타이머/Ticker 는 채널을 반환한다. 그래서 22~25장에서 본 도구들과 자연스럽게 결합한다.
ctx, cancel := context.WithTimeout(
context.Background(),
5 * time.Second,
)
defer cancel()
select {
case result := <-doWork():
handle(result)
case <-ctx.Done():
fmt.Println("타임아웃 또는 취소")
}
context.WithTimeout 내부도
사실은 타이머다.
28.6 시간대 (timezone)
time.Location 은 시간대를 표현한다.
표준 위치
| 값 | 의미 |
|---|---|
time.UTC | UTC |
time.Local | 현재 시스템의 로컬 시간대 |
임의 시간대 불러오기
loc, err := time.LoadLocation("Asia/Seoul")
if err != nil {
return err
}
t := time.Now().In(loc)
fmt.Println(t)
위치 이름은 IANA 데이터베이스 기준이다.
"UTC""Asia/Seoul""America/New_York""Europe/London"
In(loc) 은 같은 순간을 다른 시간대로 표시한다.
순간 자체는 안 바뀌고, 시간대만 바뀐다.
흔한 함정
같은 순간이라도 Format 결과는 시간대에 따라 다르다.
t := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
fmt.Println(t.Format(time.DateTime))
// "2026-01-01 00:00:00"
kst, _ := time.LoadLocation("Asia/Seoul")
fmt.Println(t.In(kst).Format(time.DateTime))
// "2026-01-01 09:00:00"
서버 로그가 UTC 인데 사용자 화면은 KST 로 보여 줘야 한다면
저장은 UTC, 표시할 때 In(loc) 으로 변환하는 게 무난한 관례다.
28.7 정리
time.Time은 시각 + 시간대를 함께 들고 다닌다time.Now()로 현재 시각,time.Date(...)로 임의 시각- 포맷팅/파싱은 reference time
2006-01-02 15:04:05를 외운다time.RFC3339,time.DateTime같은 상수도 활용
time.Duration으로 길이를 표현Add,Sub,Since,Until로 연산Before,After,Equal로 비교
- 타이머와 Ticker
time.Sleep은 단순 대기time.After는 select 타임아웃time.NewTicker는 주기 작업
- 시간대는
time.UTC,time.Local, 또는time.LoadLocation("Asia/Seoul")
시간은 단순해 보이지만 함정이 많다. “순간 자체” 와 “표시 방식” 을 머리 속에서 분리해 두면 헷갈릴 일이 줄어든다.
다음 장에서는 파일 입출력을 다룬다. 시간만큼이나 자주 쓰는 영역이다.