使用 go 设计数据结构很蛋疼的一个点

158 天前
 yujianwjj

工作中用 go 设计了一个 stack 的数据结构

type Stack struct {
	items []int
}

func (s *Stack) IsEmpty() bool {
	return len(s.items) == 0
}

func (s *Stack) Push(item int) {
	s.items = append(s.items, item)
}

func (s *Stack) Pop() (int, error) {
	if s.IsEmpty() {
		return 0, errors.New("pop from empty stack")
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item, nil
}

func xx() {
	s := Stack{}
	// 往栈中 push 一些元素
	s.Push(1)
	for !s.IsEmpty() {
		v, err := s.Pop()
		if err != nil {
			break
		}
		// do something
		fmt.Println(v)
	}
}

现在的问题就是这个 if err != nil {} 这一段代码在这里真的太丑了(我的函数其实是纯数据的处理,本来还是简单优雅的,加上这个 error 觉得代码变丑了),并且我的代码逻辑已经判断了 栈 不为空,里面的 err 判断其实根本没有必要,当然 go 可以强制忽略这个错误。但是,还是丑,并且强制忽略错误不严谨,看着别扭。

func xx() {
	s := Stack{}
	// 往栈中 push 一些元素
	s.Push(1)
	for !s.IsEmpty() {
		v, _ := s.Pop()
		// do something
		fmt.Println(v)
	}
}

最后我实在看不下去这种代码,直接用了 slice 。

func x2() {
	var s []int
	s = append(s, 1)
	for len(s) != 0 {
		v := s[len(s)-1]
		// do something
		fmt.Println(v)
		s = s[:len(s)-1]
	}
}

在我看来,go 的 error 如果用在业务逻辑里面,写 if err != nil {} 这种代码,我觉得没啥问题。但是在设计数据结构的时候,如果用到 error 确实很别扭,并且你还要 import errors 这个包。

我看了一下 go 的 sdk 里面一些数据结构的设计,比如 container/heap 堆的设计,它直接不判断 h.Len() 是否为 0 。这样倒是没有我说的那个 error 代码丑的问题,但是这样更不严谨了。

// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func Pop(h Interface) interface{} {
	n := h.Len() - 1
	h.Swap(0, n)
	down(h, 0, n)
	return h.Pop()
}

如果我用 python 或者 java 这种带有异常的语言去写数据结构。

class Stack:
    def __init__(self):
        self.items = []

    def is_empty(self):
        return len(self.items) == 0

    def push(self, item):
        self.items.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("pop from empty stack")
        return self.items.pop()
        
if __name__ == "__main__":
    stack = Stack()
    stack.push(1)
    while not stack.is_empty():
        v = stack.pop()
        # do something
        print(v)

这样我觉得好看多了。

还是不喜欢 go 一些大道至简的设计。

4935 次点击
所在节点    Go 编程语言
33 条回复
gitrebase
158 天前
python 你能 raise ,go 为啥不能 panic
BeijingBaby
158 天前
if err 在 go 中太常见了,习惯了就不丑了。
Nasei
158 天前
你可以加一个 不返回 err 的 pop 函数,为空时 panic 就行了,原来那个 pop 也能调用这个
povsister
158 天前
Pop() (int, err)
MustPop() int else panic

python 能 raise ,为啥换 go 你不会 panic 了?
henix
158 天前
如果是我的话可能会选择 pop 函数为空时 panic ,因为你已经提供了 IsEmpty ,为空时还要 Pop 可以认为是程序的逻辑错误(需要改程序)。
程序逻辑错误(需要程序员改程序):用 panic
外部错误(用户输入、上游第三方系统,程序员无法控制):用 error
lesismal
158 天前
这种纯数据结构本身就不应该设计成返回 error 的,其他语言这种数据结构也没见过返回 error 之类的啊。。:
https://github.com/golang/go/blob/master/src/container/list/list.go
doraemonki
158 天前
设计的有问题,看看别人的实现吧
Leviathann
158 天前
什么狗屁大道至简

go 的核心理念是又不是不能用,差不多得了
sagaxu
158 天前
panic => throw
recover => catch
defer => finally

很多人就是这么滥用
rrfeng
158 天前
我觉得不是语言的问题。其他语言也一样。

或许用 1.22 的 range func 试试
w568w
158 天前
"Error is also a return value" 的设计理念就会导致这样的结果。当然这个思想本身没有错,只是 Go 执行得太尴尬了。

其他语言会加一些语法糖来缓解(例如 Rust 的 ?,Zig 的 try ),但 Go 受限于 minimum syntax sugar 的思想就只能这样弄。4 楼的 MustPop 是较优解。

Go 就是丑的,美观和写法优雅从来不是它的核心追求。如果你不能忍受,就果断换语言吧。
cmdOptionKana
158 天前
哪个语言没有丑的地方?
Keuin
157 天前
这个锅其实硬扣,可以扣到 go 头上,但是没有必要

```go
var (
v int
ok bool
)
for v, ok = s.Pop(); ok; v, ok = s.Pop() {
fmt.Println(v)
}
```

你要是喜欢用 error 的话,把`ok bool`换成`err error`也是一样的。这里体现出 Go 的问题是,没有内置 Option[T]类型和迭代器类型(虽然有库,但是没有语法糖配合,基本没有使用价值),想要语法层面有糖吃,就要封装成 channel ,有性能损失。
Trim21
157 天前
你这里可以要求调用者用 IsEmpty 来保证 pop 不为空,然后发现为空直接 panic 。
darksword21
157 天前
我用 python 现在反而很头大,因为我不是很清楚哪些操作会有异常,所以我只有两个选择,1 等出了异常改代码,2 到处 try ,其次我还不知道哪些异常是需要特殊处理的,比如 http exception 可能直接返回就行,总之我很怀念把 error 作为返回值显示处理,当然可以是我刚写不几天 python 还不太了解
hundandadi
157 天前
@BeijingBaby if err 不止一个,而是每一个的情况,咋能处理的优雅一点,比如三个函数每个都返回 err ,每个都 if err 烦死了
GeruzoniAnsasu
157 天前
你这个返回 error 明明是自找的,同 #14

菜就多练,人都简完了你硬要自己把复杂度加上去然后忍着,最后就会跟 #2 一样


说到底连你这个 Stack 类也完全没有必要
bv
157 天前
func (s *Stack) Pop() (int, bool) {
if s.IsEmpty() {
return 0, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
kalista
157 天前
@darksword21 同样遇到这个问题,所以现在我选择到处 try ,捕获异常后往上抛 error_str😂
homewORK
157 天前
为何要返回 err ? 直接返回 nil 不就好了嘛

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1057520

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX