前言

任何语言处理时间的功能都是最基础也是平时十分常用的,另外需要注意任何脱离时区的时间都是没有任何意义的!

这里总结一下笔者在这一个多月写go项目用到的、收集到的一些好用的处理时间的方法以及时间处理的一些基础知识点。

golang时间操作基础 ***

go关于时间操作的基础我这边自己做了一下笔记:

常用的方法 ***

初始化时区的方法

package time_module

import "time"

var TIME_LOCATION *time.Location

func init() {
    var err error
    TIME_LOCATION, err = time.LoadLocation("Asia/Shanghai")
    if err != nil {
        panic(err)
    }
}

// 当前时区的当前时间
func GetCurrentTime() time.Time {
    return time.Now().In(TIME_LOCATION)
}

封装的一些简单方法 

package time_module

import (
    "github.com/jinzhu/now"
    "time"
)

/*
    时间比较与加减:注意先转换到同一个时区!在底层方法中将时间转换到同一个时区
*/

// 2个日期是否相等(注意先转到同一个时区)
func DateEqual(date1, date2 time.Time) bool {
    date1 = date1.In(TIME_LOCATION)
    date2 = date2.In(TIME_LOCATION)
    y1, m1, d1 := date1.Date()
    y2, m2, d2 := date2.Date()
    return y1 == y2 && m1 == m2 && d1 == d2
}

// 2个日期的前后 先转到同一时区
func DateAfter(date1, date2 time.Time) bool {
    // 在同一个时区比较
    date1 = date1.In(TIME_LOCATION)
    date2 = date2.In(TIME_LOCATION)
    return date1.After(date2)
}

// 计算2个时间相差的天数 先转到同一个时区
// 注意时间前后:t1 需要比 t2 大!如果t1比t2小得出的结果会是一个负数!
func TimeSubDays(t1, t2 time.Time) int {
    // 转换成同一个时区去比较!
    t1 = t1.In(TIME_LOCATION)
    t2 = t2.In(TIME_LOCATION)

    t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, TIME_LOCATION)
    t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, TIME_LOCATION)
    return int(t1.Sub(t2).Hours() / 24)
}

// 获取当前时间距离当天结束还有多少秒 —— redis中设置过期key常用
// 需要导入第三方包:github.com/jinzhu/now
func GetEndOfDayRemainSeconds() time.Duration {
    currentTime := time.Now().In(TIME_LOCATION)
    return time.Second * time.Duration(now.New(currentTime).EndOfDay().Sub(currentTime).Seconds())
}

计算2个时间相差的天数的测试

package scripts_stroage

import (
    "fmt"
    "testing"
    "time"
)

var TimeLocation *time.Location

// 包初始化的时候初始化时区!!!
func init() {
    var err error
    TimeLocation, err = time.LoadLocation("Asia/Shanghai")
    if err != nil {
        panic(err)
    }
}

// 计算两个时间相差的天数 TODO 注意时间前后:t1 需要比 t2 大!如果t1比t2小得出的结果会是一个负数!!!
func TimeSubDays(t1, t2 time.Time) int {
    // 转换成同一个时区去比较!
    t1 = t1.In(TimeLocation)
    t2 = t2.In(TimeLocation)

    fmt.Println("T1>>> ", t1)
    fmt.Println("T2>>> ", t2)

    t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, TimeLocation)
    t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, TimeLocation)
    return int(t1.Sub(t2).Hours() / 24)
}

func TestTimeSubDays(t *testing.T) {

    // 1、用东8区时区创建时间,结果是1
    t1 := time.Date(2018, 1, 10, 0, 0, 1, 100, time.Local)
    t2 := time.Date(2018, 1, 9, 23, 59, 22, 100, time.Local)

    fmt.Println("t1: ", t1)
    fmt.Println("t2: ", t2)
    // 即使相差1秒,相差也是1天
    println("1>>> ", TimeSubDays(t1, t2))

    // TODO 2、用UTC时区创造2个时间注意一下~
    // TODO 是因为TimeSubDays方法中的In方法将2个时间统一转换成东8区的时间区处理了!这2个时间转换成东8区时间确实是同一天!
    t3 := time.Date(2017, 1, 10, 0, 0, 1, 100, time.UTC)
    t4 := time.Date(2017, 1, 9, 23, 59, 22, 100, time.UTC)
    fmt.Println("t3: ", t3)
    fmt.Println("t4: ", t4)
    println("2>>> ", TimeSubDays(t3, t4))

}

封装好的一些方法合集(使用上面初始化的时区) ******

package time_module

import (
    "fmt"
    "strconv"
    "strings"
    "time"
)


// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
    return time.Now().UnixNano() / int64(time.Millisecond)
}

func GetMillisStr() string {
    return fmt.Sprint(GetMillis())
}

// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
    return thisTime.UnixNano() / int64(time.Millisecond)
}

func GetSecondsForMillisecond(millisecond int64) int64 {
    return millisecond / int64(time.Microsecond)
}

// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format
func PadDateStringZeros(dateString string) string {
    parts := strings.Split(dateString, "-")
    for index, part := range parts {
        if len(part) == 1 {
            parts[index] = "0" + part
        }
    }
    dateString = strings.Join(parts[:], "-")
    return dateString
}

// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day
func GetStartOfDayMillis(thisTime time.Time, loc *time.Location) int64 {
    resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, loc)
    return GetMillisForTime(resultTime)
}

// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day
func GetEndOfDayMillis(thisTime time.Time, loc *time.Location) int64 {
    resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, loc)
    return GetMillisForTime(resultTime)
}

// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day
func GetStartOfMonthMillis(thisTime time.Time, loc *time.Location) int64 {
    resultTime := time.Date(thisTime.Year(), thisTime.Month(), 1, 0, 0, 0, 0, loc)
    return GetMillisForTime(resultTime)
}

// GetEndOfMonthMillis is a convenience method to get milliseconds since epoch for provided date's end of day
func GetStartOfNextMonthMillis(thisTime time.Time, loc *time.Location) int64 {
    resultTime := time.Date(thisTime.Year(), thisTime.Month(), 1, 0, 0, 0, 0, loc)
    resultTime = resultTime.AddDate(0, 1, 0)
    return GetMillisForTime(resultTime)
}

// 下一个月开始1号的时间戳
func GetStartOfNextMonthMillisByTimeStamp(timeStamp int64) int64 {
    tm := time.Unix(0, timeStamp*int64(1000*1000)).In(TIME_LOCATION)
    return GetStartOfNextMonthMillis(tm, TIME_LOCATION)
}

func GetStartOfMonthMillisByTimeStamp(timeStamp int64) int64 {
    tm := time.Unix(0, timeStamp*int64(1000*1000)).In(TIME_LOCATION)
    return GetStartOfMonthMillis(tm, TIME_LOCATION)
}

// 当前月份开始1号的时间戳
func GetDataStorePartitionByTimeStamp(timeStamp int64) string {
    tm := time.Unix(0, timeStamp*int64(1000*1000)).In(TIME_LOCATION)
    return fmt.Sprintf("p%d", GetStartOfMonthMillis(tm, TIME_LOCATION))
}

// 按东八区解析当前date, format,返回时间戳
func GetTimeStampsFromDateCST(date, format string) (int64, string) {
    date = PadDateStringZeros(date)
    resultTime, err := time.ParseInLocation(format, date, TIME_LOCATION)
    if err != nil {
        return 0, "Model.GetTimeStampsFromDateCST"
    }
    return GetMillisForTime(resultTime), ""
}

func GetDateCSTFromTimeStamp(timeStamp int64, format string) string {
    tm := time.Unix(0, timeStamp*int64(1000*1000)).In(TIME_LOCATION)
    return tm.Format(format)
}

func GetDateCSTFromTimeStampStr(timeStamp string, format string) string {
    ts, err := strconv.ParseInt(timeStamp, 10, 64)
    if err != nil {
        return "NAN"
    }
    tm := time.Unix(0, ts*int64(1000*1000)).In(TIME_LOCATION)
    return tm.Format(format)
}

func WeekStart(year, week int) time.Time {
    // Start from the middle of the year:
    t := time.Date(year, 7, 1, 0, 0, 0, 0, TIME_LOCATION)

    // Roll back to Monday:
    if wd := t.Weekday(); wd == time.Sunday {
        t = t.AddDate(0, 0, -6)
    } else {
        t = t.AddDate(0, 0, -int(wd)+1)
    }

    // Difference in weeks:
    _, w := t.ISOWeek()
    t = t.AddDate(0, 0, (week-w)*7)

    return t
}

func WeekRange(year, week int) (start, end time.Time) {
    start = WeekStart(year, week)
    end = start.AddDate(0, 0, 6)
    return
}

func WeekRangeFormat(year, week int, format string) string {
    start, end := WeekRange(year, week)
    return start.Format(format) + "~" + end.Format(format)
}

根据开始的时间戳返回一段时间的方法 ***

