返回

Cast Introduction

Go第三方库之类型转换cast介绍。


介绍

今天我们再来介绍Steve Francia(spf13)大神的另一个库castcast是一个小巧、实用的类型转换库,用于将一个类型转为另一个类型。它提供了一套高效且安全的类型转换功能。 最初开发cast是用在hugo中的。


快速使用

先安装:

1
$ go get -u github.com/spf13/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
package main

import (
    "fmt"

    "github.com/spf13/cast"
)

func main() {
    // ToString
    fmt.Println(cast.ToString("apple"))            // apple
    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 interface{} = "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 interface{} = 8
    fmt.Println(cast.ToInt(eight)) // 8
    fmt.Println(cast.ToInt(nil))   // 0
}

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

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

这些类型包括所有的基本类型(整形、浮点型、布尔值和字符串)、空接口、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
interface{}   time.Time  time.Duration   nil

基本使用

cast提供了两组函数:

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

源码分析:

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// cast/cast.go
func ToBool(i interface{}) bool {
  v, _ := ToBoolE(i)
  return v
}

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

ToTypeE函数都接受任意类型的参数(interface{}),然后使用类型断言根据具体的类型来执行不同的转换。如果无法转换,返回错误。

 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
// cast/caste.go
// ToBoolE casts an interface to a bool type.
func ToBoolE(i interface{}) (bool, error) {
    i = indirect(i)

    switch b := i.(type) {
    case bool:
       return b, nil
    case nil:
       return false, nil
    case int:
       return b != 0, nil
    case int64:
       return b != 0, nil
    case int32:
       return b != 0, nil
    case int16:
       return b != 0, nil
    case int8:
       return b != 0, nil
    case uint:
       return b != 0, nil
    case uint64:
       return b != 0, nil
    case uint32:
       return b != 0, nil
    case uint16:
       return b != 0, nil
    case uint8:
       return b != 0, nil
    case float64:
       return b != 0, nil
    case float32:
       return b != 0, nil
    case time.Duration:
       return b != 0, nil
    case string:
       return strconv.ParseBool(i.(string))
    case json.Number:
       v, err := ToInt64E(b)
       if err == nil {
          return v != 0, nil
       }
       return false, fmt.Errorf("unable to cast %#v of type %T to bool", i, i)
    default:
       return false, fmt.Errorf("unable to cast %#v of type %T to bool", i, i)
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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()
}

所以,下面代码输出都是 8:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"

    "github.com/spf13/cast"
)

func main() {
    /*
       与make的区别:
          new创建指向该类型零值的指针
          make创建指定的类型,并分配内存,用与引用类型
    */
    p := new(int)
    *p = 8
    fmt.Println(cast.ToInt(p)) // 8

    pp := &p
    fmt.Println(cast.ToInt(pp)) // 8
}

时间和时长转换

时间类型的转换代码如下:

 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
// cast/caste.go
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,直接返回;
  • 如果是整型,将参数作为时间戳(自 UTC 时间1970.01.01 00:00:00到现在的秒数)调用time.Unix生成时间。Unix接受两个参数,第一个参数指定秒,第二个参数指定纳秒;
  • 如果是字符串,调用StringToDateInDefaultLocation函数依次尝试以下面这些时间格式调用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
// cast/caste.go
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},
	}
)


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)
}

时长类型的转换代码如下:

 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
// cast/caste.go
func ToDurationE(i interface{}) (d time.Duration, err error) {
    i = indirect(i)

    switch s := i.(type) {
    case time.Duration:
       return s, nil
    case int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8:
       d = time.Duration(ToInt64(s))
       return
    case float32, float64:
       d = time.Duration(ToFloat64(s))
       return
    case string:
       if strings.ContainsAny(s, "nsuµmh") {
          d, err = time.ParseDuration(s)
       } else {
          d, err = time.ParseDuration(s + "ns")
       }
       return
    case json.Number:
       var v float64
       v, err = s.Float64()
       d = time.Duration(v)
       return
    default:
       err = fmt.Errorf("unable to cast %#v of type %T to Duration", i, i)
       return
    }
}

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

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

时间、时长示例:

 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
package main

