프로그래밍에서 시간은 꽤 빈번히 다루는 영역이다. 가령 디비에 저장된 시간을 타임존마다 다른 값으로 보여줘야 하는 경우다. 모든 언어단에서 이를 지원하지만 자바스크립트 경우 moment.js 따위의 써드파티 라이브러리를 더 애용하는 편이다. 반면 Go언어는 time 패키지를 기본으로 제공하는데 이것만으로도 원하는 기능을 비교적 쉽게 구현할 수 있다.

Time 타입

time 패키지는 시간을 표현하기 위한 Time 타입을 제공한다. 이것은 나노초 단위의 정밀도를 가진 구조체인데 아래 세 개 프로퍼티를 가진다.

type Time struct {
    wall uint64
    ext  int64
    loc *Location
}

Location 포인터인 loc는 타임존 정보를 표현한다. 시간을 말해주는 wall과 (wall clock) 경과 시간을 측정하기 위한 ext (monotonic clock) 속성을 가지고 있는데, Go 문서를 읽어 봐도 정확한 쓰임새는 모르겠다. 그렇지만 패키지를 사용하는데 지장은 없었다.

Time을 생성하는 방법은 세 가지다. Now()는 현재 시간 기준으로 인스턴스를 만든다. 년, 월, 일 등 특정 시점 기준으로 생성하려면 Date() 함수를 사용한다. 마지막으로 Unix() 함수를 이용하면 유닉스 타임을 인자로 사용할 수 있다.

func Now() Time
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time
func Unix(sec int64, nsec int64) Time

인자 값의 종류에 따라 이들 중 하나를 사용하면 되겠다.

fmt.Println(time.Now())      // 2019-01-12 10:37:50.195069 +0900 KST m=+0.000298257
fmt.Println(time.Unix(0, 0)) // 1970-01-01 09:00:00 +0900 KST

Date() 함수 인자 중 MonthLocation 타입이 눈에 띄는데, 월과 타임존을 의미한다.

type Month int

const (
   January Month = 1 + iota
   February
   // 생략...
)

var months = [...]string{
   "January",
   "February",
   // 생략...
}

// String 은 월의 영어 이름을 반환한다.
func (m Month) String() string

time 패키지에 월을 나타내는 상수 January, February, …가 정의되어 있고 1부터 순차적으로 증가한다(자바스크립트에서는 0부터 시작하는 것이 차이). Month를 문자열로 변환하는 String() 메소드는 months 슬라이스에 담아둔 월의 영어 이름을 반환한다.

타임존을 표현하는 Location 구조체도 있는데 세계표준시를 나타내는 UTC는 Location 포인터 타입으로 미리 정의해 두었다.

// UTC represents Universal Coordinated Time (UTC).
var UTC *Location = &utcLoc

Month와 Location은 Date() 함수로 Time 값을 만들 때 인자로 전달한다.

t := time.Date(1970, 1, time.January, 0, 0, 0, 0, time.UTC)
fmt.Println(t) // 1970-01-01 00:00:00 +0000 UTC

Time 데이터 조회

이렇게 생성한 Time 인스턴스는 시간 조회를 위한 여러가지 메소드를 제공한다.

t := time.Now()

fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
  // 2019 January 12 1 2 3

fmt.Println(t.Weekday(), t.Unix(), t.Nanosecond(), t.UnixNano())
  // Saturday 1547254923 138851000 1547254923138851000

년, 월, 일을 조회하기 위한 Year(), Month(), Day()가 있고, 시, 분, 초 조회를 위한 Hour(), Minute(), Second() 메소드가 있다. Weekday()를 호출하면 영어 요일 이름을 얻을 수 있다. 유닉스 시간의 초와 나노초는 Unix(), Nanosecond() 메소드로 조회하고, UnixNano()는 이 둘을 붙여서 반환한다.

출력 포맷

시간을 다양한 포맷의 문자열로 변경해야 하는 경우가 있다. Format() 메소드를 사용하면 YYYY-mm-DD HH:MM:SS 같은 형식의 문자열을 얻을 수 있다.

func (t Time) Format(layout string) string