有时候我们会拿到一个时间点作为开始时间,而这个时间还有可能是个时间戳,这时候需要将时间戳转成时间(注意时区),而开始时间一般只有年月日,我们还需要做一些中间处理,我这里写了一个方法供大家参考:

var TIME_LOCATION_CST *time.Location

// 时区默认为东八区!
func init() {
    TIME_LOCATION_CST, _ = time.LoadLocation("Asia/Shanghai")
}

// 根据月与日是单位数还是双数返回不同的字符串
func getDateStr(month time.Month, day int) (monthStr, dayStr string) {
    if month < 10 {
        monthStr = fmt.Sprintf("0%d", month)
    } else {
        monthStr = fmt.Sprintf("%d", month)
    }
    if day < 10 {
        dayStr = fmt.Sprintf("0%d", day)
    } else {
        dayStr = fmt.Sprintf("%d", day)
    }
    return monthStr, dayStr
}

// 根据开始的时间戳获取开始与结束的日期 —— 默认东八区
func GetQueryTimeFromTimeStamp(StartTimeStamp int64) (queryStart, queryEnd time.Time, appError *AppError) {
    // TODO 注意:时间戳是 毫秒 级别的,所以下面要除以1000
    // 1、开始时间
    createTime := time.Unix(StartTimeStamp/1000, 0)
    // 先转字符串再转成年月日的格式
    year, month, day := createTime.Date()
    // 如果月与日是单位数,需要在前面加上0!否则下面parse的时候可能会报错!
    monthStr, dayStr := getDateStr(month, day)
    startTimeStr := fmt.Sprintf("%d-%s-%s", year, monthStr, dayStr)
    if ret1, err := time.ParseInLocation("2006-01-02", startTimeStr, TIME_LOCATION_CST); err == nil {
        queryStart = ret1
    } else {
        appError = NewAppError("GetQueryTimeFromTimeStamp", "time.ParseInLocation.error", err.Error(), nil)
        return ret1, ret1, appError
    }
    // 2、结束时间 TODO:注意这里返回 "明天" 的日期,返回后根据自己的实际情况做AddDate操作!
    nowTime := time.Now()
    endTime := nowTime.AddDate(0, 0, 1)
    year, month, day = endTime.Date()
    // 如果月与日是单位数,需要在前面加上0!
    monthStr, dayStr = getDateStr(month, day)
    endTImeStr := fmt.Sprintf("%d-%s-%s", year, monthStr, dayStr)
    if ret2, err := time.ParseInLocation("2006-01-02", endTImeStr, TIME_LOCATION_CST); err == nil {
        queryEnd = ret2
    } else {
        appError = NewAppError("GetQueryTimeFromTimeStamp", "time.ParseInLocation.error", err.Error(), nil)
        return ret2, ret2, appError
    }
    return queryStart, queryEnd, nil
}

根据开始与结束日期以5天为间隔返回每一个时间段的开始与结束日期 ***

还需要用到上面的 GetQueryTimeFromTimeStamp 方法,在此基础上获取这个时间段内以5天为间隔的开始与结束日期的组合:

func (s *SnapchatAccountHistoryAdCostTask) getTimeRange(createTimeStamp int64) ([]map[string]string, *model.AppError) {
    var timeRangeLst []map[string]string
    // 根据账号创建的时间戳获取开始与结束的日期 结束时间是"明天"
    startTime, endTime, err := model.GetQueryTimeFromTimeStamp(createTimeStamp)
    if err != nil {
        return nil, err
    }
    fmt.Printf("startTime: %v, endTime: %v \n", startTime, endTime)
    // startTime: 2020-08-21 00:00:00 +0800 CST, endTime: 2020-12-23 00:00:00 +0800 CST
    // 算一下两个时间差几天
    timeDUration := endTime.Sub(startTime)
    fmt.Println("相差的小时数... ", timeDUration.Hours()) // 2976
    // 小时数转天数 肯定是整数,因为开始与结束日期都是0点
    days := int(int64(timeDUration.Hours()) / 24) 
    fmt.Println("相差的天数... ", days)  // 124
    // 构建返回的列表
    // 按照每5天一组组合数据 每一组的最后一个日期不算作统计的日期
    for i := 0; i < days; i += 5 {
        currStartDate := startTime.AddDate(0, 0, i)
        currEndDate := startTime.AddDate(0, 0, i+5)
        // 如果 currEndDate超过了 "明天" 那最后的结束日期就用明天
        if currEndDate.After(endTime) {
            currEndDate = endTime
        }
        currStartStr := currStartDate.Format("2006-01-02")
        currEndStr := currEndDate.Format("2006-01-02")
        currMap := map[string]string{
            "startDate": currStartStr,
            "endDate":   currEndStr,
        }
        timeRangeLst = append(timeRangeLst, currMap)
    }
    return timeRangeLst, nil
}

