Golang 切片转集合

1.什么是 Set 类型

我们都知道 Golang 没有集合(Set)类型,为何如此设计呢?

因为 Golang 是一门追求简单、现代、易于使用的语言,所以不会引入不必要的特性。

Set 就是一个例子,Golang 有了 Map,当 Map 中的 value 为空结构体 struct{} 时,其不就是一个集合(Set)么,所以不用再单独引入一个 Set 类型。

2.Slice to Set

有了集合,在某些场景下,我们可能需要完成切片到集合类型的转换。

Golang 中,利用反射,我们可以将任意类型的切片或数组转换为对应类型的集合。

// toSetE converts a slice or array to map[any]struct{} and returns an error if occurred.
func toSetE(i any) (any, error) {
	// Check params.
	if i == nil {
		return nil, fmt.Errorf("the input i is nil")
	}
	t := reflect.TypeOf(i)
	kind := t.Kind()
	if kind != reflect.Slice && kind != reflect.Array {
		return nil, fmt.Errorf("the input %#v of type %T isn't a slice or array", i, i)
	}

	// Execute the conversion.
	v := reflect.ValueOf(i)
	mT := reflect.MapOf(t.Elem(), reflect.TypeOf(struct{}{}))
	mV := reflect.MakeMapWithSize(mT, v.Len())
	for j := 0; j < v.Len(); j++ {
		mV.SetMapIndex(v.Index(j), reflect.ValueOf(struct{}{}))
	}
	return mV.Interface(), nil
}

参考上面的函数,我们可以封装我们想要的不同类型的转换函数。

转换成 int 集合。

// ToIntSet converts a slice or array to map[int]struct{}.
func ToIntSet(i any) map[int]struct{} {
	m, _ := ToIntSetE(i)
	return m
}

// ToIntSetE converts a slice or array to map[int]struct{Set} with error.
func ToIntSetE(i any) (map[int]struct{}, error) {
	m, err := toSetE(i)
	if err != nil {
		return nil, err
	}
	if v, ok := m.(map[int]struct{}); ok {
		return v, nil
	}
	dst := make(map[int]struct{}, reflect.ValueOf(m).Len())
	for _, k := range reflect.ValueOf(m).MapKeys() {
		v, err := conv.ToIntE(k.Interface())
		if err != nil {
			return nil, err
		}
		dst[v] = struct{}{}
	}
	return dst, nil
}

转换成 float64 集合。

// ToFloat64Set converts a slice or array to map[float64]struct{}.
func ToFloat64Set(i any) map[float64]struct{} {
	m, _ := ToFloat64SetE(i)
	return m
}

// ToFloat64SetE converts a slice or array to map[float64]struct{} and returns an error if occurred.
func ToFloat64SetE(i any) (map[float64]struct{}, error) {
	m, err := toSetE(i)
	if err != nil {
		return nil, err
	}
	if v, ok := m.(map[float64]struct{}); ok {
		return v, nil
	}
	dst := make(map[float64]struct{}, reflect.ValueOf(m).Len())
	for _, k := range reflect.ValueOf(m).MapKeys() {
		v, err := conv.ToFloat64E(k.Interface())
		if err != nil {
			return nil, err
		}
		dst[v] = struct{}{}
	}
	return dst, nil
}

转换成 string 集合。

// ToStrSet converts a slice or array to map[string]struct{}.
func ToStrSet(i any) map[string]struct{} {
	m, _ := ToStrSetE(i)
	return m
}

// ToStrSetE converts a slice or array to map[string]struct{} and returns an error if occurred.
func ToStrSetE(i any) (map[string]struct{}, error) {
	m, err := toSetE(i)
	if err != nil {
		return nil, err
	}
	if v, ok := m.(map[string]struct{}); ok {
		return v, nil
	}
	dst := make(map[string]struct{}, reflect.ValueOf(m).Len())
	for _, k := range reflect.ValueOf(m).MapKeys() {
		v, err := conv.ToStringE(k.Interface())
		if err != nil {
			return nil, err
		}
		dst[v] = struct{}{}
	}
	return dst, nil
}

转换示例:

package main

import (
	"fmt"
	"reflect"

	"github.com/dablelv/cyan/conv"
)

