我们继续使用上次学习单词格式转换 的目录
在cmd目录下新增文件time.go
在internal目录下新建timer文件夹,在timer文件夹下新增文件time.go
现在我们的目录是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Tools │ go.mod │ go.sum │ main.go │ ├─cmd │ root.go │ time.go │ word.go │ └─internal ├─timer │ time.go │ └─word word.go
定义与初始化命令行参数 先在/cmd/time.go目录下定义一下time命令timeCmd
以及两个子命令,nowTimeCmd
和calculateTimeCmd
,并将子命令添加到timeCmd
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var timeCmd = &cobra.Command{ Use: "time" , Short: "时间格式处理" , Long: "时间格式处理" , Run: func (cmd *cobra.Command, args []string ) {}, } var nowTimeCmd = &cobra.Command{ Use: "now" , Short: "获取当前时间" , Long: "获取当前时间" , Run: func (cmd *cobra.Command, args []string ) {}, } var calculateTimeCmd = &cobra.Command{ Use: "calc" , Short: "计算所需时间" , Long: "计算所需时间" , Run: func (cmd *cobra.Command, args []string ) {}, } func init () { timeCmd.AddCommand(nowTimeCmd) timeCmd.AddCommand(calculateTimeCmd) }
然后到cmd/root.go中的init参数中添加一下time命令标志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package cmdimport ( "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{}func Execute () error { return rootCmd.Execute() } func init () { rootCmd.AddCommand(wordCmd) rootCmd.AddCommand(timeCmd) }
获取当前时间 到internal/timer/time.go文件中增加如下一个方法
1 2 3 func GetNowTime () time.Time{ return time.Now() }
回到cmd/time.go中,修改nowTimeCmd
的Run方法,只用输出一下即可
1 2 3 4 5 6 7 8 9 var nowTimeCmd = &cobra.Command{ Use: "now" , Short: "获取当前时间" , Long: "获取当前时间" , Run: func (cmd *cobra.Command, args []string ) { nowTime := timer.GetNowTime() log.Printf("输出结果 %s, %d" ,nowTime.Format("2006-01-02 15:04:05" )) }, }
时间推算 这部分主要是做到时间的增加与减少
到internal/timer/time.go中新增如下方法
1 2 3 4 5 6 7 8 func GetCalculateTime (currentTimer time.Time, d string ) (time.Time, error ) { duration, err := time.ParseDuration(d) if err != nil { return time.Time{}, err } return currentTimer.Add(duration), nil }
在这个方法中,我们默认如果所给的duration格式无法解析的话,返回当前时间与错误信息
到cmd/time.go中修改calculateTimeCmd
的Run方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 var calculateTimeCmd = &cobra.Command{ Use: "calc" , Short: "计算所需时间" , Long: "计算所需时间" , Run: func (cmd *cobra.Command, args []string ) { var currentTimer time.Time var layout = "2006-01-02 15:04:05" if calculateTime == "" { currentTimer = timer.GetNowTime() } else { var err error colon := strings.Count(calculateTime, ":" ) if colon == 0 { layout = "2006-01-02" } if colon == 2 { layout = "2006-01-02 15:04:05" } currentTimer, err = time.Parse(layout, calculateTime) if err != nil { t, _ := strconv.Atoi(calculateTime) currentTimer = time.Unix(int64 (t), 0 ) } } t, err := timer.GetCalculateTime(currentTimer, duration) if err != nil { log.Fatalf("timer.GetCalculateTime err: %v" , err) } log.Printf("输出结果: %s, %d" , t.Format(layout), t.Unix()) }, }
我们通过判断:
数量来判断时间格式,这里仅仅判断了两种时间格式
time库支持如下时间格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const ( Layout = "01/02 03:04:05PM '06 -0700" 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" 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" RFC3339 = "2006-01-02T15:04:05Z07:00" RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" Kitchen = "3:04PM" 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" DateTime = "2006-01-02 15:04:05" DateOnly = "2006-01-02" TimeOnly = "15:04:05" )
可以像这样子使用这些预定义格式,例如:
1 t := time.Now().Format(time.RFC3339)
验证 1 2 3 4 5 6 7 8 go run main.go time now 输出结果: 2023 -02-26 13 :32 :55 , 1677389575 go run main.go time calc -c ="2029-09-04 12:02:33" -d =5 m 输出结果: 2029 -09-04 12 :07 :33 , 1883218053 go run main.go time calc -c ="2029-09-04 12:02:33" -d =-2h 输出结果: 2029 -09-04 10 :02 :33 , 1883210553
时区问题 如果把代码放到其他地方跑,可能会遇到少了八个小时的问题,其实就是时区出了问题。因此我们在使用时间相关函数时一定要关注时区问题
在标准库 time 上,提供了 Location 的两个实例:Local 和 UTC。Local 代表当前系统本地时区;UTC 代表通用协调时间,也就是零时区,在默认值上,标准库 time 使用的是 UTC 时区。
设置时区 这一部分基本上是复制粘贴原文的内容
标准库 time 中的 LoadLocation 方法来根据名称获取特定时区的 Location 实例,原型如下:
1 func LoadLocation (name string ) (*Location, error )
在该方法中,如果所传入的 name 是”UTC”或为空,返回 UTC;如果 name 是 “Local”,返回当前的本地时区 Local;否则 name 应该是 IANA 时区数据库(IANA Time Zone Database,简称 tzdata)里有记录的地点名(该数据库记录了地点和对应的时区),如 “America/New_York”。
那么为了保证我们所获取的时间,与我们所期望的时区一致,我们要对获取时间的代码进行修改,设置当前时区为 Asia/Shanghai
internal/timer/time.go
1 2 3 4 func GetNowTime () time.Time { location, _ := time.LoadLocation("Asia/Shanghai" ) return time.Now().In(location) }
在前面的实践代码中,我们用到了 time.Format 方法,与此还有一个相对应的方法并没有介绍到它,就是 time.Parse 方法,Parse 方法会解析格式化的字符串并返回它表示的时间值,它非常的常见,并且有一个非常需要注意的点。首先我们一起看看下面这个示例程序,如下:
1 2 3 4 5 6 7 8 9 func main () { location, _ := time.LoadLocation("Asia/Shanghai" ) inputTime := "2029-09-04 12:02:33" layout := "2006-01-02 15:04:05" t, _ := time.Parse(layout, inputTime) dateTime := time.Unix(t.Unix(), 0 ).In(location).Format(layout) log.Printf("输入时间:%s,输出时间:%s" , inputTime, dateTime) }
那么你觉得这个示例程序的输出时间的结果是什么呢,还是 2029-09-04 12:02:33 吗,我们一起来看看最终的输出结果,如下:
1 输入时间:2029-09-04 12:02:33,输出时间:2029-09-04 20:02:33
从输出结果上来看,输入和输出时间竟然相差了八个小时,这显然是时区的设置问题,但是这里你可能又打起了嘀咕,明明在调用 Format 方法前我们已经设置了时区…这究竟是为什么呢?
实际上这与 Parse 方法有直接关系,因为 Parse 方法会尝试在入参的参数中中分析并读取时区信息,但是如果入参的参数没有指定时区信息的话,那么就会默认使用 UTC 时间。因此在这种情况下我们要采用 ParseInLocation 方法,指定时区就可以解决这个问题,如下:
1 2 t, _ := time.ParseInLocation(layout, inputTime, location) dateTime := time.Unix(t.Unix(), 0 ).In(location).Format(layout)
也就是所有解析与格式化的操作都最好指定时区信息,否则当你遇到时区问题的时候,并且已经上线,那么后期再进行数据清洗就比较麻烦了。
为什么是 2006-01-02 15:04:05 实际上,2006-01-02 15:04:05 是一个参考时间的格式,也就是其它语言中 Y-m-d H:i:s
格式,在功能上用于时间的格式化处理,这个我们在前面章节中已经进行过验证。
那么为什么要用 2006-01-02 15:04:05 呢,其实这些”数字“是有意义的,在 Go 语言中强调必须显示参考时间的格式,因此每个布局字符串都是一个时间戳的表示,并非随便写的时间点,如果你觉得记忆困难,可参见官方例子中的如下方式:
1 2 Jan 2 15:04:05 2006 MST 1 2 3 4 5 6 -7
而转换到 2006-01-02 15:04:05 的时间格式,我们也可以将其记忆为 2006 年 1 月 2 日 3 点 4 分 5 秒。