출력형식을 표현하는 layout 문자열을 전달하면 Format() 함수는 이 형식에 맞게 문자열을 만든다.

s := time.Now().Format("2006-01-02 15:04:05")
fmt.Println(s) // 2019-01-12 10:20:30

레이아웃 형식이 다소 생소하다.

“2006-01-02 15:04:05”

이 글에서는 date 명령어 결과의 순서라고 설명한다. date 명령어를 실행하면 다음과 같은 결과를 보여준다.

$ LANG=en_US date
Sat Jan 12 11:49:15 KST 2019

요일, 월 순으로 번호를 매기면 아래와 같다.

Mon Jan 2 15:04:05 -0700 MST 2006
0   1   2  3  4  5              6

이러한 규칙에 따라 다양한 레이아웃 문자열을 패키지 내 상수로 제공한다.

const (
   ANSIC       = "Mon Jan _2 15:04:05 2006"
   UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
   RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
   RFC822      = "02 Jan 06 15:04 MST"
   RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
   RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
   RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
   RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
   RFC3339     = "2006-01-02T15:04:05Z07:00"
   RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
   Kitchen     = "3:04PM"
   // Handy time stamps.
   Stamp      = "Jan _2 15:04:05"
   StampMilli = "Jan _2 15:04:05.000"
   StampMicro = "Jan _2 15:04:05.000000"
   StampNano  = "Jan _2 15:04:05.000000000"
)

이 상수값을 인자로 사용한다.

fmt.Println(t.Format(time.RFC3339)) // 2019-01-12T01:02:03Z

문자열 파싱

레이아웃 문자열은 출력 뿐만아니라 파싱할 때도 사용한다. 문자열을 Time으로 변환하는 Parse() 함수 시그니처를 보자.

func Parse(layout, value string) (Time, error)

layout 문자열과 파싱할 문자열 value를 전달하면 Time을 반환하다. 만약 파싱할 문자열이 layout과 일치하지 않을 경우 error를 반환한다.

s := "2019-01-12 12:30:00"
t, _ := time.Parse("2006-01-02 15:04:05", s)
fmt.Println(t) // 2019-01-12 12:30:00 +0000 UTC

타임존

개발할 때 utc와 kst를 동시에 다루기 때문에 타임존 변경이 빈번하다. 가령 디비에 저장된 utc 시간 문자열을 Time으로 파싱하고, 이것을 kst 기준으로 변경하는 경우다. time 패키지에는 타임존 정보를 표현한 Location 타입이 있다. LoadLocation()은 타임존 문자열을 이용해 Location 값을 만드는 함수다.

func LoadLocation(name string) (*Location, error)

Time 인스턴스는 Location을 인자로 받는 In() 메소드가 있는데, 타임존 기준으로 변경된 새로운 Time을 반환한다.

func (t Time) In(loc *Location) Time

이 둘을 이용해 UTC 시간 문자열을 KST 기준의 Time으로 변경해 보자.

s := "2019-01-12 00:00:00.000"

layout := "2006-01-02 15:04:05.000"
utc, _ := time.Parse(layout, s)

loc, _ := time.LoadLocation("Asia/Seoul")
kst := utc.In(loc)

fmt.Println(utc) // 2019-01-12 00:00:00 +0000 UTC
fmt.Println(kst) // 2019-01-12 09:00:00 +0900 KST

Time의 UTC() 메소드를 이용하면 간편하게 utc로 변경된 Time을 얻을 수 있다.

fmt.Println(kst.UTC()) // 2019-01-12 00:00:00 +0000 UTC

시간 문자열 파싱과 타임존 설정을 한 번에 처리하려면 ParseInLocation() 함수를 사용한다.

func ParseInLocation(layout, value string, loc *Location) (Time, error)

기간, 계산, 비교

기간을 나타내는 Duration 시그니처는 다음과 같다.

type Duration int64

나노초 단위 정수 값을 사용할 수도 있지만 아래 상수를 이용하는 것이 더 낫다.

const (
   Nanosecond  Duration = 1
   Microsecond          = 1000 * Nanosecond
   Millisecond          = 1000 * Microsecond
   Second               = 1000 * Millisecond
   Minute               = 60 * Second
   Hour                 = 60 * Minute
)

