英文:
Does a type assertion / type switch have bad performance / is slow in Go?
问题
在Go语言中,使用类型断言和类型切换作为运行时类型发现的方法会导致性能下降。与C/C++等语言相比,运行时类型发现的性能较差。为了避免这个问题,通常会在类中添加类型成员,以便与这些成员进行比较而不是进行类型转换。
关于这个问题,我在互联网上没有找到明确的答案。
以下是你提问的示例代码,与其他类型检查方法(如上述提到的方法或其他我不知道的方法)相比,这被认为是快速的吗?
func question(anything interface{}) {
switch v := anything.(type) {
case string:
fmt.Println(v)
case int32, int64:
fmt.Println(v)
case SomeCustomType:
fmt.Println(v)
default:
fmt.Println("unknown")
}
}
希望对你有帮助!
英文:
How slow is using type assertions / type switches in Go, as a method of run-time type discovery?
I've heard that in C/C++ for example, discovering types at run time has bad performance. To bypass that, you usually add type members to classes, so you can compare against these instead of casting.
I haven't found a clear answer for this throughout the www.
Here's an example of what I'm asking about - Is this considered fast when compared to other type checking methodologies (like mentioned above, or others I'm not aware of)?
func question(anything interface{}) {
switch v := anything.(type) {
case string:
fmt.Println(v)
case int32, int64:
fmt.Println(v)
case SomeCustomType:
fmt.Println(v)
default:
fmt.Println("unknown")
}
}
答案1
得分: 50
很容易编写一个基准测试来检查它:http://play.golang.org/p/E9H_4K2J9-
package main
import (
"testing"
)
type myint int64
type Inccer interface {
inc()
}
func (i *myint) inc() {
*i = *i + 1
}
func BenchmarkIntmethod(b *testing.B) {
i := new(myint)
incnIntmethod(i, b.N)
}
func BenchmarkInterface(b *testing.B) {
i := new(myint)
incnInterface(i, b.N)
}
func BenchmarkTypeSwitch(b *testing.B) {
i := new(myint)
incnSwitch(i, b.N)
}
func BenchmarkTypeAssertion(b *testing.B) {
i := new(myint)
incnAssertion(i, b.N)
}
func incnIntmethod(i *myint, n int) {
for k := 0; k < n; k++ {
i.inc()
}
}
func incnInterface(any Inccer, n int) {
for k := 0; k < n; k++ {
any.inc()
}
}
func incnSwitch(any Inccer, n int) {
for k := 0; k < n; k++ {
switch v := any.(type) {
case *myint:
v.inc()
}
}
}
func incnAssertion(any Inccer, n int) {
for k := 0; k < n; k++ {
if newint, ok := any.(*myint); ok {
newint.inc()
}
}
}
编辑于2019年10月09日
看起来上面演示的方法相等,彼此之间没有优势。以下是我的机器(AMD R7 2700X,Golang v1.12.9)的结果:
BenchmarkIntmethod-16 2000000000 1.67 ns/op
BenchmarkInterface-16 1000000000 2.03 ns/op
BenchmarkTypeSwitch-16 2000000000 1.70 ns/op
BenchmarkTypeAssertion-16 2000000000 1.67 ns/op
PASS
再来一次:
BenchmarkIntmethod-16 2000000000 1.68 ns/op
BenchmarkInterface-16 1000000000 2.01 ns/op
BenchmarkTypeSwitch-16 2000000000 1.66 ns/op
BenchmarkTypeAssertion-16 2000000000 1.67 ns/op
之前于2015年1月19日的结果
在我的amd64机器上,我得到了以下的时间:
$ go test -bench=.
BenchmarkIntmethod 1000000000 2.71 ns/op
BenchmarkInterface 1000000000 2.98 ns/op
BenchmarkTypeSwitch 100000000 16.7 ns/op
BenchmarkTypeAssertion 100000000 13.8 ns/op
所以看起来通过类型切换或类型断言访问方法比直接调用方法或通过接口调用方法要慢大约5-6倍。我不知道C++是否更慢,或者这种减速对你的应用程序是否可以接受。
英文:
It is very easy to write a Benchmark test to check it: http://play.golang.org/p/E9H_4K2J9-
package main
import (
"testing"
)
type myint int64
type Inccer interface {
inc()
}
func (i *myint) inc() {
*i = *i + 1
}
func BenchmarkIntmethod(b *testing.B) {
i := new(myint)
incnIntmethod(i, b.N)
}
func BenchmarkInterface(b *testing.B) {
i := new(myint)
incnInterface(i, b.N)
}
func BenchmarkTypeSwitch(b *testing.B) {
i := new(myint)
incnSwitch(i, b.N)
}
func BenchmarkTypeAssertion(b *testing.B) {
i := new(myint)
incnAssertion(i, b.N)
}
func incnIntmethod(i *myint, n int) {
for k := 0; k < n; k++ {
i.inc()
}
}
func incnInterface(any Inccer, n int) {
for k := 0; k < n; k++ {
any.inc()
}
}
func incnSwitch(any Inccer, n int) {
for k := 0; k < n; k++ {
switch v := any.(type) {
case *myint:
v.inc()
}
}
}
func incnAssertion(any Inccer, n int) {
for k := 0; k < n; k++ {
if newint, ok := any.(*myint); ok {
newint.inc()
}
}
}
EDIT Oct. 09, 2019
It appears that the methods demonstrated above are equal and have no advantage over one another. Here are the results from my machine (AMD R7 2700X, Golang v1.12.9):
BenchmarkIntmethod-16 2000000000 1.67 ns/op
BenchmarkInterface-16 1000000000 2.03 ns/op
BenchmarkTypeSwitch-16 2000000000 1.70 ns/op
BenchmarkTypeAssertion-16 2000000000 1.67 ns/op
PASS
AND AGAIN:
BenchmarkIntmethod-16 2000000000 1.68 ns/op
BenchmarkInterface-16 1000000000 2.01 ns/op
BenchmarkTypeSwitch-16 2000000000 1.66 ns/op
BenchmarkTypeAssertion-16 2000000000 1.67 ns/op
PREVIOUS RESULTS on Jan. 19, 2015
On my amd64 machine, I'm getting the following timing:
$ go test -bench=.
BenchmarkIntmethod 1000000000 2.71 ns/op
BenchmarkInterface 1000000000 2.98 ns/op
BenchmarkTypeSwitch 100000000 16.7 ns/op
BenchmarkTypeAssertion 100000000 13.8 ns/op
So it looks like accessing the method via type switch or type assertion is about 5-6 times slower than calling the method directly or via interface.
I don't know if C++ is slower or if this slowdown is tolerable for your application.
答案2
得分: 17
我想通过自己验证siritinga的答案,并检查在TypeAssertion中删除检查是否会使其更快。我在他们的基准测试中添加了以下内容:
func incnAssertionNoCheck(any Inccer, n int) {
for k := 0; k < n; k++ {
any.(*myint).inc()
}
}
func BenchmarkTypeAssertionNoCheck(b *testing.B) {
i := new(myint)
incnAssertionNoCheck(i, b.N)
}
然后在我的机器上重新运行了基准测试。
BenchmarkIntmethod-12 2000000000 1.77 ns/op
BenchmarkInterface-12 1000000000 2.30 ns/op
BenchmarkTypeSwitch-12 500000000 3.76 ns/op
BenchmarkTypeAssertion-12 2000000000 1.73 ns/op
BenchmarkTypeAssertionNoCheck-12 2000000000 1.72 ns/op
所以看起来从Go 1.4(我假设siritinga使用的版本)到Go 1.6(我正在使用的版本),执行类型切换的成本显著降低了:类型切换的速度从慢了5-6倍降低到不到2倍,而类型断言(无论是否进行检查)没有变慢。
英文:
I wanted to verify siritinga's answer by myself, and check whether removing the check in TypeAssertion would make it faster. I added the following in their benchmark:
func incnAssertionNoCheck(any Inccer, n int) {
for k := 0; k < n; k++ {
any.(*myint).inc()
}
}
func BenchmarkTypeAssertionNoCheck(b *testing.B) {
i := new(myint)
incnAssertionNoCheck(i, b.N)
}
and re-ran the benchmarks on my machine.
BenchmarkIntmethod-12 2000000000 1.77 ns/op
BenchmarkInterface-12 1000000000 2.30 ns/op
BenchmarkTypeSwitch-12 500000000 3.76 ns/op
BenchmarkTypeAssertion-12 2000000000 1.73 ns/op
BenchmarkTypeAssertionNoCheck-12 2000000000 1.72 ns/op
So it seems that the cost of doing a type switch went down significantly from Go 1.4 (that I assume siritinga used) to Go 1.6 (that I'm using): from 5-6 times slower to less than 2 times slower for a type switch, and no slow-down for a type assertion (with or without check).
答案3
得分: 14
我的Go 1.9的结果
BenchmarkIntmethod-4 1000000000 2.42 ns/op
BenchmarkInterface-4 1000000000 2.84 ns/op
BenchmarkTypeSwitch-4 1000000000 2.29 ns/op
BenchmarkTypeAssertion-4 1000000000 2.14 ns/op
BenchmarkTypeAssertionNoCheck-4 1000000000 2.34 ns/op
类型断言现在更快了,但最有趣的是去除类型检查会使其变慢。
英文:
My Results using Go 1.9
BenchmarkIntmethod-4 1000000000 2.42 ns/op
BenchmarkInterface-4 1000000000 2.84 ns/op
BenchmarkTypeSwitch-4 1000000000 2.29 ns/op
BenchmarkTypeAssertion-4 1000000000 2.14 ns/op
BenchmarkTypeAssertionNoCheck-4 1000000000 2.34 ns/op
Type Assertion is much faster now, but the most interesting removing the type check makes it slow.
答案4
得分: 6
TL;DR:这取决于类型分布,但接口是最安全的选择,除非你确定类型会以规律的块出现。此外,还要考虑到如果你的代码执行频率很低,分支预测器也不会被预热。
长说明:
在go1.9.2上的darwin/amd64上:
BenchmarkIntmethod-4 2000000000 1.67 ns/op
BenchmarkInterface-4 2000000000 1.89 ns/op
BenchmarkTypeSwitch-4 2000000000 1.26 ns/op
BenchmarkTypeAssertion-4 2000000000 1.41 ns/op
BenchmarkTypeAssertionNoCheck-4 2000000000 1.61 ns/op
这里需要注意的一件重要的事情是,只有一个分支的类型切换与使用接口进行比较并不是非常公平。CPU分支预测器会非常快速地热起来,并给出非常好的结果。更好的基准测试应该使用伪随机类型和带有伪随机接收器的接口。显然,我们需要去除静态方法调度,只使用接口与类型切换(类型断言也变得不那么有意义,因为它需要大量的if语句,而没有人会写那样的代码,而不是使用类型切换)。以下是代码:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// 随机生成的
var input []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0), myi
nt4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
func BenchmarkInterface(b *testing.B) {
doStuffInterface(b.N)
}
func BenchmarkTypeSwitch(b *testing.B) {
doStuffSwitch(b.N)
}
func doStuffInterface(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
结果如下:
go test -bench .
goos: darwin
goarch: amd64
pkg: test
BenchmarkInterface-4 20000000 74.0 ns/op
BenchmarkTypeSwitch-4 20000000 119 ns/op
PASS
ok test 4.067s
类型越多,分布越随机,接口的优势就越大。
为了显示这种差异,我将代码更改为对比随机选择与始终选择相同类型的基准测试。在这种情况下,类型切换再次更快,而接口的速度相同,以下是代码:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// 随机生成的
var randInput []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0),
myint4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
var oneInput []DoStuff = []DoStuff{myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0),
myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0)}
func BenchmarkRandomInterface(b *testing.B) {
doStuffInterface(randInput, b.N)
}
func BenchmarkRandomTypeSwitch(b *testing.B) {
doStuffSwitch(randInput, b.N)
}
func BenchmarkOneInterface(b *testing.B) {
doStuffInterface(oneInput, b.N)
}
func BenchmarkOneTypeSwitch(b *testing.B) {
doStuffSwitch(oneInput, b.N)
}
func doStuffInterface(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
以下是结果:
BenchmarkRandomInterface-4 20000000 76.9 ns/op
BenchmarkRandomTypeSwitch-4 20000000 115 ns/op
BenchmarkOneInterface-4 20000000 76.6 ns/op
BenchmarkOneTypeSwitch-4 20000000 68.1 ns/op
英文:
TL;DR: it really depends on type distribution, but interfaces are the safest choice unless you are sure that types will appear in regular chunks. Also consider that if your code is executed infrequently, the branch predictor will also not be warmed up.
Long explanation:
On go1.9.2 on darwin/amd64
BenchmarkIntmethod-4 2000000000 1.67 ns/op
BenchmarkInterface-4 2000000000 1.89 ns/op
BenchmarkTypeSwitch-4 2000000000 1.26 ns/op
BenchmarkTypeAssertion-4 2000000000 1.41 ns/op
BenchmarkTypeAssertionNoCheck-4 2000000000 1.61 ns/op
An important thing to note here is that a type switch with only one branch is not a very fair comparison against using an interface. The CPU branch predictor is going to get very hot, very fast and give very good results. A better bench mark would use pseudo random types and an interface with pseudo random receivers. Obviously, we need to remove the static method dispatch and stick to just interfaces versus typeswitch (type assertion also becomes less meaningful since it would require a lot of if statements, and no one would write that instead of using a type switch). Here is the code:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// Randomly generated
var input []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0), myi
nt4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
func BenchmarkInterface(b *testing.B) {
doStuffInterface(b.N)
}
func BenchmarkTypeSwitch(b *testing.B) {
doStuffSwitch(b.N)
}
func doStuffInterface(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
And the results:
go test -bench .
goos: darwin
goarch: amd64
pkg: test
BenchmarkInterface-4 20000000 74.0 ns/op
BenchmarkTypeSwitch-4 20000000 119 ns/op
PASS
ok test 4.067s
The more types and the more random the distribution, the bigger win interfaces will be.
To show this disparity I changed the code to benchmark random choice versus always picking the same type. In this case, the typeswitch is again faster, while the interface is the same speed, here is the code:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// Randomly generated
var randInput []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0),
myint4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
var oneInput []DoStuff = []DoStuff{myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0),
myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0)}
func BenchmarkRandomInterface(b *testing.B) {
doStuffInterface(randInput, b.N)
}
func BenchmarkRandomTypeSwitch(b *testing.B) {
doStuffSwitch(randInput, b.N)
}
func BenchmarkOneInterface(b *testing.B) {
doStuffInterface(oneInput, b.N)
}
func BenchmarkOneTypeSwitch(b *testing.B) {
doStuffSwitch(oneInput, b.N)
}
func doStuffInterface(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
Here are the results:
BenchmarkRandomInterface-4 20000000 76.9 ns/op
BenchmarkRandomTypeSwitch-4 20000000 115 ns/op
BenchmarkOneInterface-4 20000000 76.6 ns/op
BenchmarkOneTypeSwitch-4 20000000 68.1 ns/op
答案5
得分: 4
我在我的笔记本电脑上运行了@siritinga的bench示例(go1.7.3 linux/amd64),得到了以下结果:
$ go test -bench .
BenchmarkIntmethod-4 2000000000 1.99 ns/op
BenchmarkInterface-4 1000000000 2.30 ns/op
BenchmarkTypeSwitch-4 2000000000 1.80 ns/op
BenchmarkTypeAssertion-4 2000000000 1.67 ns/op
英文:
I run bench example by @siritinga in my laptop (go1.7.3 linux/amd64), got this result:
$ go test -bench .
BenchmarkIntmethod-4 2000000000 1.99 ns/op
BenchmarkInterface-4 1000000000 2.30 ns/op
BenchmarkTypeSwitch-4 2000000000 1.80 ns/op
BenchmarkTypeAssertion-4 2000000000 1.67 ns/op
答案6
得分: 1
在你的代码中,
switch v := anything.(type) {
case SomeCustomType:
fmt.Println(v)
...
如果你不需要使用SomeCustomType.Fields
或者像fmt.Println(v)
中的方法,可以这样做:
switch anything.(type) { //避免 'v:= '接口转换,只进行断言
case SomeCustomType:
fmt.Println("anything的类型是SomeCustomType", anything)
...
这样做的话,速度大约会快两倍。
英文:
In your
switch v := anything.(type) {
case SomeCustomType:
fmt.Println(v)
...
if you need not SomeCustomType.Fields
or methods like in fmt.Println(v)
, doing
switch anything.(type) { //avoid 'v:= ' interface conversion, only assertion
case SomeCustomType:
fmt.Println("anything type is SomeCustomType", anything)
...
should be approximately two times faster
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论