Go第三方库之Cast

好久没有更新了,最近一直忙着工作,还有毕业的事情。再加上最近心态上的一些变化,导致博客更新没有像之前在学校那样频繁了。现在趁着这篇文章,试着开始重新更新博客,记录自己的学习经历和一些心路历程。

之前在工作中,看见项目在其他类型转string的时候没有用Go的标准库Strconv,而是使用了cast包。点进去看了下源码发现还是挺简单的,这里简单记录一下。

go-cast

github cast

Go 每日一库之 cast

Quick Start

cast的使用非常简单:

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
33
34
35
36
37
package main

import (
"fmt"
"log"

"github.com/spf13/cast"
)

func main() {
// 1.ToType系列
// ToString
fmt.Println(cast.ToString("qingbo1011")) // qingbo1011
fmt.Println(cast.ToString(8)) // 8
fmt.Println(cast.ToString(8.31)) // 8.31
fmt.Println(cast.ToString([]byte("one time"))) // one time
fmt.Println(cast.ToString(nil)) // ""
var foo any = "one more time"
fmt.Println(cast.ToString(foo)) // one more time

// ToInt
fmt.Println(cast.ToInt(8)) // 8
fmt.Println(cast.ToInt(8.31)) // 8
fmt.Println(cast.ToInt("8")) // 8
fmt.Println(cast.ToInt(true)) // 1
fmt.Println(cast.ToInt(false)) // 0
var eight any = 8
fmt.Println(cast.ToInt(eight)) // 8
fmt.Println(cast.ToInt(nil)) // 0

// 2.ToTypeE系列
float64E, err := cast.ToFloat64E("abc")
if err != nil {
log.Panicln(err)
}
fmt.Println(float64E)
}

实际上,cast实现了多种常见类型之间的相互转换,返回最符合直觉的结果。例如:

  • nil转为string的结果为"",而不是nil
  • true转为string的结果为"true",而true转为int的结果为1
  • any转为其他类型,要看它里面存储的值类型

cast支持的类型转换包括所有的基本类型(整形、浮点型、布尔值和字符串)、空接口、nil,时间(time.Time)、时长(time.Duration)以及它们的切片类型,还有map[string]Type,其中Type为:

1
2
3
4
byte     bool      float32    float64    string  
int8 int16 int32 int64 int
uint8 uint16 uint32 uint64 uint
any time.Time time.Duration nil

cast提供了两个系列的转换:

  • ToType(其中Type可以为任何支持的类型),将参数转换为Type类型。如果无法转换,返回Type类型的零值或nil
  • ToTypeE以 E 结尾,返回转换后的值和一个error。这组函数可以区分参数中实际存储了零值,还是转换失败了。

实现上在cast源码中,ToType在内部调用ToTypeE函数,返回结果并忽略错误。ToType函数的实现在文件cast.go中,而ToTypeE函数的实现在文件caste.go中。

1
2
3
4
5
// ToInt casts an interface to an int type.
func ToInt(i interface{}) int {
v, _ := ToIntE(i)
return v
}

更多cast的用法和解析,查看cast源码即可。

源码分析

cast的源码还是很简单的。上面我们说了cast主要有两个系列的函数,其中ToType在内部调用ToTypeE函数,返回结果并忽略错误。阅读源码我们可以发现,ToType函数的实现在文件cast.go中,而ToTypeE函数的实现在文件caste.go中。

indirect函数

以ToInt & ToIntE为例。

cast.go:

1
2
3
4
5
// ToInt casts an interface to an int type.
func ToInt(i interface{}) int {
v, _ := ToIntE(i)
return v
}

caste.go:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ToIntE casts an interface to an int type.
func ToIntE(i interface{}) (int, error) {
i = indirect(i)

intv, ok := toInt(i)
if ok {
return intv, nil
}

switch s := i.(type) {
case int64:
return int(s), nil
case int32:
return int(s), nil
case int16:
return int(s), nil
case int8:
return int(s), nil
case uint:
return int(s), nil
case uint64:
return int(s), nil
case uint32:
return int(s), nil
case uint16:
return int(s), nil
case uint8:
return int(s), nil
case float64:
return int(s), nil
case float32:
return int(s), nil
case string:
v, err := strconv.ParseInt(trimZeroDecimal(s), 0, 0)
if err == nil {
return int(v), nil
}
return 0, fmt.Errorf("unable to cast %#v of type %T to int64", i, i)
case json.Number:
return ToIntE(string(s))
case bool:
if s {
return 1, nil
}
return 0, nil
case nil:
return 0, nil
default:
return 0, fmt.Errorf("unable to cast %#v of type %T to int", i, i)
}
}

