Go 语言学习中遇到的问题

2023-07-01 06:01:13 +08:00
 Richard14

如题,初学者。当然因为是科班所以可能问题也不是那么初学。。。

目前做了的工作:根据 v2 推荐看了 B 站的 8 小时转职 go 的视频教程,大略翻读了下《 Go 程序语言设计》

以上学习过程中,主要从一线农民的实用注意出发,产生了以下问题:

语言设计相关

  1. Go 语言中对象的生命周期是怎么样的,我注意到例如函数内部创建的对象,在函数结束后并未被销毁。
  2. Go 语言基础中是否存在与 java 的万物皆对象类似的基础原则,我注意到似乎不是所有东西都是对象,起码 err 不能用反射调查其属性和方法....
  3. defer 的概念非常有趣,也很符合 Go 的设计主旨,不知道 Go 语言中是否有类似 Python 中上下文管理器的工具(即实现打开文件时确保其使用后被关闭,上锁时确保使用后会开锁等操作)
  4. 从原理角度,如何理解 channel 的调用开销。使用 channel 传递数据究竟是一种廉价操作,还是昂贵操作,其究竟是否依赖锁?按我的理解它依赖于事件循环,每次激活后才会唤醒依赖它的协程,应该是无锁的,但是书里写它其实是有锁的,我不是很懂。
  5. 如果 channel 有锁,通道通信相较于内存共享的优越性体现在哪里?
  6. 对于 string 的处理方式。我们都知道 string 通常是比看上去更复杂的数据结构,教学视频中看到的所有赋值和参数调用基本都是直接传值,实际情况是否如此,这是否意味着如果不加优化通常效率会很低?
  7. Go 语言设计上多大程度上接近底层,(例如自行推算数组第 n 位指针的地址,这种操作是否允许/是否可靠)
  8. Go 语言中闭包的概念是如何的?(由于 goroutine 的出现,似乎搞清闭包规则变得很重要)
  9. 命令行打印是否是同步阻塞行为(如果用打印输出程序状态,是否需要考虑类似 console.log 的异步输出情况)
  10. Go 事件循环内部提供的 select 是否依赖系统调用?这是不是一种昂贵操作?
  11. Go 语言是强类型还是弱类型,为什么函数指定返回 int 类型时还可以返回 nil...
  12. 吐个槽,panic 和 recover 使用起来还必须限定于当前绑定的事件循环内,让我觉得需要使用这个特性的场景会变得非常难以理解和不可控。。。

多线程调度相关

  1. 多线程竞争资源相关,读书注意到 Go 有提供信号量的概念,请问有与自旋锁对标的东西吗?
  2. Go 的调度模型下,单个协程应该是很省的,但是如果我有一万个协程同时对一个 map 进行交互,此种情况下应该如何进行优化?(或者说 Go 应对此种场景时也并无特殊优势?)

最佳实践相关

  1. 通常情况下,Go 语言调查未知对象内部属性和方法的最佳实践是什么?
  2. Go 语言中代码计时的最佳实践?(例如我想对比两种不同算法的 Go 运行时间,通常在各语言里想要准确的计算运行时间都有些坑)
  3. 使用 interface{}当做万能指针(例如实际应用场景中使用万能指针在数组中储存不同类型的对象引用)实践中是否有哪些坑?
  4. 常见数据结构中,除了 vector 和 map 外,set 、deque 、list 这些有什么官方实现吗?(或者最佳实践)

以上,各位能回答其中任意问题都非常欢迎。如果问题太菜了请勿喷。拜谢。

