一瞥之见

0%

记录 - 我在go中实现Enum的优化过程

写python的时候、其enum用起来非常方便。写go却发现python自带支持的功能有很多都需要自己去实现一遍,故特此记录下我优化的过程

以一个视频分辨率的枚举类型为例

python中的实现

除了最基本也是最重要的需要能够直接与具体枚举类型比较外,python中的enum用起来可以自动的支持:

  1. 如果输入值不在枚举值中会raise exception
  2. 对于常见的获取全部枚举值的需求能够很方便的获取到
    代码如下:
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
from enum import Enum
from typing import List


class Resolution(Enum):
R480P = "480P"
R720P = "720P"
R1080P = "1080P"
R2K = "2K"
R4K = "4K"

def can_compare_to_enum_directly():
resolution = Resolution('1080P')
print(resolution == Resolution.R1080P)

def raise_exception_if_value_not_in_enum():
not_valid_value = '10P'
try:
resolution = Resolution(not_valid_value)
except ValueError as e:
print('raised not valid Resolution exception')
except Exception as e:
print(e)

def get_all_enum_values() -> List[str]:
return [i.value for i in Resolution]

if __name__ == '__main__':
# 需要能够支持直接和类型比较
can_compare_to_enum_directly()
# 要能够检测输出值是否是有效的
raise_exception_if_value_not_in_enum()
# 要能够输出全部的可选值
all_enum_values = get_all_enum_values()
print(all_enum_values)

输出:

1
2
3
True
raised not valid Resolution exception
['480P', '720P', '1080P', '2K', '4K']

Go中的实现&优化过程记录

写Go的时候发现python里面能够简单实现/直接支持的功能/需求,实现起来却还真是有点儿“麻烦”。故特此记录我是如何一步步改进我的代码实现的。

原始版本代码及其存在的问题:

以下版本下意识最容易/直接写出来的代码

1
2
3
4
5
6
7
8
9
type Resolution string

const (
Resolution480P Resolution = "480P"
Resolution720P Resolution = "720P"
Resolution1080P Resolution = "1080P"
Resolution2K Resolution = "2K"
Resolution4K Resolution = "4K"
)

这可能是了,但是这样实现的话,会有以下问题:

  1. 即使输入的是不在enum中的值,也不会有问题(代码可见):
1
2
3
4
func main() {
resolution := Resolution("10P")
fmt.Println(resolution) // 成功输出10P,但是10P压根不是有效的值
}
  1. 常见的一个需求就是要能够提供一个列表列出全部的值、也就是[“480P”, “720P”, “1080P”, “2K”, “4K”]。这么写并不能够较为优雅的实现输出这个列表!

  2. 直接使用string类型,内存占用较大

优化一:解决上面不支持的功能

当然,最好的学习办法就是看官方库相似的需求是怎么实现的了,比如crypto里面的实现。

学习了其实现后,我的实现改为了如下(其已经解决了识别输入非有效枚举值 & 输出全部枚举值的需求):

特别注意下面代码中第10行的技巧!利用了这个标识位才实现了这两个功能

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"fmt"
)

type Resolution int

const (
Resolution300 Resolution = iota
Resolution600
Resolution720P
Resolution1080P
Resolution2K
Resolution4K
maxResolution
)

const (
DefaultResolution = Resolution300
)

type enumItem struct {
Label string
Value int
}
var allEnumItems = make([]enumItem, 0, maxResolution-1)

func init() {
for i := Resolution(0); i < maxResolution; i++ {
allEnumItems = append(allEnumItems, enumItem{Resolution(i).String(), int(i)})
}
}

func (this Resolution) String() string {
switch this {
case Resolution300:
return "300"
case Resolution600:
return "600"
case Resolution720P:
return "720P"
case Resolution1080P:
return "1080P"
case Resolution2K:
return "2K"
case Resolution4K:
return "4K"
default:
return "not valid"
}
}

func (resolution Resolution) IsValid() bool {
if resolution >= Resolution(0) && resolution < maxResolution {
return true
}
return false
}

func AllResolutions() []enumItem{
return allEnumItems
}

func main() {
fmt.Printf("%+v\n", AllResolutions()) // [{Label:300 Value:0} {Label:600 Value:1} {Label:720P Value:2} {Label:1080P Value:3} {Label:2K Value:4} {Label:4K Value:5}]

var resolution Resolution

validValue := 0
resolution = Resolution(validValue)
fmt.Println(resolution.IsValid()) // true
fmt.Println(resolution == Resolution300) // true
fmt.Println(resolution == Resolution600) // false
fmt.Println(resolution.String()) // 300

notValidValue := 100
resolution = Resolution(notValidValue)
fmt.Println(resolution.IsValid()) // false
fmt.Println(resolution.String()) // not valid
}

⚠️:改为上面的实现后需要注意的就是输入值从string变成了int型。具体运行代码可见

优化二:代码复用问题

自然,我们的代码里面会有大量的不同类的枚举值,那么都要按照上面实现一遍的话,不是不可以,但是我认为可以有部分的代码可以“节约”