返回的结果如下(注意返回的结果每个元素是字典):

timeRangeLst>>>  
map[endDate:2020-11-04 startDate:2020-10-30]
map[endDate:2020-11-09 startDate:2020-11-04]
map[endDate:2020-11-14 startDate:2020-11-09] map[endDate:2020-11-19 startDate:2020-11-14] map[endDate:2020-11-24 startDate:2020-11-19] map[endDate:2020-11-29 startDate:2020-11-24] map[endDate:2020-12-04 startDate:2020-11-29] map[endDate:2020-12-09 startDate:2020-12-04] map[endDate:2020-12-14 startDate:2020-12-09]
map[endDate:2020-12-19 startDate:2020-12-14]
map[endDate:2020-12-23 startDate:2020-12-19]]

根据开始时间戳获取一个开始到结束日期的时间字符串切片 ***

还需要用到上面的 GetQueryTimeFromTimeStamp 方法,写一个死循环即可实现: 

// 根据账号的创建时间戳得到一个包含开始到结束日期的字符串切片
func (f *FacebookAccountHistoryAdCostTask) getDateSlice(createTimeStamp int64) ([]string, *model.AppError) {
    var dateSlice []string
    // 根据账号的创建时间获取开始与结束日期的0点
    startTime, endTime, err := model.GetQueryTimeFromTimeStamp(createTimeStamp)
    if err != nil {
        return dateSlice, err
    }
    // 这里获取到的endTime是"明天",Facebook渠道的endTime是今天
    endTime = endTime.AddDate(0, 0, -1)
    mlog.Info(fmt.Sprintf("startTime: %v \n, endTime: %v \n", startTime, endTime))
    // startTime: 2019-05-16 00:00:00 +0800 CST , endTime: 2020-12-24 00:00:00 +0800 CST
    // 得到一个从开始时间到结束时间字符串的列表
    for {
        if startTime.After(endTime){
            break
        }
        startString := startTime.Format("2006-01-02")
        dateSlice = append(dateSlice, startString)
        startTime = startTime.AddDate(0, 0, 1)
    }
    return dateSlice, nil
}

结果如下:

[2020-11-13 
2020-11-14 2020-11-15 2020-11-16 2020-11-17 2020-11-18 2020-11-19 2020-11-20 2020-11-21 2020-11-22 2020-11-23 2020-11-24 2020-11-25 2020-11-26 2020-11-27 2020-11-28 2020-11-29 2020-11-30 2020-12-01 2020-12-02 2020-12-03 2020-12-04 2020-12-05 2020-12-06 2020-12-07 2020-12-08 2020-12-09 2020-12-10 2020-12-11 2020-12-12 2020-12-13 2020-12-14 2020-12-15 2020-12-16 2020-12-17 2020-12-18 2020-12-19 2020-12-20 2020-12-21 2020-12-22 2020-12-23
2020-12-24]

根据上一步的时间切片获取分割的时间切片

根据开始与结束日期获取一个开始到结束日期的时间切片:

package t9

import (
    "fmt"
    "testing"
    "time"
)

var TIME_LOCATION_CST, _ = time.LoadLocation("Asia/Shanghai")

// 根据开始与结束日期获取一个包含开始到结束日期字符串的切片
func GetDateSlice(startTime, endTime time.Time) (dateSlice []string) {
    for {
        if startTime.After(endTime) {
            break
        }
        startStr := startTime.Format("2006-01-02")
        dateSlice = append(dateSlice, startStr)
        startTime = startTime.AddDate(0, 0, 1)
    }
    return
}