2719 次点击
所在节点    Go 编程语言
15 条回复
mcfog
2023-07-01 07:02:59 +08:00
问题的平均质量其实挺高的,就是太多导致没有回答的欲望
tairan2006
2023-07-01 07:56:04 +08:00
为啥不问 gpt
Richard14
2023-07-01 08:11:15 +08:00
@mcfog 大佬可择一二回。
@tairan2006 gtp 能讲明白这个我信,但他能拍胸脯打包票吗。。。
LaTero
2023-07-01 08:24:20 +08:00
不是面向对象,没有 raii ,资源用 defer 销毁。
GeruzoniAnsasu
2023-07-01 08:27:55 +08:00
1. golang 有 扫描式 gc ,也有逃逸分析,水平有限细节说不上来
2. 无。 你应该把 golang 看做「内建包管理和工程化套件的 C 」
3. 无。这种东西需要 RAII 类的机制,但 golang 的 defer 与 RAII 差异很大,不能类同,否则会踩大坑。
4. 有锁。你要知道 golang 的「协程」这个说法是错误的,仅仅是中文环境的惯用说法而已,实际上 goroutine 是具有 parallel 能力的轻量级 协程/线程混合体,由于你不能强制 goroutine 具象化为线程还是同执行块内的协程,所以用起来得非常小心,这与大多数初学者对它的印象是大相径庭的。当你意识到这点之后才能考虑 channel 的复杂性。
5. 有锁意味着自带临界区。goroutine 是可以 parallel 的,再强调一次。
6. 没太理解问题指什么,string 在 golang 中是 immutable 对象,但底层是 []byte ,所以绝大多数可观察的行为模式都会跟 slice 一样
7. golang 可以直接使用 C 式接口,当然也就能计算裸内存地址,但是要转回 golang 可用的结构就会有一系列检查和重新声明的过程(比如转回 string 要显式传入 string 长度),这样 golang 数据类型依然可以由 GC 清理,而 underlying 的,由外部「借内存地址给 golang 用」的,unsafe 部分则不会动
8. 基本上所有语言中闭包的规则都一样,闭包中 capture 到的名字与它外面那个名字的变量就是同一个。( c++除外,它默认 capture 行为是复制,而且可以显式指定如何 capture )
9. 不同步,要考虑输出混在一起。工程上会有各种开箱即用的 log package, 它们来解决这些问题。
10. 不依赖,select 的问题不在昂不昂贵上
11. 纠结这个没意义,你只需要知道的是 golang 没有隐式类型转换机制,仅仅会自动转换字面量的类型。另外 func () int {}不能 return nil
12. 它是 golang 中的 exception handler , 你不会想让异常跨线程处理吧??? 那内存访问早就乱了套了
GeruzoniAnsasu
2023-07-01 08:54:03 +08:00
多线程:
- goroutine 不是协程,是混合式线程
- 关注 sync.atomic , 而不是自旋锁本身
- 高竞争态跟线程切换开销并没有什么直接逻辑,这很取决于实现,比如用 channel 来实现就只会有一个线程被唤醒,被 select block 住的线程会等待调度器来带它离开 landpad ,但 mutex 就很难说了。所以其实也没太理解「一万个协程访问同一个 map 」是想讨论什么方向的问题


best practice:
1. 我理解在说写代码时。 —— golang 的 duck type 非常离谱,是 「我实现了鸭子,我就是鸭子」。所以不借助 IDE 的全量索引根本不太可能知道任意一个类型有什么方法。只要还在同一个 package 内(可以不同文件),就能随时给类型增加方法。 至于 properties ,golang 没有 property ,只有定义结构时的 fields
2. pprof
3. 能用 named interface 就用有名字的,interface {} 一般只会作为反序列化函数的入参类型。interface {} 就跟 Object class / any 差不多
4. 体会一下「有内建包管理和工程管理功能的 C 」
yzbythesea
2023-07-01 09:04:26 +08:00
关于 channel 和 内存共享,这有一篇有意思的博文

Do not communicate by sharing memory; instead, share memory by communicating.
https://go.dev/blog/codelab-share
Trim21
2023-07-01 09:07:22 +08:00
2 err 是可以用反射查找他支持的方法的,怀疑你用错反射了。


```golang
func main() {
err := getError()

rv := reflect.ValueOf(err)
fmt.Println(rv)
for i := 0; i < rv.NumMethod(); i++ {
fmt.Println(rv.Type().Method(i).Name)
}
}
```
Trim21
2023-07-01 09:16:38 +08:00
关于最佳实践 1

一般来说 go 更偏向于会使用接口+方法来实现这个。

如果你是要“调查”的话直接看源码就好了。但如果你是在代码中想要根据传进来的不同类型的值进行不同的操作的话,使用 interface + type assert 来判断一个值是否实现了某个接口。


比如官方的 errors 包

