Golang 任意类型切片互转

2023-10-12 15:21:38 浏览数 (2)

1.切片转换

日常开发我们可能需要一个类型的切片转换为另一个类型的切片。

比如 int[] 转为 []string。

代码语言:javascript复制
import "strconv"

func IntSliceToStrSlice(s []int) []string {
	if s == nil {
		return nil
	}
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = strconv.Itoa(v)
	}
	return r
}

再如 []float64 转为 []string。

代码语言:javascript复制
func Float64SliceToStrSlice(s []float64) []string {
	if s == nil {
		return nil
	}
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = strconv.FormatFloat(v, 'f', -1, 64)
	}
	return r
}

上面的两个例子,除了元素类型转换的实现不同,其他的代码都是重复的。如果为多种不同类型切片互转都实现各自的转换函数,无疑是低效繁琐的。

2.反射

实际上,利用 Golang 反射,可以为目标类型切片的转换只写一个函数。比如,可以接收任意类型切片,将其转换为 []string。

代码语言:javascript复制
// ToStrSliceE converts an any type value to a []string with returned error.
func ToStrSliceE(a any) ([]string, error) {
	if a == nil {
		return nil, nil
	}

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

	// If a is a slice.
	kind := reflect.TypeOf(a).Kind()
	if kind == reflect.Slice || kind == reflect.Array {
		sl := reflect.ValueOf(a)
		s := make([]string, sl.Len())
		for i := 0; i < sl.Len(); i   {
			v, err := ToStringE(sl.Index(i).Interface())
			if err != nil {
				return nil, err
			}
			s[i] = v
		}
		return s, nil
	}
	return nil, fmt.Errorf("unable to cast %#v of type %T to []string", a, a)
}

其中 ToStringE 是一个将任意类型转换为 string 的函数,其实现如下:

代码语言:javascript复制
// ToStringE casts any type to a string type.
func ToStringE(i any) (string, error) {
	i = indirectToStringerOrError(i)

	switch s := i.(type) {
	case string:
		return s, nil
	case bool:
		return strconv.FormatBool(s), nil
	case int:
		return strconv.Itoa(s), nil
	case int64:
		return strconv.FormatInt(s, 10), nil
	case int32:
		return strconv.Itoa(int(s)), nil
	case int16:
		return strconv.FormatInt(int64(s), 10), nil
	case int8:
		return strconv.FormatInt(int64(s), 10), nil
	case uint:
		return strconv.FormatUint(uint64(s), 10), nil
	case uint64:
		return strconv.FormatUint(uint64(s), 10), nil
	case uint32:
		return strconv.FormatUint(uint64(s), 10), nil
	case uint16:
		return strconv.FormatUint(uint64(s), 10), nil
	case uint8:
		return strconv.FormatUint(uint64(s), 10), nil
	case float64:
		return strconv.FormatFloat(s, 'f', -1, 64), nil
	case float32:
		return strconv.FormatFloat(float64(s), 'f', -1, 32), nil
	case json.Number:
		return s.String(), nil
	case []byte:
		return string(s), nil
	case template.HTML:
		return string(s), nil
	case template.HTMLAttr:
		return string(s), nil
	case template.URL:
		return string(s), nil
	case template.JS:
		return string(s), nil
	case template.JSStr:
		return string(s), nil
	case template.CSS:
		return string(s), nil
	case template.Srcset:
		return string(s), nil
	case nil:
		return "", nil
	case fmt.Stringer:
		return s.String(), nil
	case error:
		return s.Error(), nil
	default:
		return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
	}
}

使用反射,可以避免为不同类型转为指定类型切片书写多个相似的转换函数,一定程度上实现了泛化。但其也有不足之处。

  • 反射涉及到运行时类型的判断,有一定性能开销,性能会降低 20% 左右。
  • 如果转换为不同的目标类型切片,仍需要编写多个不同的转换函数。

3.泛型

Golang 在 1.18 中引入了泛型。利用泛型,我们可以不用针对不同的目标类型切片单独实现转换函数,真正做到一个函数,完成所有类型切片间的转换。

代码语言:javascript复制
// ToSliceE converts any type slice or array to the specified type slice.
// An error will be returned if an error occurred.
func ToSliceE[T any](a any) ([]T, error) {
	if a == nil {
		return nil, nil
	}
	switch v := a.(type) {
	case []T:
		return v, nil
	case string:
		return ToSliceE[T](strings.Fields(v))
	}

	kind := reflect.TypeOf(a).Kind()
	switch kind {
	case reflect.Slice, reflect.Array:
		// If input is a slice or array.
		v := reflect.ValueOf(a)
		if kind == reflect.Slice && v.IsNil() {
			return nil, nil
		}
		s := make([]T, v.Len())
		for i := 0; i < v.Len(); i   {
			val, err := ToAnyE[T](v.Index(i).Interface())
			if err != nil {
				return nil, err
			}
			s[i] = val
		}
		return s, nil
	default:
		// If input is a single value.
		v, err := ToAnyE[T](a)
		if err != nil {
			return nil, err
		}
		return []T{v}, nil
	}
}

如果不关注错误,可以忽略。

代码语言:javascript复制
// ToSlice converts an any type slice to the specified type slice.
func ToSlice[E, T any, S ~[]E](s S) []T {
	r, _ := ToSliceE[E, T](s)
	return r
}

注意,不同类型元素之间的转换,依赖 ToAnyE 函数,其实现如下:

代码语言:javascript复制
// ToAnyE converts one type to another and returns an error if error occurred.
func ToAnyE[T any](a any) (T, error) {
	var t T
	switch any(t).(type) {
	case bool:
		v, err := ToBoolE(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case int:
		v, err := ToIntE(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case int8:
		v, err := ToInt8E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case int16:
		v, err := ToInt16E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case int32:
		v, err := ToInt32E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case int64:
		v, err := ToInt64E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case uint:
		v, err := ToUintE(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case uint8:
		v, err := ToUint8E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case uint16:
		v, err := ToUint16E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case uint32:
		v, err := ToUint32E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case uint64:
		v, err := ToUint64E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case float32:
		v, err := ToFloat32E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case float64:
		v, err := ToFloat64E(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	case string:
		v, err := ToStringE(a)
		if err != nil {
			return t, err
		}
		t = any(v).(T)
	default:
		return t, fmt.Errorf("the type %T isn't supported", t)
	}
	return t, nil
}

将不同类型的元素转换为不同目标类型时,仍需要单独实现其转换函数,因为这里的转换逻辑是不同的。

具体类型值之间的转换函数就不贴了,详见 dablelv/cyan。

4.dablelv/cyan

为了方便大家使用,以上相关代码已开源至 Github 工具库 dablelv/cyan,大家可 import 后使用,欢迎大家 star 和 pr。

代码语言:javascript复制
import (
	"fmt"
	"github.com/dablelv/cyan/conv"
)

func main() {
	s1 := []int{1, 2, 3}
	s2 := []float64{1.1, 1.2, 1.30}
	s3 := []string{"1", "2", "3.00"}
	fmt.Println(conv.ToSliceE[string](s1))
	fmt.Println(conv.ToSliceE[string](s2))
	fmt.Println(conv.ToSliceE[int](s3))
	// and so on...
}

运行输出:

代码语言:javascript复制
[1 2 3] <nil>
[1.1 1.2 1.3] <nil>
[1 2 3] <nil>

参考文献

github.com/dablelv/cyan

0 人点赞