使用 go 设计数据结构很蛋疼的一个点 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
yujianwjj
V2EX    Go 编程语言

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

  •  
  •   yujianwjj 2024 年 7 月 15 日 5790 次点击
    这是一个创建于 555 天前的主题,其中的信息可能已经有所发展或是发生改变。

    工作中用 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 一些大道至简的设计。

    第 1 条附言    2024 年 7 月 16 日
    1. 我这里特指是 go 在设计数据结构的时候,里面返回 error 会造成代码很不优雅,但是如果不返回 error 会造成代码的不严谨。比如 go 标准库的 container/heap 里面就是直接没有判断是否为空。我这里用 stack 来只是举个例子,实际上工作中设计的数据结构可能更复杂一点。
    2. 用 panic 的话严格来说并不合适,实际中用 panic 的话,会造成后面的代码就无法运行,语意不对。
    v = s.pop() //这里如果 panic 的话,后面的逻辑可能没法允许。 
    第 2 条附言    2024 年 7 月 16 日
    把第二个返回值改成 bool 确实满足了我的需求。
    33 条回复    2024-07-16 16:03:22 +08:00
    gitrebase
        1
    gitrebase  
       2024 年 7 月 15 日   7
    python 你能 raise ,go 为啥不能 panic
    BeijingBaby
        2
    BeijingBaby  
       2024 年 7 月 15 日   1
    if err 在 go 中太常见了,习惯了就不丑了。
    Nasei
        3
    Nasei  
       2024 年 7 月 15 日
    你可以加一个 不返回 err 的 pop 函数,为空时 panic 就行了,原来那个 pop 也能调用这个
    povsister
        4
    povsister  
       2024 年 7 月 15 日   3
    Pop() (int, err)
    MustPop() int else panic

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

    go 的核心理念是又不是不能用,差不多得了
    sagaxu
        9
    sagaxu  
       2024 年 7 月 15 日   2
    panic => throw
    recover => catch
    defer => finally

    很多人就是这么滥用
    rrfeng
        10
    rrfeng  
       2024 年 7 月 15 日 via Android
    我觉得不是语言的问题。其他语言也一样。

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

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

    Go 就是丑的,美观和写法优雅从来不是它的核心追求。如果你不能忍受,就果断换语言吧。
    cmdOptionKana
        12
    cmdOptionKana  
       2024 年 7 月 15 日
    哪个语言没有丑的地方?
    Keuin
        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 ,有性能损失。
    Trim21
        14
    Trim21  
       2024 年 7 月 16 日
    你这里可以要求调用者用 IsEmpty 来保证 pop 不为空,然后发现为空直接 panic 。
    darksword21
        15
    darksword21  
    PRO
       2024 年 7 月 16 日 via iPhone   1
    我用 python 现在反而很头大,因为我不是很清楚哪些操作会有异常,所以我只有两个选择,1 等出了异常改代码,2 到处 try ,其次我还不知道哪些异常是需要特殊处理的,比如 http exception 可能直接返回就行,总之我很怀念把 error 作为返回值显示处理,当然可以是我刚写不几天 python 还不太了解
    hundandadi
        16
    hundandadi  
       2024 年 7 月 16 日 via Android
    @BeijingBaby if err 不止一个,而是每一个的情况,咋能处理的优雅一点,比如三个函数每个都返回 err ,每个都 if err 烦死了
    GeruzoniAnsasu
        17
    GeruzoniAnsasu  
       2024 年 7 月 16 日
    你这个返回 error 明明是自找的,同 #14

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


    说到底连你这个 Stack 类也完全没有必要
    bv
        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
    }
    kalista
        19
    kalista  
       2024 年 7 月 16 日
    @darksword21 同样遇到这个问题,所以现在我选择到处 try ,捕获异常后往上抛 error_str
    homewORK
        20
    homewORK  
       2024 年 7 月 16 日
    为何要返回 err ? 直接返回 nil 不就好了嘛
    zealic
        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)
    }
    }
    }
    ```
    su943515688
        22
    su943515688  
       2024 年 7 月 16 日
    @gitrebase 协程的 panic,其他协程无法捕获.程序就崩了.用这玩意等于埋雷
    MoYi123
        23
    MoYi123  
       2024 年 7 月 16 日   1
    多大点事.
    cpp 里对空 vector popback 还是 ub 呢.
    james122333
        24
    james122333  
       2024 年 7 月 16 日 via Android
    if v, err := s.Pop() ; err != nil {
    break
    } else {
    fmt.Println(v)
    }
    CLMan
        25
    CLMan  
       2024 年 7 月 16 日
    gongquanlin
        26
    gongquanlin  
       2024 年 7 月 16 日
    一直把 if err != nil 和 java 的 if(xxx == null) 做等值处理,就不觉着丑了哈哈哈
    写 java 的时候也得写一堆 if(xxxx == null) return xxx; 的处理
    hxysnail
        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……
    guanzhangzhang
        28
    guanzhangzhang  
       2024 年 7 月 16 日
    @darksword21 是的,一切都可能出错,所以 golang 的 err 返回值虽然很反人类,但是你的应用里对这个 err 怎么处理都是你可以做决定的
    FYFX
        29
    FYFX  
       2024 年 7 月 16 日
    我看了一下 rust 的 vector 和 zig 的 array_list 的 pop/popOrNull 实现,它们都可以在 list 为空的时候返回 None/null 的功能,感觉是要比抛异常/报错合适
    jonsmith
        30
    jonsmith  
       2024 年 7 月 16 日 via Android
    该 panic 还是要 panic
    sagaxu
        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);
    }
    feiyan35488
        32
    feiyan35488  
       2024 年 7 月 16 日
    python: 相信用户
    go: 相信用户都是 xx
    ns09005264
        33
    ns09005264  
       2024 年 7 月 16 日 via Android
    pop 没必要返回 err ,对栈而言,pop 出一个 nil 值非常合理。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2440 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 21ms UTC 15:55 PVG 23:55 LAX 07:55 JFK 10:55
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86