https://cs.opensource.google/go/go/+/go1.20.5:src/errors/wrap.go;l=16
Trim21
2023-07-01 09:27:07 +08:00
语言设计 11 Go 语言是强类型还是弱类型,为什么函数指定返回 int 类型时还可以返回 nil...

这是错的,你应该是看到了 func () *int 返回 nil 吧
baiyi
2023-07-01 10:00:12 +08:00
1. 关键词:逃逸分析,由编译器判断放在堆上走 gc ,还是在栈里直接回收。
2. 没有
3. 没有
4. 一定要有锁,因为要线程安全
5. 这是一种设计模式而不是实现方式,因为你传递消息总是要通过内存的,关于并发模型这里的关键词是: CSP
6. 关于数据结构的问题都可以看下 Go 语言的底层实现,Go 是自举的,这块能够通过源码很好理解。
7. 没理解
8. 个人认为与其他语言没什么区别
9. 看你用的打印工具包的实现
10. 不依赖,select 跟 channel 的配合使用这块也可以看下 go 的实现,实现的比较简洁,也挺有意思
11. 是,理论上不能,应该是你的示例有问题
12. 其实我没太理解你说的事件循环,是你把其他语言的机制映射到了 go 语言上吗
hsfzxjy
2023-07-01 11:28:29 +08:00
go 的核心没有所谓的事件循环,和 js 不一样
jiac
2023-07-01 12:46:04 +08:00
语言设计
1. Go 在编译期用逃逸分析确定一个对象应该分配在堆上还是栈上,和是否用 new 创建无关,在栈上创建的对象函数退出即销毁,堆上垃圾回收负责销毁。PS Go 的大多数小对象都是在栈上直接销毁的,不需要垃圾回收,这也是 Go GC 不使用分代回收的设计原因。
2. 不是万物皆对象。
3. defer 和 Python 中 with 关键词类似,Go 没有上下文管理工具。PS 我自己总结 Go 的一个设计原则是“没有魔法”,其他语言中非直观的语法糖在 Go 中几乎都不存在。
4. channel 这块还没看,channel 是 Go 为了 CSP 编程模式提供的组建,使用 channel 不是是否廉价操作的问题,是性能开销可接受的情况下,如何选择设计模式的问题。
5. channel 代表着 "share memory by communicating" 的编程思想,锁一类的内存共享代表着 "communicate by sharing memory",两者只是解决方案的不同,不同场景各有优劣,编程语言和思想没有哪个更优越之分。
6. 不会,Go 帮你做好了优化。
7. 没有 Rust 和 C / C++ 那么接近,比 Java 接近,数组指针操作允许,用官方提供的 unsafe 包就可以做到,从包的名字就可以知道可靠程度完全依赖程序员,Go 自身不做保证。
8. 我觉得和其他语言的闭包概念没区别。
9. 不清楚。
10. 相对于其他语言,不是,在具体场景下 select 是否昂贵就需要具体分析,go 在编译期间会把 AST 中的 select 节点视使用方法替换成不同的 runtime 方法,各自方法有不同的优化策略。
11. 强类型,返回 int 不能返回 nil ,你可以自己试一下。
12. 前面我分享了自己总结 Go 的一个设计原则是“没有魔法”,panic 这种非直观语法其实生产中很少用,如果使用 Go ,返回错误请直接返回 error 类型,而不是依赖 panic 和 recover 。

多线程
1. sync.Mutex 会在不同的情况下自己决定是否自旋,如果你需要手工自旋检查某条件,建议使用 sync.Cond
2. 可以使用 channel 把对 map 的并发操作变为顺序操作,即多个 goroutines 把操作发到 channel 里,一个 goroutine 消费这个 channel 来操作 map ; Go 官方应该会推荐你使用 `sync.Map`。

最佳实践
1. 不要去调查,用接口来双方约定。
2. Go 自身提供了 benchmark 功能,自己去学吧。
3. 不要使用 interface{},除非万不得已,业务代码不会出现这个情况。
4. map[K]struct{} 就是 set ,deque 自己用 channel 或者 slice 封一个吧,list 没有官方实现,欢迎其他朋友补充最佳实践。
lesismal
2023-07-01 13:52:34 +08:00
挑几个来说