func main() {
	intSet, err := ToIntSetE([]int{1, 2, 3})
	fmt.Println(intSet, err)

	f64Set, err := ToFloat64SetE([]float64{1.1, 2.2, 3.3})
	fmt.Println(f64Set, err)

	strSet, err := ToStrSetE([]string{"foo", "bar", "baz"})
	fmt.Println(strSet, err)
}

运行输出:

map[1:{} 2:{} 3:{}] <nil>
map[1.1:{} 2.2:{} 3.3:{}] <nil>
map[bar:{} baz:{} foo:{}] <nil>

3.泛型

Golang 在 1.18 中引入了千呼万唤的泛型,利用泛型,我们可以不用针对具体类型单独封装,少写上面很多重复的代码。

// ToSet converts a slice or array to map[T]struct{} and returns a nil if error occurred.
func ToSet[T comparable](i any) map[T]struct{} {
	m, _ := ToSetE[T](i)
	return m
}

// ToSetE converts a slice or array to map[T]struct{} and returns an error if occurred.
// Note that the the element type of input don't need to be equal to the map key type.
// For example, []uint64{1, 2, 3} can be converted to map[uint64]struct{}{1:struct{}, 2:struct{},3:struct{}}
// and also can be converted to map[string]struct{}{"1":struct{}, "2":struct{}, "3":struct{}}
// if you want.
// Note that this function is implemented through 1.18 generics, so the element type needs to
// be specified when calling it, e.g. ToSetE[int]([]int{1,2,3}).
func ToSetE[T comparable](a any) (map[T]struct{}, error) {
	// Check input.
	if a == nil {
		return nil, nil
	}
	
	t := reflect.TypeOf(a)
	kind := t.Kind()
	if kind != reflect.Slice && kind != reflect.Array {
		return nil, fmt.Errorf("the type %T of input %#v isn't a slice or array", a, a)
	}

	v := reflect.ValueOf(a)
	if kind == reflect.Slice && v.IsNil() {
		return nil, nil
	}

	// Execute the conversion.
	mapT := reflect.MapOf(t.Elem(), reflect.TypeOf(struct{}{}))
	mapV := reflect.MakeMapWithSize(mapT, v.Len())
	for i := 0; i < v.Len(); i++ {
		mapV.SetMapIndex(v.Index(i), reflect.ValueOf(struct{}{}))
	}
	if v, ok := mapV.Interface().(map[T]struct{}); ok {
		return v, nil
	}
	// Convert the element to the type T.
	set := make(map[T]struct{}, v.Len())
	for _, k := range mapV.MapKeys() {
		v, err := ToAnyE[T](k.Interface())
		if err != nil {
			return nil, err
		}
		set[v] = struct{}{}
	}
	return set, nil
}

注意: 如果目标集合的类型和切片或数组元素类型不一致,会尝试进行类型转换。转换用到的函数如下:

import "github.com/dablelv/cyan/conv"

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

转换示例:

func main() {
	intSet, err := ToSetE[int]([]int{1, 2, 3})
	fmt.Println(intSet, err)

	f64Set, err := ToSetE[float64]([]float64{1.1, 2.2, 3.3})
	fmt.Println(f64Set, err)

	strSet, err := ToSetE[string]([]string{"foo", "bar", "baz"})
	fmt.Println(strSet, err)
}

运行输出:

map[1:{} 2:{} 3:{}] <nil>
map[1:{} 2:{} 3:{}] <nil>
map[bar:{} baz:{} foo:{}] <nil>

4.dablelv/cyan

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

import (
    "github.com/dablelv/cyan/conv"
)

// Convert bool slice or array to set.
bools := []bool{true, false, true}
set := conv.ToBoolSet(bools)
set, _ := conv.ToBoolSetE(bools)
set := conv.ToSet[bool](bools)
set, _ := conv.ToSetE[bool](bools)

// Convert int slice or array to set.
ints := []int{1, 2, 3}
set := conv.ToIntSet(ints)
set, _ := conv.ToIntSetE(ints)
set := conv.ToSet[int](ints)
set, _ := conv.ToSetE[int](ints)

// Convert string slice or array to set.
strs := []string{"foo", "bar", "baz"}
set := conv.ToStrSet(strs)
set, _ := conv.ToStrSetE(strs)
set := conv.ToSet[string](strs)
set, _ := conv.ToSetE[string](strs)

// Convert int8, int16, int32, uint etc. slice or array to set.
// ...

参考文献

github.com/dablelv/cyan

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值