Duration은 시간 계산시 사용되는데 Time 구조체의 Add(), Sub() 같은 메소드의 인자로 사용된다.

t := time.Date(2019, 1, 12, 0, 0, 0, 0, time.UTC)
afterTenSec := t.Add(time.Second * 10)
afterTenMin := t.Add(time.Minute * 10)
afterTenHour := t.Add(time.Hour * 10)

fmt.Println(afterTenSec)  // 2019-01-12 00:00:10 +0000 UTC
fmt.Println(afterTenMin)  // 2019-01-12 00:10:00 +0000 UTC
fmt.Println(afterTenHour) // 2019-01-12 10:00:00 +0000 UTC

두 개 Time 값을 비교할 때는 불리언 값을 반환하는 Before(), After(), Equal()을 사용한다.

fmt.Println(t.Before(afterTenSec)) // true
fmt.Println(t.After(afterTenSec))  // false
fmt.Println(t.Equal(afterTenSec))  // false

Timer와 Ticker

자바스크립트에 setTimeout(), setInterval()이 있는 것 처럼 Go언어도 Timer와 Ticker 를 제공한다. Timer 타입의 시그니처부터 확인해 보자.

type Timer struct {
   C <-chan Time
   // ...
}

Time을 전달하는 채널 C를 속성으로 갖는다. 설정한 시간이 경과된 뒤 이 채널로 알리는 방식이다.

타이머를 생성하려면 NewTimer() 함수를 사용한다.

fmt.Println("Start main")

t1 := time.NewTimer(time.Second * 2)
<-t1.C
fmt.Println("Timer1 expired")

t2 := time.NewTimer(time.Second * 1)
go func() {
   <-t2.C
   fmt.Println("Timer2 expired")
}()
s2 := t2.Stop()
if s2 {
   fmt.Println("Timer2 stopped")
}

time.Sleep(time.Second * 3)
fmt.Println("End main")

<-t1.C 는 2초 후 Time을 수신할 때까지 함수 실행을 지연한다. 2초 후에 “Timer1 expired” 문자열이 터미널에 출력된다. t2는 1초 후에 동작하는 타이머다. 그 아래 정의한 고루틴에서 통지를 받고 “Timer2 expired”를 출력하도록 했다. 하지만 t2.Stop()으로 타이머를 정지했기 때문에 로그는 출력되지 않는다. 마지막에 Sleep() 함수는 실행을 취소 할 수 없다는 것이 Timer와 다른 점이다.

Timer가 미래에 한 번을 위한 것이라면, Ticker 타입은 반복적으로 사용하기 위함이다. Timer와 같은 시그니쳐다.

type Ticker struct {
   C <-chan Time // The channel on which the ticks are delivered.
   // ...
}

NewTicker() 함수로 인스턴스를 생성한 뒤 1초마다 통지하는 코드를 만들어 보자.

ticker := time.NewTicker(time.Second * 1)
go func() {
   for t := range ticker.C {
      fmt.Println(t)
   }
}()

time.Sleep(time.Second * 3)
ticker.Stop()

Ticker에서 1초 마다 채널 C로 Time 값을 받을 수 있는데 for을 이용해 순서대로 처리한다. 여기서는 3초간 함수 실행을 지연한 뒤 ticker.Stop()으로 중단했으므로 2번 출력된다.

2019-01-12 20:35:55.415154 +0900 KST m=+1.000586341
2019-01-12 20:35:56.417435 +0900 KST m=+2.002878280

정리

Time을 생성하려면 인자 형식에 따라 Now(), Date(), Unix() 함수를 사용하고, 문자열을 파싱해서 만들려면 Parse() 함수를 이용한다.

반대로 Format()은 Time을 문자열로 변경할 수 있다.

LocaLocation(), In(), ParseInLocation() 을 이용하면 타임존을 쉽게 변경할 수 있다.

Duration 타입과 Add(), Sub() 메소드를 이용해 시간을 계산한다. Before(), After(), Equal() 메소드는 두 Time간의 값을 비교할 수 있다.

미래 특정 시간에 한 번 동작하는 로직은 Timer, 주기적인 동작은 Tikcer를 사용한다.