33

Go 每日一库之 cast

 5 years ago
source link: https://studygolang.com/articles/26433
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

简介

今天我们再来介绍 spf13 大神的另一个库 castcast 是一个小巧、实用的类型转换库,用于将一个类型转为另一个类型。

最初开发 cast 是用在 hugo 中的。

快速使用

先安装:

$ go get github.com/spf13/cast

后使用:

package main

import (
  "fmt"

  "github.com/spf13/cast"
)

func main() {
  // ToString
  fmt.Println(cast.ToString("leedarjun"))        // leedarjun
  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 为前面提到的类型):

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 中。

// 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{} ),然后使用类型断言根据具体的类型来执行不同的转换。如果无法转换,返回错误。

// cast/caste.go
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:
    if i.(int) != 0 {
      return true, nil
    }
    return false, nil
  case string:
    return strconv.ParseBool(i.(string))
  default:
    return false, fmt.Errorf("unable to cast %#v of type %T to bool", i, i)
  }
}

首先调用 indirect 函数将参数中可能的指针去掉。如果类型本身不是指针,那么直接返回。否则返回指针指向的值。

循环直到返回一个非指针的值:

// cast/caste.go
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:

package main

import (
    "fmt"

    "github.com/spf13/cast"
)

func main() {
  p := new(int)
  *p = 8
  fmt.Println(cast.ToInt(p))   // 8

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

时间和时长转换

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

func ToTimeE(i interface{}) (tim time.Time, err error) {
  i = indirect(i)

  switch v := i.(type) {
  case time.Time:
    return v, nil
  case string:
    return StringToDate(v)
  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 接受两个参数,第一个参数指定秒,第二个参数指定纳秒;
  • 如果是字符串,调用 StringToDate 函数依次尝试以下面这些时间格式调用 time.Parse 解析该字符串。如果某个格式解析成功,则返回获得的 time.Time 。否则解析失败,返回错误;
  • 其他任何类型都无法转换为 time.Time

字符串转换为时间:

// cast/caste.go
func StringToDate(s string) (time.Time, error) {
  return parseDateWith(s, []string{
    time.RFC3339,
    "2006-01-02T15:04:05", // iso8601 without timezone
    time.RFC1123Z,
    time.RFC1123,
    time.RFC822Z,
    time.RFC822,
    time.RFC850,
    time.ANSIC,
    time.UnixDate,
    time.RubyDate,
    "2006-01-02 15:04:05.999999999 -0700 MST", // Time.String()
    "2006-01-02",
    "02 Jan 2006",
    "2006-01-02T15:04:05-0700", // RFC3339 without timezone hh:mm colon
    "2006-01-02 15:04:05 -07:00",
    "2006-01-02 15:04:05 -0700",
    "2006-01-02 15:04:05Z07:00", // RFC3339 without T
    "2006-01-02 15:04:05Z0700",  // RFC3339 without T or timezone hh:mm colon
    "2006-01-02 15:04:05",
    time.Kitchen,
    time.Stamp,
    time.StampMilli,
    time.StampMicro,
    time.StampNano,
  })
}

func parseDateWith(s string, dates []string) (d time.Time, e error) {
  for _, dateType := range dates {
    if d, e = time.Parse(dateType, s); e == nil {
      return
    }
  }
  return d, fmt.Errorf("unable to parse date: %s", s)
}

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

// 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
  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 解析;
  • 其他类型解析失败。

示例:

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
  strWithUnit := "130s"
  strWithoutUnit := "130"

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

转换为切片

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

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

首先是 ToIntSliceE

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

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 string:
    return strings.Fields(v), 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 函数按空白符将参数拆分,返回拆分后的字符串切片;
  • 其他情况,不能转换。

示例:

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, "darjun"}
  sliceOfString := []string{"abc", "dj", "pipi"}
  stringFields := " abc  def hij   "
  any := interface{}(37)
  // ToStringSliceE
  fmt.Println(cast.ToStringSlice(sliceOfInterface))  // [1 2 darjun]
  fmt.Println(cast.ToStringSlice(sliceOfString))     // [abc dj pipi]
  fmt.Println(cast.ToStringSlice(stringFields))      // [abc def hij]
  fmt.Println(cast.ToStringSlice(any))               // [37]
}

转为 map[string]Type 类型

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

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

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 ,然后返回结果
  • 其他情况,返回错误。

示例:

package main

import (
  "fmt"

  "github.com/spf13/cast"
)

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

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

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

  m4 := map[interface{}]interface{} {
    "name": "did",
    "age": 29,
  }

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

  fmt.Println(cast.ToStringMapString(m1))      // map[job:developer name:darjun]
  fmt.Println(cast.ToStringMapString(m2))      // map[age:18 name:jingwen] 
  fmt.Println(cast.ToStringMapString(m3))      // map[job:designer name:pipi]
  fmt.Println(cast.ToStringMapString(m4))      // map[job:designer name:pipi]
  fmt.Println(cast.ToStringMapString(jsonStr)) // map[job:manager name:bibi]
}