观察源码可以发现,在断言之前,都会有i = indirect(i)这样的操作。

indirect函数函数功能:将参数中可能的指针去掉。如果类型本身不是指针,那么直接返回。否则返回指针指向的值。循环直到返回一个非指针的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// From html/template/content.go
// Copyright 2011 The Go Authors. All rights reserved.
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
func indirect(a interface{}) interface{} {
if a == nil {
return nil
}
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
// Avoid creating a reflect.Value if it's not a pointer.
return a
}
v := reflect.ValueOf(a)
for v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}
return v.Interface()
}

indirect函数处理含有嵌套指针的情况,将最终指向的非指针值返回。

看个例子:

1
2
3
4
5
6
7
8
9
func main() {
var p *int = new(int)
*p = 8
fmt.Println(cast.ToInt(p)) // 8

var pp **int
pp = &p
fmt.Println(cast.ToInt(pp)) // 8
}

时间和时长转换

ToTime

cast.go:

1
2
3
4
5
// ToTime casts an interface to a time.Time type.
func ToTime(i interface{}) time.Time {
v, _ := ToTimeE(i)
return v
}

caste.go:

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
33
34
35
36
37
38
// ToTimeE casts an interface to a time.Time type.
func ToTimeE(i interface{}) (tim time.Time, err error) {
return ToTimeInDefaultLocationE(i, time.UTC)
}

// ToTimeInDefaultLocationE casts an empty interface to time.Time,
// interpreting inputs without a timezone to be in the given location,
// or the local timezone if nil.
func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) {
i = indirect(i)

switch v := i.(type) {
case time.Time:
return v, nil
case string:
return StringToDateInDefaultLocation(v, location)
case json.Number:
s, err1 := ToInt64E(v)
if err1 != nil {
return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i)
}
return time.Unix(s, 0), nil
case int:
return time.Unix(int64(v), 0), nil
case int64:
return time.Unix(v, 0), nil
case int32:
return time.Unix(int64(v), 0), nil
case uint:
return time.Unix(int64(v), 0), nil
case uint64:
return time.Unix(int64(v), 0), nil
case uint32:
return time.Unix(int64(v), 0), nil
default:
return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i)
}
}

根据传入的类型执行不同的处理:

  • 如果是time.Time,直接返回

  • 如果是字符串,调用StringToDate函数依次尝试以下面这些时间格式调用time.Parse()解析该字符串。如果某个格式解析成功,则返回获得的time.Time。否则解析失败,返回错误,其他任何类型都无法转换为time.Time

    字符串转时间的具体代码:

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    ...
    case string:
    return StringToDateInDefaultLocation(v, location)
    ...

    var (
    timeFormats = []timeFormat{
    // Keep common formats at the top.
    {"2006-01-02", timeFormatNoTimezone},
    {time.RFC3339, timeFormatNumericTimezone},
    {"2006-01-02T15:04:05", timeFormatNoTimezone}, // iso8601 without timezone
    {time.RFC1123Z, timeFormatNumericTimezone},
    {time.RFC1123, timeFormatNamedTimezone},
    {time.RFC822Z, timeFormatNumericTimezone},
    {time.RFC822, timeFormatNamedTimezone},
    {time.RFC850, timeFormatNamedTimezone},
    {"2006-01-02 15:04:05.999999999 -0700 MST", timeFormatNumericAndNamedTimezone}, // Time.String()
    {"2006-01-02T15:04:05-0700", timeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon
    {"2006-01-02 15:04:05Z0700", timeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon
    {"2006-01-02 15:04:05", timeFormatNoTimezone},
    {time.ANSIC, timeFormatNoTimezone},
    {time.UnixDate, timeFormatNamedTimezone},
    {time.RubyDate, timeFormatNumericTimezone},
    {"2006-01-02 15:04:05Z07:00", timeFormatNumericTimezone},
    {"02 Jan 2006", timeFormatNoTimezone},
    {"2006-01-02 15:04:05 -07:00", timeFormatNumericTimezone},
    {"2006-01-02 15:04:05 -0700", timeFormatNumericTimezone},
    {time.Kitchen, timeFormatTimeOnly},
    {time.Stamp, timeFormatTimeOnly},
    {time.StampMilli, timeFormatTimeOnly},
    {time.StampMicro, timeFormatTimeOnly},
    {time.StampNano, timeFormatTimeOnly},
    }
    )

    // StringToDateInDefaultLocation casts an empty interface to a time.Time,
    // interpreting inputs without a timezone to be in the given location,
    // or the local timezone if nil.
    func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
    return parseDateWith(s, location, timeFormats)
    }

    func parseDateWith(s string, location *time.Location, formats []timeFormat) (d time.Time, e error) {

    for _, format := range formats {
    if d, e = time.Parse(format.format, s); e == nil {

    // Some time formats have a zone name, but no offset, so it gets
    // put in that zone name (not the default one passed in to us), but
    // without that zone's offset. So set the location manually.
    if format.typ <= timeFormatNamedTimezone {
    if location == nil {
    location = time.Local
    }
    year, month, day := d.Date()
    hour, min, sec := d.Clock()
    d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location)
    }

    return
    }
    }
    return d, fmt.Errorf("unable to parse date: %s", s)
    }
  • 如果是整型,将参数作为时间戳(自 UTC 时间1970.01.01 00:00:00到现在的秒数)调用time.Unix生成时间。Unix()函数接受两个参数,第一个参数指定秒,第二个参数指定纳秒

