Golang Go语言中使用设计数据结构很蛋疼的一个点

发布于 1周前 作者 gougou168 来自 Go语言

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


Golang Go语言中使用设计数据结构很蛋疼的一个点

更多关于Golang Go语言中使用设计数据结构很蛋疼的一个点的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

34 回复

python 你能 raise ,go 为啥不能 panic

更多关于Golang Go语言中使用设计数据结构很蛋疼的一个点的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


if err 在 go 中太常见了,习惯了就不丑了。

你可以加一个 不返回 err 的 pop 函数,为空时 panic 就行了,原来那个 pop 也能调用这个

Pop() (int, err)
MustPop() int else panic

python 能 raise ,为啥换 go 你不会 panic 了?

如果是我的话可能会选择 pop 函数为空时 panic ,因为你已经提供了 IsEmpty ,为空时还要 Pop 可以认为是程序的逻辑错误(需要改程序)。
程序逻辑错误(需要程序员改程序):用 panic
外部错误(用户输入、上游第三方系统,程序员无法控制):用 error

这种纯数据结构本身就不应该设计成返回 error 的,其他语言这种数据结构也没见过返回 error 之类的啊。。:
https://github.com/golang/go/blob/master/src/container/list/list.go

设计的有问题,看看别人的实现吧

什么狗屁大道至简

go 的核心理念是又不是不能用,差不多得了

panic => throw
recover => catch
defer => finally

很多人就是这么滥用

我觉得不是语言的问题。其他语言也一样。

或许用 1.22 的 range func 试试

“Error is also a return value” 的设计理念就会导致这样的结果。当然这个思想本身没有错,只是 Go 执行得太尴尬了。

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

Go 就是丑的,美观和写法优雅从来不是它的核心追求。如果你不能忍受,就果断换语言吧。

哪个语言没有丑的地方?

这个锅其实硬扣,可以扣到 go 头上,但是没有必要

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

你要是喜欢用 error 的话,把ok bool换成err error也是一样的。这里体现出 Go 的问题是,没有内置 Option[T]类型和迭代器类型(虽然有库,但是没有语法糖配合,基本没有使用价值),想要语法层面有糖吃,就要封装成 channel ,有性能损失。

你这里可以要求调用者用 IsEmpty 来保证 pop 不为空,然后发现为空直接 panic 。

我用 python 现在反而很头大,因为我不是很清楚哪些操作会有异常,所以我只有两个选择,1 等出了异常改代码,2 到处 try ,其次我还不知道哪些异常是需要特殊处理的,比如 http exception 可能直接返回就行,总之我很怀念把 error 作为返回值显示处理,当然可以是我刚写不几天 python 还不太了解

if err 不止一个,而是每一个的情况,咋能处理的优雅一点,比如三个函数每个都返回 err ,每个都 if err 烦死了

你这个返回 error 明明是自找的,同 #14

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


说到底连你这个 Stack 类也完全没有必要

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
}

同样遇到这个问题,所以现在我选择到处 try ,捕获异常后往上抛 error_str😂

为何要返回 err ? 直接返回 nil 不就好了嘛

自己把没必要 error 的地方加了 error 怪谁

golang<br>package main<br><br>import (<br> "fmt"<br>)<br><br>type Stack struct {<br> items []int<br>}<br><br>func (s *Stack) IsEmpty() bool {<br> return len(s.items) == 0<br>}<br><br>func (s *Stack) Push(item int) {<br> s.items = append(s.items, item)<br>}<br><br>func (s *Stack) Pop() (int, bool) {<br> if s.IsEmpty() {<br> return 0, false<br> }<br> item := s.items[len(s.items)-1]<br> s.items = s.items[:len(s.items)-1]<br> return item, true<br>}<br><br>func main() {<br> s := Stack{}<br> // 往栈中 push 一些元素<br> s.Push(1)<br> for !s.IsEmpty() {<br> if v, ok := s.Pop(); ok {<br> // do something<br> fmt.Println(v)<br> }<br> }<br>}<br>

协程的 panic,其他协程无法捕获.程序就崩了.用这玩意等于埋雷

多大点事.
cpp 里对空 vector popback 还是 ub 呢.

if v, err := s.Pop() ; err != nil {
break
} else {
fmt.Println(v)
}

一直把 if err != nil 和 java 的 if(xxx == null) 做等值处理,就不觉着丑了哈哈哈
写 java 的时候也得写一堆 if(xxxx == null) return xxx; 的处理

实现都一个版本的 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……

是的,一切都可能出错,所以 golang 的 err 返回值虽然很反人类,但是你的应用里对这个 err 怎么处理都是你可以做决定的

我看了一下 rust 的 vector 和 zig 的 array_list 的 pop/popOrNull 实现,它们都可以在 list 为空的时候返回 None/null 的功能,感觉是要比抛异常/报错合适

该 panic 还是要 panic

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);
}

python: 相信用户
go: 相信用户都是 xx

pop 没必要返回 err ,对栈而言,pop 出一个 nil 值非常合理。

在Golang(Go语言)中设计数据结构时,虽然有一些方面可能会让开发者感到挑战或“蛋疼”,但这也是Go语言设计哲学的一部分,强调简洁、明确和高效。以下是对此观点的一些专业回应:

Go语言在数据结构设计上确实有其独特之处,它更强调通过接口(interface)和类型(type)来定义数据的行为和特性。这种设计方式可能会让习惯于其他编程语言(如Java、C++)的开发者感到不适应,因为这些语言通常提供更丰富的类继承和多态性支持。

在Go中,如果你发现设计数据结构时遇到挑战,可能是因为还没有充分理解和利用Go的类型系统和接口。例如,Go的切片(slice)和映射(map)等内置数据结构已经为大多数常见场景提供了高效的解决方案。同时,通过组合(composition)而不是继承来构建复杂的数据结构,也是Go语言推荐的做法。

此外,Go语言的并发模型(如goroutines和channels)也要求我们在设计数据结构时考虑并发安全性。这可能需要额外的思考和努力,但这也是Go语言能够高效处理并发任务的优势所在。

总之,虽然Go语言在数据结构设计上可能有一些让开发者感到挑战的地方,但这也是其简洁、明确和高效设计哲学的体现。通过深入学习和实践,我们可以逐渐掌握Go语言的精髓,并设计出既符合业务需求又高效的数据结构。

回到顶部