总结

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

完整示例代码在 GitHub 上。

ps: 本以为春节就这么几天,偷个懒,没想到。。。

参考

  1. cast GitHub 仓库

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

bMFFVjA.jpg!web

本文由博客一文多发平台 OpenWrite 发布!


Recommend

  • 32
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 go-homedir

    简介 今天我们来看一个很小,很实用的库 go-homedir 。顾名思义, go-homedir 用来获取用户的主目录。 实际上,使用标准库 os/user

  • 46
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 go-ini

    简介 ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。 go-ini 是 Go 语言中用于操作 ini 文件的第三方库。 本文...

  • 23
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 cobra

    简介 cobra 是一个命令行程序库,可以用来编写命令行程序。同时,它也提供了一个脚手架, 用于生成基于 cobra 的应用程序框架。非常多知名的开源项目使用了 cobra 库...

  • 34
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 viper

    简介 上一篇文章介绍 cobra 的时候提到了 viper ,今天我们就来介绍一下这个库。 viper 是一...

  • 29
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 fsnotify

    简介 上一篇文章 Go 每日一库之 viper 中,我们介绍了 viper 可以监听文件修改进而自动重新加载。 其内部使用的就是 fsnotify 这...

  • 39
    • segmentfault.com 5 years ago
    • Cache

    Go 每日一库之 log

    简介 在日常开发中,日志是必不可少的功能。虽然有时可以用 fmt 库输出一些信息,但是灵活性不够。Go 标准库提供了一个日志库 log 。本文介绍 log 库的使用。 快速使用

  • 23
    • segmentfault.com 5 years ago
    • Cache

    Go 每日一库之 logrus

    简介 前一篇文章 介绍了 Go 标准库中的日志库 log 。最后我们也提到, log 库只提供了三组接口,功能过于简单了。 ...

  • 42
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 carbon

    简介 一线开发人员每天都要使用日期和时间相关的功能,各种定时器,活动时间处理等。标准库 time 使用起来不太灵活,特别是日期时间的创建和运算。

  • 32
    • segmentfault.com 5 years ago
    • Cache

    Go 每日一库之 email

    简介 程序中时常有发送邮件的需求。有异常情况了需要通知管理员和负责人,用户下单后可能需要通知订单信息,电商平台、中国移动和联通都有每月账单,这些都可以通过邮件来推送。还有我们平时收到的垃圾邮件大都也是通过这种方...

  • 24
    • studygolang.com 5 years ago
    • Cache

    Go 每日一库之 dig

    简介 今天我们来介绍 Go 语言的一个依赖注入(DI)库—— dig 。dig 是 uber 开源的库。Java 依赖注入的库有很多,相信即使不是做 Java 开发的童鞋也听过大名鼎鼎的 Spring。相比...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK