詳解Go語言中空結(jié)構(gòu)體的慣用法
在 Go 語言中,空結(jié)構(gòu)體 struct{} 是一個(gè)非常特殊的類型,它不包含任何字段并且不占用任何內(nèi)存空間。雖然聽起來似乎沒什么用,但空結(jié)構(gòu)體在 Go 編程中實(shí)際上有著廣泛的應(yīng)用。本文將詳細(xì)探討空結(jié)構(gòu)體的幾種典型用法,并解釋為何它們?cè)谔囟▓?chǎng)景下非常有用。
空結(jié)構(gòu)體不占用內(nèi)存空間
首先我們來驗(yàn)證下空結(jié)構(gòu)體是否占用內(nèi)存空間:
type Empty struct{}
var s1 struct{}
s2 := Empty{}
s3 := struct{}{}
fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1))
fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2))
fmt.Printf("s3 addr: %p, size: %d\n", &s3, unsafe.Sizeof(s3))
fmt.Printf("s1 == s2 == s3: %t\n", s1 == s2 && s2 == s3)
NOTE: 為了保持代碼邏輯清晰,這里只展示了代碼主邏輯。后文中所有示例代碼都會(huì)如此,完整代碼可以在文末給出的示例代碼 GitHub 鏈接中獲取。
在 Go 語言中,我們可以使用 unsafe.Sizeof 計(jì)算一個(gè)對(duì)象占用的字節(jié)數(shù)。
執(zhí)行以上示例代碼,輸出結(jié)果如下:
$ go run main.go
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0
s3 addr: 0x1044ef4a0, size: 0
s1 == s2 == s3: true
根據(jù)輸出結(jié)果可知:
- 多個(gè)空結(jié)構(gòu)體內(nèi)存地址相同。
- 空結(jié)構(gòu)體占用字節(jié)數(shù)為 0,即不占用內(nèi)存空間。
- 多個(gè)空結(jié)構(gòu)體值相等。
后面兩個(gè)結(jié)論很好理解,第一個(gè)結(jié)論有點(diǎn)反常識(shí)。為什么不同變量實(shí)例化的空結(jié)構(gòu)體內(nèi)存地址會(huì)相同?
真的是這樣嗎?我們可以看下另一個(gè)示例:
var (
a struct{}
b struct{}
c struct{}
d struct{}
)
println("&a:", &a)
println("&b:", &b)
println("&c:", &c)
println("&d:", &d)
println("&a == &b:", &a == &b)
x := &a
y := &b
println("x == y:", x == y)
fmt.Printf("&c(%p) == &d(%p): %t\n", &c, &d, &c == &d)
這段代碼中定義了 4 個(gè)空結(jié)構(gòu)體,依次打印它們的內(nèi)存地址,然后又分別對(duì)比了 a 與 b 的內(nèi)存地址和 c 與 d 的內(nèi)存地址兩兩是否相等。
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run -gcflags='-m -N -l' main.go
# command-line-arguments
./main.go:11:3: moved to heap: c
./main.go:12:3: moved to heap: d
./main.go:23:12: ... argument does not escape
./main.go:23:50: &c == &d escapes to heap
&a: 0x1400010ae84
&b: 0x1400010ae84
&c: 0x104ec74a0
&d: 0x104ec74a0
&a == &b: false
x == y: true
&c(0x104ec74a0) == &d(0x104ec74a0): true
在 Go 語言中使用 go run 命令時(shí),可以通過 -gcflags 選項(xiàng)向 Go 編譯器傳遞多個(gè)標(biāo)志,這些標(biāo)志會(huì)影響編譯器的行為。
-m標(biāo)志用于啟動(dòng)編譯器的內(nèi)存逃逸分析。-N標(biāo)志用于禁用編譯器優(yōu)化。-l標(biāo)志用于禁用函數(shù)內(nèi)聯(lián)。
根據(jù)輸出可以發(fā)現(xiàn),變量 c 和 d 發(fā)生了內(nèi)存逃逸,并且最終二者的內(nèi)存地址相同,相等比較結(jié)果為 true。
而 a 和 b 兩個(gè)變量的輸出結(jié)果就比較有意思了,兩個(gè)變量沒有發(fā)生內(nèi)存逃逸,并且二者打印出來的內(nèi)存地址相同,但內(nèi)存地址相等比較結(jié)果卻為 false。
所以,我們可以推翻之前的結(jié)論,新結(jié)論為:「多個(gè)空結(jié)構(gòu)體內(nèi)存地址可能相同」。
在 Go 官方的語言規(guī)范中 Size and alignment guarantees 部分對(duì)關(guān)于空結(jié)構(gòu)體內(nèi)存地址進(jìn)行了說明:
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
大概意思是說:如果一個(gè)結(jié)構(gòu)體或數(shù)組類型不包含任何占用內(nèi)存大小大于零的字段(或元素),那么它的大小為零。兩個(gè)不同的零大小變量可能在內(nèi)存中具有相同的地址。
注意,這里說的是可能:may have the same。所以前文所述「多個(gè)空結(jié)構(gòu)體內(nèi)存地址相同」的結(jié)論并不準(zhǔn)確。
NOTE: 本文示例執(zhí)行結(jié)果基于 Go 1.22.0 版本,對(duì)于多個(gè)空結(jié)構(gòu)體內(nèi)存地址打印結(jié)果既存在相同情況,也存在不同情況,這跟 Go 編譯器實(shí)現(xiàn)有關(guān),后續(xù)實(shí)現(xiàn)可能會(huì)有變化。
另外,對(duì)于嵌套的空結(jié)構(gòu)體,其表現(xiàn)結(jié)果與普通空結(jié)構(gòu)體相同:
type Empty struct{}
type MultiEmpty struct {
A Empty
B struct{}
}
s1 := Empty{}
s2 := MultiEmpty{}
fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1))
fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2))
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0
空結(jié)構(gòu)體影響內(nèi)存對(duì)齊
空結(jié)構(gòu)體也并不是什么時(shí)候都不會(huì)占用內(nèi)存空間,比如空結(jié)構(gòu)體作為另一個(gè)結(jié)構(gòu)體字段時(shí),根據(jù)位置不同,可能因內(nèi)存對(duì)齊原因,導(dǎo)致外層結(jié)構(gòu)體大小不一樣:
type A struct {
x int
y string
z struct{}
}
type B struct {
x int
z struct{}
y string
}
type C struct {
z struct{}
x int
y string
}
a := A{}
b := B{}
c := C{}
fmt.Printf("struct a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("struct b size: %d\n", unsafe.Sizeof(b))
fmt.Printf("struct c size: %d\n", unsafe.Sizeof(c))
以上示例中,定義了三個(gè)結(jié)構(gòu)體 A、B、C,并且都定義了三個(gè)字段,類型分別是 int、string、struct{},空結(jié)構(gòu)體字段分別放在最后、中間、最前面不同的位置。
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
struct a size: 32
struct b size: 24
struct c size: 24
可以發(fā)現(xiàn),當(dāng)空結(jié)構(gòu)體放在另一個(gè)結(jié)構(gòu)體最后一個(gè)字段時(shí),會(huì)觸發(fā)內(nèi)存對(duì)齊。
此時(shí)外層結(jié)構(gòu)體會(huì)占用更多的內(nèi)存空間,所以如果你的程序?qū)?nèi)存要求比較嚴(yán)格,則在使用空結(jié)構(gòu)體作為字段時(shí)需要考慮這一點(diǎn)。
NOTE: 這里先挖個(gè)坑,我會(huì)再寫一篇 Go 中結(jié)構(gòu)體內(nèi)存對(duì)齊的文章,分析下為什么 struct{} 放在結(jié)構(gòu)體字段最后會(huì)出現(xiàn)內(nèi)存對(duì)齊現(xiàn)象,敬請(qǐng)期待。防止迷路,
空結(jié)構(gòu)體用法
根據(jù)前文的講解,我們對(duì) Go 中空結(jié)構(gòu)體的特性和一些使用時(shí)注意事項(xiàng)已經(jīng)有所了解,是時(shí)候探索空結(jié)構(gòu)體的用處了。
實(shí)現(xiàn) Set
首先,空結(jié)構(gòu)體最常用的地方,就是用來實(shí)現(xiàn) set(集合) 類型了。
我們知道 Go 語言在語法層面沒有提供 set 類型。不過我們可以很方便的使用 map + struct{} 來實(shí)現(xiàn) set 類型,代碼如下:
// Set 基于空結(jié)構(gòu)體實(shí)現(xiàn) set
type Set map[string]struct{}
// Add 添加元素到 set
func (s Set) Add(element string) {
s[element] = struct{}{}
}
// Remove 從 set 中移除元素
func (s Set) Remove(element string) {
delete(s, element)
}
// Contains 檢查 set 中是否包含指定元素
func (s Set) Contains(element string) bool {
_, exists := s[element]
return exists
}
// Size 返回 set 大小
func (s Set) Size() int {
return len(s)
}
// String implements fmt.Stringer
func (s Set) String() string {
format := "("
for element := range s {
format += element + " "
}
format = strings.TrimRight(format, " ") + ")"
return format
}
s := make(Set)
s.Add("one")
s.Add("two")
s.Add("three")
fmt.Printf("set: %s\n", s)
fmt.Printf("set size: %d\n", s.Size())
fmt.Printf("set contains 'one': %t\n", s.Contains("one"))
fmt.Printf("set contains 'onex': %t\n", s.Contains("onex"))
s.Remove("one")
fmt.Printf("set: %s\n", s)
fmt.Printf("set size: %d\n", s.Size())
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
set: (one two three)
set size: 3
set contains 'one': true
set contains 'onex': false
set: (three two)
set size: 2
使用 map 和空結(jié)構(gòu)體非常容易實(shí)現(xiàn) set 類型。map 的 key 實(shí)際上與 set 不重復(fù)的特性剛好一致,一個(gè)不需要關(guān)心 value 的 map 即為 set。
也正因?yàn)槿绱?,空結(jié)構(gòu)體類型最適合作為這個(gè)不需要關(guān)心的 value 的 map 了,因?yàn)樗?strong>不占空間,沒有語義。
也許有人會(huì)認(rèn)為使用 any 作為 map 的 value 也可以實(shí)現(xiàn) set。但其實(shí) any 是會(huì)占用空間的。
示例如下:
s := make(map[string]any)
s["t1"] = nil
s["t2"] = struct{}{}
fmt.Printf("set t1 value: %v, size: %d\n", s["t1"], unsafe.Sizeof(s["t1"]))
fmt.Printf("set t2 value: %v, size: %d\n", s["t2"], unsafe.Sizeof(s["t2"]))
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
set t1 value: <nil>, size: 16
set t2 value: {}, size: 16
可以發(fā)現(xiàn),any 類型的 value 是有大小的,所以并不合適。
日常開發(fā)中,我們還會(huì)用到一種 set 的慣用法:
s := map[string]struct{}{
"one": {},
"two": {},
"three": {},
}
for element := range s {
fmt.Println(element)
}
這種用法也比較常見,無需聲明一個(gè) set 類型,直接通過字面量定義一個(gè) value 為空結(jié)構(gòu)體的 map,非常方便。
申請(qǐng)超大容量 Array
基于空結(jié)構(gòu)體不占內(nèi)存空間的特性,我們可以考慮創(chuàng)建一個(gè)容量為 100 萬的 array:
var a [1000000]string
var b [1000000]struct{}
fmt.Printf("array a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("array b size: %d\n", unsafe.Sizeof(b))
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
array a size: 16000000
array b size: 0
使用空結(jié)構(gòu)體創(chuàng)建的 array 其大小依然為 0。
申請(qǐng)超大容量 Slice
我們還以考慮創(chuàng)建一個(gè)容量為 100 萬的 slice:
var a = make([]string, 1000000)
var b = make([]struct{}, 1000000)
fmt.Printf("slice a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("slice b size: %d\n", unsafe.Sizeof(b))
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
slice a size: 24
slice b size: 24
當(dāng)然,可以發(fā)現(xiàn),其實(shí)不管是否使用空結(jié)構(gòu)體,slice 只占用 header 的空間。
信號(hào)通知
空結(jié)構(gòu)體另一個(gè)我經(jīng)常使用的方法是與 channel 結(jié)合當(dāng)作信號(hào)來使用,示例如下:
done := make(chan struct{})
go func() {
time.Sleep(1 * time.Second) // 執(zhí)行一些操作...
fmt.Printf("goroutine done\n")
done <- struct{}{} // 發(fā)送完成信號(hào)
}()
fmt.Printf("waiting...\n")
<-done // 等待完成
fmt.Printf("main exit\n")
這段代碼中聲明了一個(gè)長度為 0 的 channel,其類型為 chan struct{}。
然后啟動(dòng)一個(gè) goroutine 執(zhí)行業(yè)務(wù)邏輯,主協(xié)程等待信號(hào)退出,二者使用 channel 進(jìn)行通信。
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
waiting...
goroutine done
main exit
主協(xié)程先輸出 waiting...,然后等待 1s,goroutine 輸出 goroutine done,接著主協(xié)程收到退出信號(hào),輸出 main exit 程序執(zhí)行完成。
由于 struct{} 并不占用內(nèi)存,所以實(shí)際上 channel 內(nèi)部只需要將計(jì)數(shù)器加一即可,不涉及數(shù)據(jù)傳輸,故沒有額外內(nèi)存開銷。
這段代碼還有另一種實(shí)現(xiàn):
done := make(chan struct{})
go func() {
time.Sleep(1 * time.Second) // 執(zhí)行一些操作...
fmt.Printf("goroutine done\n")
close(done) // 不需要發(fā)送 struct{}{},直接 close,發(fā)送完成信號(hào)
}()
fmt.Printf("waiting...\n")
<-done // 等待完成
fmt.Printf("main exit\n")
這里 goroutine 中都不需要發(fā)送空結(jié)構(gòu)體,直接對(duì) channel 進(jìn)行 close 就行了,struct{} 在這里起到的作用更像是一個(gè)「占位符」的作用。
在 Go 語言 context 源碼中也使用了 struct{} 作為完成信號(hào):
type Context interface {
Deadline() (deadline time.Time, ok bool)
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}
Err() error
Value(key any) any
}
context.Context 的 Done 方法返回值即為 chan struct{}。
無操作的方法接收器
有時(shí)候,我們需要“組合”一些方法,并且這些方法內(nèi)部并不會(huì)用到方法接收器,這時(shí)就可以使用 struct{} 作為方法接收器。
type NoOp struct{}
func (n NoOp) Perform() {
fmt.Println("Performing no operation.")
}
方法中代碼并沒有引用 n,如果換成其他類型則會(huì)占用內(nèi)存空間。
在實(shí)際開發(fā)過程中,有時(shí)候代碼寫到一半,為了編譯通過,我們也會(huì)寫出這種代碼,先寫出代碼整體框架,再實(shí)現(xiàn)內(nèi)部細(xì)節(jié)。
作為接口實(shí)現(xiàn)
用 struct{} 作為方法接收器,還有另一個(gè)用途,就是作為接口的實(shí)現(xiàn)。常用于忽略不需要的輸出,和單元測(cè)試。啥意思呢?往下看。
我們知道 Go 中有個(gè) io.Writer 接口:
type Writer interface {
Write(p []byte) (n int, err error)
}
我們還知道,Go 的 io 包中有個(gè) io.Discard 變量,它的主要作用是提供一個(gè)“黑洞”設(shè)備,任何寫入到 io.Discard 的數(shù)據(jù)都會(huì)被消耗掉而不會(huì)有任何效果(這類似于 Unix 中的 /dev/null 設(shè)備)。
io.Discard 定義如下:
// Discard is a [Writer] on which all Write calls succeed
// without doing anything.
var Discard Writer = discard{}
type discard struct{}
func (discard) Write(p []byte) (int, error) {
return len(p), nil
}
io.Discard 代碼定義極其簡單,它實(shí)現(xiàn)了 io.Writer 接口,并且這個(gè) Writer 方法的實(shí)現(xiàn)也極其簡單,什么都沒做直接返回。
根據(jù)注釋也能發(fā)現(xiàn),Writer 方法的目的就是啥都不做,所有調(diào)用都會(huì)成功,所以可以類比為 Unix 系統(tǒng)中的 /dev/null。
io.Discard 可以用于忽略日志:
// 設(shè)置日志輸出為 `io.Discard`,忽略所有日志
log.SetOutput(io.Discard)
// 這條日志不會(huì)在任何地方顯示
log.Println("This log will not be shown anywhere")
此外,我曾寫過一篇文章《在 Go 語言單元測(cè)試中如何解決 MySQL 存儲(chǔ)依賴問題》。里面有這樣一段示例代碼:
type UserStore interface {
Create(user *User) error
Get(id int) (*User, error)
}
...
type fakeUserStore struct{}
func (f *fakeUserStore) Create(user *store.User) error {
return nil
}
func (f *fakeUserStore) Get(id int) (*store.User, error) {
return &store.User{ID: id, Name: "test"}, nil
}
這就是空結(jié)構(gòu)體作為接口實(shí)現(xiàn)的另一種用途,編寫測(cè)試用 fake object 時(shí)非常有用。
即我們定義一個(gè) struct{} 類型 fakeUserStore,然后實(shí)現(xiàn) UserStore 接口,這樣在單元測(cè)試代碼中,就可以用 fakeUserStore 來替換真實(shí)的 UserStore 實(shí)例對(duì)象,以此來解決對(duì)象間的依賴問題。
標(biāo)識(shí)符
最后,我們?cè)賮斫榻B一種空結(jié)構(gòu)體比較好玩的用法。
相信很多同學(xué)都直接或間接的使用過 Go 中的 sync.Pool,其定義如下:
type Pool struct {
noCopy noCopy
local unsafe.Pointer
localSize uintptr
victim unsafe.Pointer
victimSize uintptr
New func() any
}
其中有一個(gè) noCopy 屬性,其定義如下:
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
noCopy 即為一個(gè)空結(jié)構(gòu)體,其實(shí)現(xiàn)也非常簡單,僅定義了兩個(gè)空方法。
而這個(gè) noCopy 屬性看似沒什么用,實(shí)際上卻有著大作用。這個(gè)字段的主要作用是阻止 sync.Pool 被意外復(fù)制。它是一種通過編譯器靜態(tài)分析來防止結(jié)構(gòu)體被不當(dāng)復(fù)制的技巧,以確保正確的使用和內(nèi)存安全性。
可以通過 go vet 命令檢測(cè)出 sync.Pool 是否被意外復(fù)制。
在這里,noCopy 屬性對(duì)當(dāng)前結(jié)構(gòu)體本身沒有作用,但可以將其作為一個(gè)是否允許復(fù)制的標(biāo)識(shí)符,有了這個(gè)標(biāo)記,就代表結(jié)構(gòu)體不能被復(fù)制,go vet 命令就可以檢查出來。
我們自定義的 struct 也可以通過嵌入 noCopy 屬性來實(shí)現(xiàn)禁止復(fù)制:
package main
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
func main() {
type A struct {
noCopy noCopy
a string
}
type B struct {
b string
}
a := A{a: "a"}
b := B{b: "b"}
_ = a
_ = b
}
使用 go vet 命令檢查是否存在意外的結(jié)構(gòu)體復(fù)制:
$ go vet main.go
# command-line-arguments
# [command-line-arguments]
./main.go:21:6: assignment copies lock value to _: command-line-arguments.A contains command-line-arguments.noCopy
可以發(fā)現(xiàn),go vet 已經(jīng)檢測(cè)出我們通過 _ = a 復(fù)制了 noCopy 結(jié)構(gòu)體 A。
總結(jié)
空結(jié)構(gòu)體 struct{} 在 Go 中雖小卻有著巧妙的用途。
從節(jié)省內(nèi)存的角度看,它是表示空概念的理想選擇。從語義上考慮,使用 struct{} 語義更明確,就是不關(guān)注值。
由于內(nèi)存對(duì)齊的影響,空結(jié)構(gòu)體字段順序可能影響外層結(jié)構(gòu)體的大小,建議將空結(jié)構(gòu)體放在外層結(jié)構(gòu)體的第一個(gè)字段。
無論是作使用空結(jié)構(gòu)體實(shí)現(xiàn)集合、信號(hào)通知、方法載體還是占位符等,struct{} 都顯示了其獨(dú)特的價(jià)值。
以上就是詳解Go語言中空結(jié)構(gòu)體的慣用法的詳細(xì)內(nèi)容,更多關(guān)于Go語言空結(jié)構(gòu)體的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Golang空結(jié)構(gòu)體struct{}用途,你知道嗎
- Go語言空結(jié)構(gòu)體詳解
- 一文帶你感受Go語言空結(jié)構(gòu)體的魔力
- GO語言中ni,零值與空結(jié)構(gòu)體的使用
- 關(guān)于Go 空結(jié)構(gòu)體的 3 種使用場(chǎng)景
- 細(xì)說Go語言中空結(jié)構(gòu)體的奇妙用途
- Go空結(jié)構(gòu)體struct{}的作用是什么
- 簡單聊聊Go語言中空結(jié)構(gòu)體和空字符串的特殊之處
- go語言中空結(jié)構(gòu)體的實(shí)現(xiàn)
- Golang 空結(jié)構(gòu)體特性與用法詳解
相關(guān)文章
Go語言中使用 buffered channel 實(shí)現(xiàn)線程安全的 pool
這篇文章主要介紹了Go語言中使用 buffered channel 實(shí)現(xiàn)線程安全的 pool,因?yàn)镚o語言自帶的sync.Pool并不是很好用,所以自己實(shí)現(xiàn)了一線程安全的 pool,需要的朋友可以參考下2014-10-10
Golang動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)示例
動(dòng)態(tài)數(shù)組能自動(dòng)調(diào)整大小,與靜態(tài)數(shù)組不同,其大小不固定,可根據(jù)需求變化,實(shí)現(xiàn)通常依賴于數(shù)據(jù)結(jié)構(gòu)如鏈表或數(shù)組加額外信息,本文就來介紹一下Golang動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)示例,感興趣的可以了解一下2024-10-10
7分鐘讀懂Go的臨時(shí)對(duì)象池pool以及其應(yīng)用場(chǎng)景
這篇文章主要給大家介紹了關(guān)于如何通過7分鐘讀懂Go的臨時(shí)對(duì)象池pool以及其應(yīng)用場(chǎng)景的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或使用Go具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2018-11-11
Go語言使用net/http構(gòu)建一個(gè)RESTful?API的示例代碼
Go的標(biāo)準(zhǔn)庫net/http提供了構(gòu)建Web服務(wù)所需的強(qiáng)大功能,雖然眾多第三方框架(如?Gin、Echo)已經(jīng)封裝了很多功能,但如果你真正想理解?Go的底層Web服務(wù)機(jī)制,那么使用net/http實(shí)現(xiàn)一個(gè)原生的RESTful?API?是最好的入門方式,需要的朋友可以參考下本文2025-08-08

