
工作中用 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 一些大道至简的设计。
v = s.pop() //这里如果 panic 的话,后面的逻辑可能没法允许。 1 gitrebase 2024 年 7 月 15 日 python 你能 raise ,go 为啥不能 panic |
2 BeijingBaby 2024 年 7 月 15 日 if err 在 go 中太常见了,习惯了就不丑了。 |
3 Nasei 2024 年 7 月 15 日 你可以加一个 不返回 err 的 pop 函数,为空时 panic 就行了,原来那个 pop 也能调用这个 |
4 povsister 2024 年 7 月 15 日 Pop() (int, err) MustPop() int else panic python 能 raise ,为啥换 go 你不会 panic 了? |
5 henix 2024 年 7 月 15 日 如果是我的话可能会选择 pop 函数为空时 panic ,因为你已经提供了 IsEmpty ,为空时还要 Pop 可以认为是程序的逻辑错误(需要改程序)。 程序逻辑错误(需要程序员改程序):用 panic 外部错误(用户输入、上游第三方系统,程序员无法控制):用 error |
6 lesismal 2024 年 7 月 15 日 这种纯数据结构本身就不应该设计成返回 error 的,其他语言这种数据结构也没见过返回 error 之类的啊。。: https://github.com/golang/go/blob/master/src/container/list/list.go |
7 doraemonki 2024 年 7 月 15 日 via Android 设计的有问题,看看别人的实现吧 |
8 Leviathann 2024 年 7 月 15 日 什么狗屁大道至简 go 的核心理念是又不是不能用,差不多得了 |
9 sagaxu 2024 年 7 月 15 日 panic => throw recover => catch defer => finally 很多人就是这么滥用 |
10 rrfeng 2024 年 7 月 15 日 via Android 我觉得不是语言的问题。其他语言也一样。 或许用 1.22 的 range func 试试 |
11 "Error is also a return value" 的设计理念就会导致这样的结果。当然这个思想本身没有错,只是 Go 执行得太尴尬了。 其他语言会加一些语法糖来缓解(例如 Rust 的 ?,Zig 的 try ),但 Go 受限于 minimum syntax sugar 的思想就只能这样弄。4 楼的 MustPop 是较优解。 Go 就是丑的,美观和写法优雅从来不是它的核心追求。如果你不能忍受,就果断换语言吧。 |
12 cmdOptionKana 2024 年 7 月 15 日 哪个语言没有丑的地方? |
13 Keuin 2024 年 7 月 16 日 这个锅其实硬扣,可以扣到 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 ,有性能损失。 |
14 Trim21 2024 年 7 月 16 日 你这里可以要求调用者用 IsEmpty 来保证 pop 不为空,然后发现为空直接 panic 。 |
15 darksword21 PRO 我用 python 现在反而很头大,因为我不是很清楚哪些操作会有异常,所以我只有两个选择,1 等出了异常改代码,2 到处 try ,其次我还不知道哪些异常是需要特殊处理的,比如 http exception 可能直接返回就行,总之我很怀念把 error 作为返回值显示处理,当然可以是我刚写不几天 python 还不太了解 |
16 hundandadi 2024 年 7 月 16 日 via Android @BeijingBaby if err 不止一个,而是每一个的情况,咋能处理的优雅一点,比如三个函数每个都返回 err ,每个都 if err 烦死了 |
17 GeruzoniAnsasu 2024 年 7 月 16 日 你这个返回 error 明明是自找的,同 #14 菜就多练,人都简完了你硬要自己把复杂度加上去然后忍着,最后就会跟 #2 一样 说到底连你这个 Stack 类也完全没有必要 |
18 bv 2024 年 7 月 16 日 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 } |
19 kalista 2024 年 7 月 16 日 @darksword21 同样遇到这个问题,所以现在我选择到处 try ,捕获异常后往上抛 error_str |
20 homewORK 2024 年 7 月 16 日 为何要返回 err ? 直接返回 nil 不就好了嘛 |
21 zealic 2024 年 7 月 16 日 自己把没必要 error 的地方加了 error 怪谁 ```golang package main import ( "fmt" ) 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, 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 } func main() { s := Stack{} // 往栈中 push 一些元素 s.Push(1) for !s.IsEmpty() { if v, ok := s.Pop(); ok { // do something fmt.Println(v) } } } ``` |
22 su943515688 2024 年 7 月 16 日 @gitrebase 协程的 panic,其他协程无法捕获.程序就崩了.用这玩意等于埋雷 |
23 MoYi123 2024 年 7 月 16 日 多大点事. cpp 里对空 vector popback 还是 ub 呢. |
24 james122333 2024 年 7 月 16 日 via Android if v, err := s.Pop() ; err != nil { break } else { fmt.Println(v) } |
25 CLMan 2024 年 7 月 16 日 |
26 gongquanlin 2024 年 7 月 16 日 一直把 if err != nil 和 java 的 if(xxx == null) 做等值处理,就不觉着丑了哈哈哈 写 java 的时候也得写一堆 if(xxxx == null) return xxx; 的处理 |
27 hxysnail 2024 年 7 月 16 日 实现都一个版本的 Pop 不就好了…… func (stack *Stack) MustPop() int { value, err := stack.Pop() if err != nil { panic(err) } return value } 如果有可能产生 err ,那么一定是要返回并检查 err 的; 如果不可能产生 err ,那么就实现一个不返回 err 的版本。 err 的真正槽点在于,当调用链比较深时,每一层都需要判断 err ,return err…… |
28 guanzhangzhang 2024 年 7 月 16 日 @darksword21 是的,一切都可能出错,所以 golang 的 err 返回值虽然很反人类,但是你的应用里对这个 err 怎么处理都是你可以做决定的 |
29 FYFX 2024 年 7 月 16 日 我看了一下 rust 的 vector 和 zig 的 array_list 的 pop/popOrNull 实现,它们都可以在 list 为空的时候返回 None/null 的功能,感觉是要比抛异常/报错合适 |
30 jonsmith 2024 年 7 月 16 日 via Android 该 panic 还是要 panic |
31 sagaxu 2024 年 7 月 16 日 @gongquanlin Java 8 之后就是 Optional 了,少写很多 if (xxx == null) public Membership getAccountMembership_classic() { Account account = accountRepository.get("johnDoe"); if(account == null || account.getMembership() == null) { throw new AccountNotEligible(); } return account.getMembership(); } 变成 public Membership getAccountMembership_optional() { return accountRepository.find("johnDoe") .flatMap(Account::getMembershipOptional) .orElseThrow(AccountNotEligible::new); } |
32 feiyan35488 2024 年 7 月 16 日 python: 相信用户 go: 相信用户都是 xx |
33 ns09005264 2024 年 7 月 16 日 via Android pop 没必要返回 err ,对栈而言,pop 出一个 nil 值非常合理。 |