import (
    "fmt"
    "time"

    "github.com/spf13/cast"
)

func main() {
    now := time.Now()
    timestamp := 1579615973
    timeStr := "2020-01-21 22:13:48"

    fmt.Println(cast.ToTime(now))       // 2020-01-22 06:31:50.5068465 +0800 CST m=+0.000997701
    fmt.Println(cast.ToTime(timestamp)) // 2020-01-21 22:12:53 +0800 CST
    fmt.Println(cast.ToTime(timeStr))   // 2020-01-21 22:13:48 +0000 UTC

    d, _ := time.ParseDuration("1m30s")
    ns := 30000
    strWithS := "130s"
    strWithoutNs := "130"

    fmt.Println(cast.ToDuration(d))            // 1m30s
    fmt.Println(cast.ToDuration(ns))           // 30µs
    fmt.Println(cast.ToDuration(strWithS))     // 2m10s
    fmt.Println(cast.ToDuration(strWithoutNs)) // 130ns
}

转换为切片

实际上,这些函数的实现基本类似。使用类型断言判断类型。如果就是要返回的类型,直接返回。否则根据类型进行相应的转换。

我们主要分析两个实现:ToIntSliceEToStringSliceEToBoolSliceE/ToDurationSliceEToIntSliceE基本相同。

首先是ToIntSliceE

 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

func ToIntSliceE(i interface{}) ([]int, error) {
    if i == nil {
       return []int{}, fmt.Errorf("unable to cast %#v of type %T to []int", i, i)
    }

    switch v := i.(type) {
    case []int:
       return v, nil
    }

    kind := reflect.TypeOf(i).Kind()
    switch kind {
    case reflect.Slice, reflect.Array:
       s := reflect.ValueOf(i)
       a := make([]int, s.Len())
       for j := 0; j < s.Len(); j++ {
          val, err := ToIntE(s.Index(j).Interface())
          if err != nil {
             return []int{}, fmt.Errorf("unable to cast %#v of type %T to []int", i, i)
          }
          a[j] = val
       }
       return a, nil
    default:
       return []int{}, fmt.Errorf("unable to cast %#v of type %T to []int", i, i)
    }
}

根据传入参数的类型:

  • 如果是nil,直接返回错误;
  • 如果是[]int,不用转换,直接返回;
  • 如果传入类型为切片数组,新建一个[]int,将切片或数组中的每个元素转为int放到该[]int中。最后返回这个[]int
  • 其他情况,不能转换。

ToStringSliceE

 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
func ToStringSliceE(i interface{}) ([]string, error) {
    var a []string

    switch v := i.(type) {
    case []interface{}:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []string:
       return v, nil
    case []int8:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []int:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []int32:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []int64:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []float32:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case []float64:
       for _, u := range v {
          a = append(a, ToString(u))
       }
       return a, nil
    case string:
       return strings.Fields(v), nil
    case []error:
       for _, err := range i.([]error) {
          a = append(a, err.Error())
       }
       return a, nil
    case interface{}:
       str, err := ToStringE(v)
       if err != nil {
          return a, fmt.Errorf("unable to cast %#v of type %T to []string", i, i)
       }
       return []string{str}, nil
    default:
       return a, fmt.Errorf("unable to cast %#v of type %T to []string", i, i)
    }
}

根据传入的参数类型:

  • 如果是[]interface{},将该参数中每个元素转为string,返回结果切片;
  • 如果是[]string,不需要转换,直接返回;
  • 如果是interface{},将参数转为string,返回只包含这个值的切片;
  • 如果是string,调用strings.Fields函数按空白符将参数拆分,返回拆分后的字符串切片;
  • 其他情况,不能转换。

转换为切片示例:

 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
package main

import (
    "fmt"

    "github.com/spf13/cast"
)

func main() {
    sliceOfInt := []int{1, 3, 7}
    arrayOfInt := [3]int{8, 12}
    // ToIntSlice
    fmt.Println(cast.ToIntSlice(sliceOfInt)) // [1 3 7]
    fmt.Println(cast.ToIntSlice(arrayOfInt)) // [8 12 0]

    sliceOfInterface := []interface{}{1, 2.0, "apple"}
    sliceOfString := []string{"abc", "dj", "banana"}
    stringFields := " abc  def hij   hah"
    common := interface{}(37)
    // ToStringSliceE
    fmt.Println(cast.ToStringSlice(sliceOfInterface)) // [1 2 apple]
    fmt.Println(cast.ToStringSlice(sliceOfString))
    toStringFields := cast.ToStringSlice(stringFields)
    fmt.Println(toStringFields, len(toStringFields)) // [abc dj banana hah] 4                    // [abc def hij]
    fmt.Println(cast.ToStringSlice(common))          // [37]

    // ToToDurationSlice
    stringDurationSlice := []string{"1m23s", "22h"}
    intDurationArray := [3]int{222222222, 88383838888}
    fmt.Println(cast.ToDurationSlice(stringDurationSlice)) // [1m23s 22h0m0s]
    fmt.Println(cast.ToDurationSlice(intDurationArray))    // [222.222222ms 1m28.383838888s 0s]

    //
    stringBoolSlice := []string{"true", "false", "1", "0", "T"}
    intBoolArray := [3]int{1, 0, 22}
    fmt.Println(cast.ToBoolSlice(stringBoolSlice)) // /*[true false true false true]*/
    fmt.Println(cast.ToBoolSlice(intBoolArray))    // [true false true]
}

转为map[string]Type类型

cast库能将传入的参数转为map[string]Type类型,Type为上面支持的类型。

其实只需要分析一个ToStringMapStringE函数就可以了,其他的实现基本一样。ToStringMapStringE返回map[string]string类型的值。

 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
func ToStringMapStringE(i interface{}) (map[string]string, error) {
    var m = map[string]string{}

    switch v := i.(type) {
    case map[string]string:
       return v, nil
    case map[string]interface{}:
       for k, val := range v {
          m[ToString(k)] = ToString(val)
       }
       return m, nil
    case map[interface{}]string:
       for k, val := range v {
          m[ToString(k)] = ToString(val)
       }
       return m, nil
    case map[interface{}]interface{}:
       for k, val := range v {
          m[ToString(k)] = ToString(val)
       }
       return m, nil
    case string:
       err := jsonStringToObject(v, &m)
       return m, err
    default:
       return m, fmt.Errorf("unable to cast %#v of type %T to map[string]string", i, i)
    }
}

根据传入的参数类型:

  • 如果是map[string]string,不用转换,直接返回;
  • 如果是map[string]interface{},将每个值转为string存入新的 map,最后返回新的 map;
  • 如果是map[interface{}]string,将每个键转为string存入新的 map,最后返回新的 map;
  • 如果是map[interface{}]interface{},将每个键和值都转为string存入新的 map,最后返回新的 map;
  • 如果是string类型,cast将它看成一个 JSON 串,解析这个 JSON 到map[string]string,然后返回结果
  • 其他情况,返回错误。

转换为映射示例:

 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
package main

import (
    "fmt"

    "github.com/spf13/cast"
)

func main() {
    m1 := map[string]string{
       "name": "apple",
       "job":  "developer",
    }

    m2 := map[string]interface{}{
       "name": "banana",
       "age":  18,
    }

    m3 := map[interface{}]string{
       "name": "orange",
       "job":  "designer",
    }

    m4 := map[any]interface{}{
       "name": "strawberry",
       "age":  29,
       18:     "hi",
    }

    jsonStr := `{"name":"bibi", "job":"manager"}`

    fmt.Println(cast.ToStringMapString(m1))      // map[job:developer name:apple]
    fmt.Println(cast.ToStringMapString(m2))      // map[age:18 name:banana]
    fmt.Println(cast.ToStringMapString(m3))      // map[job:designer name:orange]
    fmt.Println(cast.ToStringMapString(m4))      // map[18:hi age:29 name:strawberry]
    fmt.Println(cast.ToStringMapString(jsonStr)) // map[job:manager name:bibi]
}

总结

cast库能在几乎所有常见类型之间转换,使用非常方便。代码量也很小,有时间建议读读源码。常用于解析配置数据。


参考

  1. cast GitHub 仓库
  2. 原作者:Go 每日一库之 cast