举个例子:

1
2
3
4
5
6
func main() {
day := cast.ToTime("2023-08-08")
time2 := cast.ToTime(1437101011)
fmt.Println(day) // 2023-08-08 00:00:00 +0000 UTC
fmt.Println(time2) // 2015-07-17 10:43:31 +0800 CST
}

ToDuration

cast.go:

1
2
3
4
5
// ToDuration casts an interface to a time.Duration type.
func ToDuration(i interface{}) time.Duration {
v, _ := ToDurationE(i)
return v
}

caste.go:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// ToTimeInDefaultLocationE casts an empty interface to time.Time,
// interpreting inputs without a timezone to be in the given location,
// or the local timezone if nil.
func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) {
i = indirect(i)

switch v := i.(type) {
case time.Time:
return v, nil
case string:
return StringToDateInDefaultLocation(v, location)
case json.Number:
s, err1 := ToInt64E(v)
if err1 != nil {
return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i)
}
return time.Unix(s, 0), nil
case int:
return time.Unix(int64(v), 0), nil
case int64:
return time.Unix(v, 0), nil
case int32:
return time.Unix(int64(v), 0), nil
case uint:
return time.Unix(int64(v), 0), nil
case uint64:
return time.Unix(int64(v), 0), nil
case uint32:
return time.Unix(int64(v), 0), nil
default:
return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i)
}
}

// StringToDateInDefaultLocation casts an empty interface to a time.Time,
// interpreting inputs without a timezone to be in the given location,
// or the local timezone if nil.
func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
return parseDateWith(s, location, timeFormats)
}

func parseDateWith(s string, location *time.Location, formats []timeFormat) (d time.Time, e error) {

for _, format := range formats {
if d, e = time.Parse(format.format, s); e == nil {

// Some time formats have a zone name, but no offset, so it gets
// put in that zone name (not the default one passed in to us), but
// without that zone's offset. So set the location manually.
if format.typ <= timeFormatNamedTimezone {
if location == nil {
location = time.Local
}
year, month, day := d.Date()
hour, min, sec := d.Clock()
d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location)
}

return
}
}
return d, fmt.Errorf("unable to parse date: %s", s)
}

根据传入的类型进行不同的处理:

  • 如果是time.Duration类型,直接返回;
  • 如果是整型或浮点型,将其数值强制转换为time.Duration类型,单位默认为ns;
  • 如果是字符串,分为两种情况:
    • 如果字符串中有时间单位符号nsuµmh,直接调用time.ParseDuration解析;
    • 否则在字符串后拼接ns再调用time.ParseDuration解析;
  • 其他类型解析失败。

实践推荐

  1. 涉及到时间的转换,最好使用Go标准库Strconv
  2. cast只是图方便使用,比如在cast.ToString(),如果确定两种类型的转换,最好使用Go标准库Strconv