一开始就说了,要解决的要点有三个:

  1. 支持直接与具体枚举类型比较
  2. 支持能够检测输入值是不是有效的
  3. 支持输出全部枚举值

这里面,功能1肯定是不能够摘出去的(不是不能、是摘出去后可能还需要通过类型转换再来比较、不优雅);功能3是很容易(因为是输出而已,不需要再在内部类型之间再做比较了)能够摘出去实现进行复用的;功能2的话也可以摘出去,不过有一定的性能损失;

实现如下(以下仅以原本值为string类型的做说明),go playground

  1. 单独写enum相关的包
    文件enum/str_enum.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
    package enum

    type StrEnum interface {
    ItemsAmount() int
    Value() int
    String() string
    StringByValue(value int) string
    }

    type StrEnumItem struct {
    Label string
    Value int
    }

    func AllStrEnums(strEnum StrEnum) []StrEnumItem {
    itemsAmount := strEnum.ItemsAmount()
    resp := make([]StrEnumItem, 0, itemsAmount-1)
    for i := 0; i < itemsAmount; i++ {
    resp = append(resp, StrEnumItem{strEnum.StringByValue(i), i})
    }
    return resp
    }

    func IsStrEnumValid(strEnum StrEnum) bool {
    itemsAmount := strEnum.ItemsAmount()
    number := strEnum.Value()
    if number >= 0 && number < itemsAmount {
    return true
    }
    return false
    }
  2. 定义枚举

    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
    type Resolution int

    const (
    Resolution300 Resolution = iota
    Resolution600
    Resolution720P
    Resolution1080P
    Resolution2K
    Resolution4K
    maxResolution
    )
    const (
    DefaultResolution = Resolution300
    )

    func (r Resolution) Value() int {
    return int(r)
    }

    func (r Resolution) String() string {
    switch r {
    case Resolution300:
    return "300"
    case Resolution600:
    return "600"
    case Resolution720P:
    return "720P"
    case Resolution1080P:
    return "1080P"
    case Resolution2K:
    return "2K"
    case Resolution4K:
    return "4K"
    default:
    return "not valid"
    }
    }

    func (r Resolution) StringByValue(value int) string {
    res := Resolution(value)
    return res.String()
    }

    func (r Resolution) ItemsAmount() int {
    return int(maxResolution) - 1
    }

    可见、定义枚举可以省去生成枚举值列表判断输入值是否有效两部分的逻辑!

  3. 实际使用举例

    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
    import (
    "fmt"
    "xx/enum"
    )

    func main() {
    // 列出全部枚举选项
    // 不优雅: 这点是比较别扭的,需要通过一个具体的类型才导出了全部的可选项!
    fmt.Printf("%+v\n", enum.AllStrEnums(DefaultResolution)) // [{Label:300 Value:0} {Label:600 Value:1} {Label:720P Value:2} {Label:1080P Value:3} {Label:2K Value:4}]

    var resolution Resolution

    value := 0
    resolution = Resolution(value)

    // 直接与具体枚举类型比较
    fmt.Println(resolution == Resolution300) // true
    fmt.Println(resolution == Resolution600) // false


    // 判断输入值是否valid
    fmt.Println(enum.IsStrEnumValid(resolution)) // true

    notValidValue := 100
    resolution = Resolution(notValidValue)
    fmt.Println(enum.IsStrEnumValid(resolution)) // false
    }

优化二和优化一的对比

  1. 优势
    1. 复用了代码:Resolution枚举包里的代码行数从57降低到了46(而且优化二里的代码接口实际提供的功能是更多的!)
    2. 变化隔离:我们看到最后导出全部枚举值最后都是调用的enum包的AllStrEnums函数,其结构是:[{“label”: “300”, “value”: 0}, …], 这个其实很明显是可能给前端使用的,假设我们有10种枚举类型(如电视品牌、音频格式…),这样实现的输出统一都会是以上格式!假设哪一天突然前端要求将label这个字段改为display,我们也只需要再enum包中改一下AllStrEnums函数即可(或者新写一个进行替换/新旧兼容),想想如果是优化一中的话,那就需要在10个地方改了啊!
    3. 可扩展更好:因为进行了分层和对于interface的引入,对于后续的扩展支持会更加规范和方便!
  2. 劣势
    1. IsValid函数的判断性能有所下降,其实这里完全可以也更为推荐不在enum包中实现这个有效性判断的,可以通过interface规定让每个枚举类型自己去实现,这样更好(因为上面说的优化其实都是对于输出枚举值列表而言的以及分层本身带来的变化隔离)。在更为复杂的系统里面,各个枚举的有效性判断可能会不都是如此就能判断的,故下派到具体的地方去实现是更好的,而且调用的时候也会更优雅,从enum.IsStrEnumValid(resolution) -> resolution.IsValid(),如此的例子可见playground。这里我只是觉得我的系统如此够用了、能省就省一点儿写代码才如此放到了enum包。
    2. 获取全部枚举值的参数需要一个具体的枚举值、不优雅