V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
yujianwjj
V2EX  ›  Go 编程语言

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

  •  
  •   yujianwjj · 54 天前 · 4745 次点击
    这是一个创建于 54 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

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

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

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

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

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


    说到底连你这个 Stack 类也完全没有必要
    bv
        18
    bv  
       53 天前
    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  
       53 天前
    @darksword21 同样遇到这个问题,所以现在我选择到处 try ,捕获异常后往上抛 error_str😂
    homewORK
        20
    homewORK  
       53 天前
    为何要返回 err ? 直接返回 nil 不就好了嘛
    zealic
        21
    zealic  
       53 天前
    自己把没必要 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  
       53 天前
    @gitrebase 协程的 panic,其他协程无法捕获.程序就崩了.用这玩意等于埋雷
    MoYi123
        23
    MoYi123  
       53 天前   ❤️ 1
    多大点事.
    cpp 里对空 vector popback 还是 ub 呢.
    james122333
        24
    james122333  
       53 天前 via Android
    if v, err := s.Pop() ; err != nil {
    break
    } else {
    fmt.Println(v)
    }
    CLMan
        25
    CLMan  
       53 天前
    gongquanlin
        26
    gongquanlin  
       53 天前
    一直把 if err != nil 和 java 的 if(xxx == null) 做等值处理,就不觉着丑了哈哈哈
    写 java 的时候也得写一堆 if(xxxx == null) return xxx; 的处理
    hxysnail
        27
    hxysnail  
       53 天前
    实现都一个版本的 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  
       53 天前
    @darksword21 是的,一切都可能出错,所以 golang 的 err 返回值虽然很反人类,但是你的应用里对这个 err 怎么处理都是你可以做决定的
    FYFX
        29
    FYFX  
       53 天前
    我看了一下 rust 的 vector 和 zig 的 array_list 的 pop/popOrNull 实现,它们都可以在 list 为空的时候返回 None/null 的功能,感觉是要比抛异常/报错合适
    jonsmith
        30
    jonsmith  
       53 天前 via Android
    该 panic 还是要 panic
    sagaxu
        31
    sagaxu  
       53 天前
    @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  
       53 天前
    python: 相信用户
    go: 相信用户都是 xx
    ns09005264
        33
    ns09005264  
       53 天前 via Android
    pop 没必要返回 err ,对栈而言,pop 出一个 nil 值非常合理。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1191 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 23:57 · PVG 07:57 · LAX 16:57 · JFK 19:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.