func TestRange(t *testing.T) {

    startTime := time.Date(2020, 01, 01, 00, 00, 00, 00, TIME_LOCATION_CST)
    endTime := time.Date(2020, 02, 03, 00, 00, 00, 00, TIME_LOCATION_CST)

    timeRangeLst := GetDateSlice(startTime, endTime)
    fmt.Println("timeRangeLst>>> ", timeRangeLst)
    /*
        [2020-01-01 2020-01-02 2020-01-03 2020-01-04 2020-01-05 2020-01-06 2020-01-07 2020-01-08 2020-01-09 
         2020-01-10 2020-01-11 2020-01-12 2020-01-13 2020-01-14 2020-01-15
         2020-01-16 2020-01-17 2020-01-18 2020-01-19 2020-01-20 2020-01-21 2020-01-22 2020-01-23 2020-01-24 
         2020-01-25 2020-01-26 2020-01-27 2020-01-28 2020-01-29 2020-01-30 2020-01-31 2020-02-01 2020-02-02 2020-02-03]
    */

把它里面的字符串按照每4个一组整理成切片套切片的格式:

// 根据存时间字符串的切片获取一个按照每4天分组的新切片
func getNewDateSlice(dateSlice []string) [][]string {
    var retSlice [][]string
    length := len(dateSlice)
    // 分组
    for i := 0; i < length; i += 4 {
        var currSlice []string
        if i+4 > length{
            currSlice = dateSlice[i : length]
        }else{
            currSlice = dateSlice[i : i+4]
        }
        retSlice = append(retSlice, currSlice)
    }
    return retSlice
}

结果如下:

[[2020-01-01 2020-01-02 2020-01-03 2020-01-04] 
[2020-01-05 2020-01-06 2020-01-07 2020-01-08]
[2020-01-09 2020-01-10 2020-01-11 2020-01-12]
[2020-01-13 2020-01-14 2020-01-15 2020-01-16]
[2020-01-17 2020-01-18 2020-01-19 2020-01-20]
[2020-01-21 2020-01-22 2020-01-23 2020-01-24]
[2020-01-25 2020-01-26 2020-01-27 2020-01-28]
[2020-01-29 2020-01-30 2020-01-31 2020-02-01]
[2020-02-02 2020-02-03]]

下一个时间的开始是上一个时间的结尾的切片嵌套的构建方法:

package t9

import (
    "fmt"
    "testing"
    "time"
)

var TIME_LOCATION_CST, _ = time.LoadLocation("Asia/Shanghai")

// 根据开始与结束日期获取一个包含开始到结束日期字符串的切片
func GetDateSlice(startTime, endTime time.Time) (dateSlice []string) {
    for {
        if startTime.After(endTime) {
            break
        }
        startStr := startTime.Format("2006-01-02")
        dateSlice = append(dateSlice, startStr)
        startTime = startTime.AddDate(0, 0, 1)
    }
    return
}

// 整理下dateSlice的格式:下一个列表的第一个元素是上一个列表最后一个元素
func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func getNewDateSlice(dateSlice []string) (newDateSlice [][]string) {
    for i := 1; i < len(dateSlice); i += 5 {
        currSlice := dateSlice[i-1 : MinInt(i+5, len(dateSlice))]
        newDateSlice = append(newDateSlice, currSlice)
    }
    return
}

func TestRange(t *testing.T) {

    startTime := time.Date(2020, 01, 01, 00, 00, 00, 00, TIME_LOCATION_CST)
    endTime := time.Date(2020, 02, 03, 00, 00, 00, 00, TIME_LOCATION_CST)

    timeRangeLst := GetDateSlice(startTime, endTime)
    fmt.Println("timeRangeLst>>> ", timeRangeLst)
    /*
            [2020-01-01 2020-01-02 2020-01-03 2020-01-04 2020-01-05 2020-01-06 2020-01-07 2020-01-08 2020-01-09
             2020-01-10 2020-01-11 2020-01-12 2020-01-13 2020-01-14 2020-01-15
             2020-01-16 2020-01-17 2020-01-18 2020-01-19 2020-01-20 2020-01-21 2020-01-22 2020-01-23 2020-01-24
             2020-01-25 2020-01-26 2020-01-27 2020-01-28 2020-01-29 2020-01-30 2020-01-31 2020-02-01 2020-02-02 2020-02-03]
    */

    ret := getNewDateSlice(timeRangeLst)
    fmt.Println("ret>>> ", ret)
    /*
            [[2020-01-01 2020-01-02 2020-01-03 2020-01-04 2020-01-05 2020-01-06]
            [2020-01-06 2020-01-07 2020-01-08 2020-01-09 2020-01-10 2020-01-11]
            [2020-01-11 2020-01-12 2020-01-13 2020-01-14 2020-01-15 2020-01-16]
            [2020-01-16 2020-01-17 2020-01-18 2020-01-19 2020-01-20 2020-01-21]
            [2020-01-21 2020-01-22 2020-01-23 2020-01-24 2020-01-25 2020-01-26]
            [2020-01-26 2020-01-27 2020-01-28 2020-01-29 2020-01-30 2020-01-31]
            [2020-01-31 2020-02-01 2020-02-02 2020-02-03]]

    */

}

~~~