## 语言设计相关

> 3. defer 的概念非常有趣,也很符合 Go 的设计主旨,不知道 Go 语言中是否有类似 Python 中上下文管理器的工具(即实现打开文件时确保其使用后被关闭,上锁时确保使用后会开锁等操作)

既然知道 defer ,defer close/unlock 就可以了,不应该还有这种疑问。不同语言提供不同的东西,也没必要非得别人有啥就要 go 有同样的东西。
另外,比如 os.File 这种,即使不 defer close ,gc 时也会自动 close 、不至于泄露,只是可能不太及时,或者如果你的代码泄露了没有 defer 也没有 gc 才会副作用:
https://github.com/golang/go/blob/master/src/os/file_unix.go#L239

> 4. 从原理角度,如何理解 channel 的调用开销。使用 channel 传递数据究竟是一种廉价操作,还是昂贵操作,其究竟是否依赖锁?按我的理解它依赖于事件循环,每次激活后才会唤醒依赖它的协程,应该是无锁的,但是书里写它其实是有锁的,我不是很懂。

直接看源码吧,也可以配合一些别人的文章来看源码,比这种问答要认识的更清晰:
https://github.com/golang/go/blob/master/src/runtime/chan.go#L160
https://github.com/golang/go/blob/master/src/runtime/chan.go#L457

5. 如果 channel 有锁,通道通信相较于内存共享的优越性体现在哪里?

channel 适合业务层简单逻辑:你把它当成进程内的消息队列就可以了,与锁的区别主要是带容量、可以把逻辑串行化并去锁,用于异步、解耦之类的比较好。但简单的队列数据结构同步,channel 性能是不如锁的、而且用锁可以在当前上下文继续进行剩下的穿行逻辑,放到 channel 后则需要其他地方去异步从护理了,未必就是 channel 方便。
性能需要的场景,比如基础设施领域,锁仍然是主力,不能被 channel 替代。

> 6. 对于 string 的处理方式。我们都知道 string 通常是比看上去更复杂的数据结构,教学视频中看到的所有赋值和参数调用基本都是直接传值,实际情况是否如此,这是否意味着如果不加优化通常效率会很低?

你说的 string 传值只是 string 结构体定义的这个小结构体:
https://github.com/golang/go/blob/master/src/runtime/string.go#L232

并不是需要深拷贝 string 的 payload 然后再传值,所以相比于传指针开销并不算大

## 多线程调度相关

> 2. Go 的调度模型下,单个协程应该是很省的,但是如果我有一万个协程同时对一个 map 进行交互,此种情况下应该如何进行优化?(或者说 Go 应对此种场景时也并无特殊优势?)

1) 减小锁粒度:无非就是多加点 shard/bucket ,减小每个 map 元素数量,从而减小锁粒度
2) 提高并发度:1 )中处理后,就可以去多协程去搞每个 shard/bucket 的 foreach 了,典型的如广播场景,广播优化主要就几个点:避免写阻塞,降低锁粒度,提高并发度,批次发送

## 最佳实践相关

> 1. 通常情况下,Go 语言调查未知对象内部属性和方法的最佳实践是什么?

设计者应该根据需要设计数据结构,除了三方互接并且对方乱搞,为什么一定要依赖调查?绝大多数正常的业务领域,脑子拧巴的实现者才喜欢去猜、让自己或者让别人用奇淫巧计去实现需求
避免使用反射。

那些不理解甚至嘲讽 go 的大道至简的人,其实就是花了很多时间学会了很多其他语言的奇淫巧计,沉没成本效应,让这部分人舍不得放弃以前学的那些糟粕思维,然后惯性地用那些思维来思考 go ,然后才会有这些垃圾问题和需求。当然,我不否认少数时候 go 里也需要奇淫巧计,我自己也用。


其他问题好像多数偏于垃圾问题,不答了
PTLin
2023-07-01 18:22:58 +08:00
推荐你学习下 py 或者 rs 的协程模型,虽然 go 的 goroutine 是有栈的但是姑且能让你有更深一点的理解。
此外推荐你看看 Go 语言精进之路这本书。

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

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

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

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

© 2021 V2EX