詳解Golang中string的實(shí)現(xiàn)原理與高效使用
字符串類(lèi)型是現(xiàn)代編程語(yǔ)言中最常使用的數(shù)據(jù)類(lèi)型之一。在Go語(yǔ)言的先祖之一C語(yǔ)言當(dāng)中,字符串類(lèi)型并沒(méi)有被顯式定義,而是以字符串字面值常量或以'\0'結(jié)尾的字符類(lèi)型(char)數(shù)組來(lái)呈現(xiàn)的。
const char * s = "hello world" char s[] = "hello gopher"
這給C程序員在使用字符串時(shí)帶來(lái)一些問(wèn)題,諸如:
- 類(lèi)型安全性差;
- 字符串操作要時(shí)時(shí)刻刻考慮結(jié)尾的'\0';
- 字符串?dāng)?shù)據(jù)可變(主要指以字符數(shù)組形式定義的字符串類(lèi)型);
- 獲取字符串長(zhǎng)度代價(jià)大(O(n)的時(shí)間復(fù)雜度);
- 未內(nèi)置對(duì)非ASCII字符(如中文字符)的處理。
Go語(yǔ)言修復(fù)了C語(yǔ)言的這一“缺陷”,內(nèi)置了string類(lèi)型,統(tǒng)一了對(duì)字符串的抽象。
在Go語(yǔ)言中,無(wú)論是字符串常量、字符串變量還是代碼中出現(xiàn)的字符串字面量,它們的類(lèi)型都被統(tǒng)一設(shè)置為string。
Go的string類(lèi)型設(shè)計(jì)充分吸取了C語(yǔ)言字符串設(shè)計(jì)的經(jīng)驗(yàn)教訓(xùn),并結(jié)合了其他主流語(yǔ)言在字符串類(lèi)型設(shè)計(jì)上的最佳實(shí)踐,最終呈現(xiàn)的string類(lèi)型具有如下功能特點(diǎn)。
(1)string類(lèi)型的數(shù)據(jù)是不可變的,一旦聲明了一個(gè)string類(lèi)型的標(biāo)識(shí)符,無(wú)論是常量還是變量,該標(biāo)識(shí)符所指代的數(shù)據(jù)在整個(gè)程序的生命周期內(nèi)便無(wú)法更改。下面嘗試修改一下string數(shù)據(jù),看看能得到怎樣的結(jié)果。
func main() { // 原始字符串 var s string = "hello" fmt.Println("original string:", s) // 切片化后試圖改變?cè)址? sl := []byte(s) sl[0] = 't' fmt.Println("slice:", string(sl)) fmt.Println("after reslice, the original string is:", string(s)) }
該程序的運(yùn)行結(jié)果如下:
$go run string.go
original string: hello
slice: tello
after reslice, the original string is: hello
(2)零值可用
Go string類(lèi)型支持“零值可用”的理念。Go字符串無(wú)須像C語(yǔ)言中那樣考慮結(jié)尾'\0'字符,因此其零值為"",長(zhǎng)度為0。
(3)獲取長(zhǎng)度的時(shí)間復(fù)雜度是O(1)級(jí)別
Go string類(lèi)型數(shù)據(jù)是不可變的,因此一旦有了初值,那塊數(shù)據(jù)就不會(huì)改變,其長(zhǎng)度也不會(huì)改變。Go將這個(gè)長(zhǎng)度作為一個(gè)字段存儲(chǔ)在運(yùn)行時(shí)的string類(lèi)型的內(nèi)部表示結(jié)構(gòu)中(后文有說(shuō)明)。這樣獲取string長(zhǎng)度的操作,即len(s)實(shí)際上就是讀取存儲(chǔ)在運(yùn)行時(shí)中的那個(gè)長(zhǎng)度值,這是一個(gè)代價(jià)極低的O(1)操作。
(4)支持通過(guò)+/+=操作符進(jìn)行字符串連接
對(duì)開(kāi)發(fā)者而言,通過(guò)+/+=操作符進(jìn)行的字符串連接是體驗(yàn)最好的字符串連接操作,Go語(yǔ)言支持這種操作:
s := "Rob Pike, " s = s + "Robert Griesemer, " s += " Ken Thompson"
(5)支持各種比較關(guān)系操作符:==、!= 、>=、<=、>和<
由于Go string是不可變的,因此如果兩個(gè)字符串的長(zhǎng)度不相同,那么無(wú)須比較具體字符串?dāng)?shù)據(jù)即可斷定兩個(gè)字符串是不同的。如果長(zhǎng)度相同,則要進(jìn)一步判斷數(shù)據(jù)指針是否指向同一塊底層存儲(chǔ)數(shù)據(jù)。如果相同,則兩個(gè)字符串是等價(jià)的;如果不同,則還需進(jìn)一步比對(duì)實(shí)際的數(shù)據(jù)內(nèi)容。
(6)對(duì)非ASCII字符提供原生支持
Go語(yǔ)言源文件默認(rèn)采用的Unicode字符集。Unicode字符集是目前市面上最流行的字符集,幾乎囊括了所有主流非ASCII字符(包括中文字符)。Go字符串的每個(gè)字符都是一個(gè)Unicode字符,并且這些Unicode字符是以UTF-8編碼格式存儲(chǔ)在內(nèi)存當(dāng)中的。我們來(lái)看一個(gè)例子:
// func main() { // 中文字符 Unicode碼點(diǎn) UTF8編碼 // 中 U+4E2D E4B8AD // 國(guó) U+56FD E59BBD // 歡 U+6B22 E6ACA2 // 迎 U+8FCE E8BF8E // 您 U+60A8 E682A8 s := "中國(guó)歡迎您" rs := []rune(s) sl := []byte(s) for i, v := range rs { var utf8Bytes []byte for j := i * 3; j < (i+1)*3; j++ { utf8Bytes = append(utf8Bytes, sl[j]) } fmt.Printf("%s => %X => %X\n", string(v), v, utf8Bytes) } } $go run 中 => 4E2D => E4B8AD 國(guó) => 56FD => E59BBD 歡 => 6B22 => E6ACA2 迎 => 8FCE => E8BF8E 您 => 60A8 => E682A8
我們看到字符串變量s中存儲(chǔ)的文本是“中國(guó)歡迎您”五個(gè)漢字字符(非ASCII字符范疇),這里輸出了每個(gè)中文字符對(duì)應(yīng)的Unicode碼點(diǎn)(Code Point,見(jiàn)輸出結(jié)果的第二列),一個(gè)rune對(duì)應(yīng)一個(gè)碼點(diǎn)。UTF-8編碼是Unicode碼點(diǎn)的一種字符編碼形式,是最常用的一種編碼格式,也是Go默認(rèn)的字符編碼格式。我們還可以使用其他字符編碼格式來(lái)映射Unicode碼點(diǎn),比如UTF-16等。
在UTF-8中,大多數(shù)中文字符都使用三字節(jié)表示。[]byte(s)的轉(zhuǎn)型讓我們獲得了s底層存儲(chǔ)的“復(fù)制品”,從而得到每個(gè)漢字字符對(duì)應(yīng)的UTF-8編碼字節(jié)(見(jiàn)輸出結(jié)果的第三列)。
(7)原生支持多行字符串
C語(yǔ)言中要構(gòu)造多行字符串,要么使用多個(gè)字符串的自然拼接,要么結(jié)合續(xù)行符“\”,很難控制好格式:
#include <stdio.h> char *s = "古藤老樹(shù)昏鴉\n" "小橋流水人家\n" "古道西風(fēng)瘦馬\n" "斷腸人在天涯"; int main() { printf("%s\n", s); }
go語(yǔ)言方式:
const s = `古藤老樹(shù)昏鴉 小橋流水人家 古道西風(fēng)瘦馬 斷腸人在天涯`; func main () { fmt.Println(s) }
字符串內(nèi)部結(jié)構(gòu)
Go string類(lèi)型上述特性的實(shí)現(xiàn)與Go運(yùn)行時(shí)對(duì)string類(lèi)型的內(nèi)部表示是分不開(kāi)的。Go string在運(yùn)行時(shí)表示為下面的結(jié)構(gòu):
// $GOROOT/src/runtime/string.go type stringStruct struct { str unsafe.Pointer len int }
我們看到string類(lèi)型也是一個(gè)描述符,它本身并不真正存儲(chǔ)數(shù)據(jù),而僅是由一個(gè)指向底層存儲(chǔ)的指針和字符串的長(zhǎng)度字段組成。我們結(jié)合一個(gè)string的實(shí)例化過(guò)程來(lái)看。下面是runtime包中實(shí)例化一個(gè)字符串對(duì)應(yīng)的函數(shù):
// $GOROOT/src/runtime/string.go func rawstring(size int) (s string, b []byte) { p := mallocgc(uintptr(size), nil, false) stringStructOf(&s).str = p stringStructOf(&s).len = size *(*slice)(unsafe.Pointer(&b)) = slice{p, size, size} return }
我們看到每個(gè)字符串類(lèi)型變量/常量對(duì)應(yīng)一個(gè)stringStruct實(shí)例,經(jīng)過(guò)rawstring實(shí)例化后,stringStruct中的str指針指向真正存儲(chǔ)字符串?dāng)?shù)據(jù)的底層內(nèi)存區(qū)域,len字段存儲(chǔ)的是字符串的長(zhǎng)度(這里是5);rawstring同時(shí)還創(chuàng)建了一個(gè)臨時(shí)slice,該slice的array指針也指向存儲(chǔ)字符串?dāng)?shù)據(jù)的底層內(nèi)存區(qū)域。注意,rawstring調(diào)用后,新申請(qǐng)的內(nèi)存區(qū)域還未被寫(xiě)入數(shù)據(jù),該slice就是供后續(xù)運(yùn)行時(shí)層向其中寫(xiě)入數(shù)據(jù)("hello")用的。寫(xiě)完數(shù)據(jù)后,該slice就可以被回收掉了
根據(jù)string在運(yùn)行時(shí)的表示可以得到這樣一個(gè)結(jié)論:直接將string類(lèi)型通過(guò)函數(shù)/方法參數(shù)傳入也不會(huì)有太多的損耗,因?yàn)閭魅氲膬H僅是一個(gè)“描述符”,而不是真正的字符串?dāng)?shù)據(jù)。我們通過(guò)一個(gè)簡(jiǎn)單的基準(zhǔn)測(cè)試來(lái)驗(yàn)證一下:
// var s string = `Go, also known as Golang, is a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and communicating sequential processes (CSP)-style concurrency.` func handleString(s string) { _ = s + "hello" } func handlePtrToString(s *string) { _ = *s + "hello" } func BenchmarkHandleString(b *testing.B) { for n := 0; n < b.N; n++ { handleString(s) } } func BenchmarkHandlePtrToString(b *testing.B) { for n := 0; n < b.N; n++ { handlePtrToString(&s) } }
$go test -bench . -benchmem string_as_param_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkHandleString-8 15668872 70.7 ns/op 320 B/op 1 allocs/op
BenchmarkHandlePtrToString-8 15809401 71.8 ns/op 320 B/op 1 allocs/op
PASS
ok command-line-arguments 2.407s
我們看到直接傳入string與傳入string指針兩者的基準(zhǔn)測(cè)試結(jié)果幾乎一模一樣,因此Gopher大可放心地直接使用string作為函數(shù)/方法參數(shù)類(lèi)型。
高效構(gòu)造
前面提到過(guò),Go原生支持通過(guò)+/+=操作符來(lái)連接多個(gè)字符串以構(gòu)造一個(gè)更長(zhǎng)的字符串,并且通過(guò)+/+=操作符的字符串連接構(gòu)造是最自然、開(kāi)發(fā)體驗(yàn)最好的一種。但Go還提供了其他一些構(gòu)造字符串的方法,比如:
使用fmt.Sprintf;使用strings.Join;使用strings.Builder;使用bytes.Buffer。
在這些方法中哪種方法最為高效呢?我們使用基準(zhǔn)測(cè)試的數(shù)據(jù)作為參考:
// var sl []string = []string{ "Rob Pike ", "Robert Griesemer ", "Ken Thompson ", } func concatStringByOperator(sl []string) string { var s string for _, v := range sl { s += v } return s } func concatStringBySprintf(sl []string) string { var s string for _, v := range sl { s = fmt.Sprintf("%s%s", s, v) } return s } func concatStringByJoin(sl []string) string { return strings.Join(sl, "") } func concatStringByStringsBuilder(sl []string) string { var b strings.Builder for _, v := range sl { b.WriteString(v) } return b.String() } func concatStringByStringsBuilderWithInitSize(sl []string) string { var b strings.Builder b.Grow(64) for _, v := range sl { b.WriteString(v) } return b.String() } func concatStringByBytesBuffer(sl []string) string { var b bytes.Buffer for _, v := range sl { b.WriteString(v) } return b.String() } func concatStringByBytesBufferWithInitSize(sl []string) string { buf := make([]byte, 0, 64) b := bytes.NewBuffer(buf) for _, v := range sl { b.WriteString(v) } return b.String() } func BenchmarkConcatStringByOperator(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByOperator(sl) } } func BenchmarkConcatStringBySprintf(b *testing.B) { for n := 0; n < b.N; n++ { concatStringBySprintf(sl) } } func BenchmarkConcatStringByJoin(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByJoin(sl) } } func BenchmarkConcatStringByStringsBuilder(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByStringsBuilder(sl) } } func BenchmarkConcatStringByStringsBuilderWithInitSize(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByStringsBuilderWithInitSize(sl) } } func BenchmarkConcatStringByBytesBuffer(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByBytesBuffer(sl) } } func BenchmarkConcatStringByBytesBufferWithInitSize(b *testing.B) { for n := 0; n < b.N; n++ { concatStringByBytesBufferWithInitSize(sl) } }
運(yùn)行:
$go test -bench=. -benchmem ./string_concat_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkConcatStringByOperator-8 11744653 89.1 ns/op 80 B/op 2 allocs/op
BenchmarkConcatStringBySprintf-8 2792876 420 ns/op 176 B/op 8 allocs/op
BenchmarkConcatStringByJoin-8 22923051 49.1 ns/op 48 B/op 1 allocs/op
BenchmarkConcatStringByStringsBuilder-8 11347185 96.6 ns/op 112 B/op 3 allocs/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8 26315769 42.3 ns/op 64 B/op 1 allocs/op
BenchmarkConcatStringByBytesBuffer-8 14265033 82.6 ns/op 112 B/op 2 allocs/op
BenchmarkConcatStringByBytesBufferWithInitSize-8 24777525 48.1 ns/op 48 B/op 1 allocs/op
PASS
ok command-line-arguments 8.816s
從基準(zhǔn)測(cè)試的輸出結(jié)果的第三列,即每操作耗時(shí)的數(shù)值來(lái)看:做了預(yù)初始化的strings.Builder連接構(gòu)建字符串效率最高;帶有預(yù)初始化的bytes.Buffer和strings.Join這兩種方法效率十分接近,分列二三位;未做預(yù)初始化的strings.Builder、bytes.Buffer和操作符連接在第三檔次;fmt.Sprintf性能最差,排在末尾。由此可以得出一些結(jié)論:在能預(yù)估出最終字符串長(zhǎng)度的情況下,使用預(yù)初始化的strings.Builder連接構(gòu)建字符串效率最高;strings.Join連接構(gòu)建字符串的平均性能最穩(wěn)定,如果輸入的多個(gè)字符串是以[]string承載的,那么strings.Join也是不錯(cuò)的選擇;使用操作符連接的方式最直觀、最自然,在編譯器知曉欲連接的字符串個(gè)數(shù)的情況下,使用此種方式可以得到編譯器的優(yōu)化處理;fmt.Sprintf雖然效率不高,但也不是一無(wú)是處,如果是由多種不同類(lèi)型變量來(lái)構(gòu)建特定格式的字符串,那么這種方式還是最適合的。
高效轉(zhuǎn)換
在前面的例子中,我們看到了string到[]rune以及string到[]byte的轉(zhuǎn)換,這兩個(gè)轉(zhuǎn)換也是可逆的,也就是說(shuō)string和[]rune、[]byte可以雙向轉(zhuǎn)換。下面就是從[]rune或[]byte反向轉(zhuǎn)換為string的例子:
// func main() { rs := []rune{ 0x4E2D, 0x56FD, 0x6B22, 0x8FCE, 0x60A8, } s := string(rs) fmt.Println(s) sl := []byte{ 0xE4, 0xB8, 0xAD, 0xE5, 0x9B, 0xBD, 0xE6, 0xAC, 0xA2, 0xE8, 0xBF, 0x8E, 0xE6, 0x82, 0xA8, } s = string(sl) fmt.Println(s) } $go run string_slice_to_string.go 中國(guó)歡迎您 中國(guó)歡迎您
無(wú)論是string轉(zhuǎn)slice還是slice轉(zhuǎn)string,轉(zhuǎn)換都是要付出代價(jià)的,這些代價(jià)的根源在于string是不可變的,運(yùn)行時(shí)要為轉(zhuǎn)換后的類(lèi)型分配新內(nèi)存。我們以byte slice與string相互轉(zhuǎn)換為例,看看轉(zhuǎn)換過(guò)程的內(nèi)存分配情況:
// func byteSliceToString() { sl := []byte{ 0xE4, 0xB8, 0xAD, 0xE5, 0x9B, 0xBD, 0xE6, 0xAC, 0xA2, 0xE8, 0xBF, 0x8E, 0xE6, 0x82, 0xA8, 0xEF, 0xBC, 0x8C, 0xE5, 0x8C, 0x97, 0xE4, 0xBA, 0xAC, 0xE6, 0xAC, 0xA2, 0xE8, 0xBF, 0x8E, 0xE6, 0x82, 0xA8, } _ = string(sl) } func stringToByteSlice() { s := "中國(guó)歡迎您,北京歡迎您" _ = []byte(s) } func main() { fmt.Println(testing.AllocsPerRun(1, byteSliceToString)) fmt.Println(testing.AllocsPerRun(1, stringToByteSlice)) }
運(yùn)行:
go run
1
1
我們看到,針對(duì)“中國(guó)歡迎您,北京歡迎您”這個(gè)長(zhǎng)度的字符串,在string與byte slice互轉(zhuǎn)的過(guò)程中都要有一次內(nèi)存分配操作。
在Go運(yùn)行時(shí)層面,字符串與rune slice、byte slice相互轉(zhuǎn)換對(duì)應(yīng)的函數(shù)如下:
// $GOROOT/src/runtime/string.go slicebytetostring: []byte -> string slicerunetostring: []rune -> string stringtoslicebyte: string -> []byte stringtoslicerune: string -> []rune
以byte slice為例,看看slicebytetostring和stringtoslicebyte的實(shí)現(xiàn):
// $GOROOT/src/runtime/string.go const tmpStringBufSize = 32 type tmpBuf [tmpStringBufSize]byte func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { b = rawbyteslice(len(s)) } copy(b, s) return b } func slicebytetostring(buf *tmpBuf, b []byte) (str string) { l := len(b) if l == 0 { return "" } // 此處省略一些代碼 if l == 1 { stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]]) stringStructOf(&str).len = 1 return } var p unsafe.Pointer if buf != nil && len(b) <= len(buf) { p = unsafe.Pointer(buf) } else { p = mallocgc(uintptr(len(b)), nil, false) } stringStructOf(&str).str = p stringStructOf(&str).len = len(b) memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b))) return }
想要更高效地進(jìn)行轉(zhuǎn)換,唯一的方法就是減少甚至避免額外的內(nèi)存分配操作。我們看到運(yùn)行時(shí)實(shí)現(xiàn)轉(zhuǎn)換的函數(shù)中已經(jīng)加入了一些避免每種情況都要分配新內(nèi)存操作的優(yōu)化(如tmpBuf的復(fù)用)。slice類(lèi)型是不可比較的,而string類(lèi)型是可比較的,因此在日常Go編碼中,我們會(huì)經(jīng)常遇到將slice臨時(shí)轉(zhuǎn)換為string的情況。Go編譯器為這樣的場(chǎng)景提供了優(yōu)化。在運(yùn)行時(shí)中有一個(gè)名為slicebytetostringtmp的函數(shù)就是協(xié)助實(shí)現(xiàn)這一優(yōu)化的:
// $GOROOT/src/runtime/string.go func slicebytetostringtmp(b []byte) string { if raceenabled && len(b) > 0 { racereadrangepc(unsafe.Pointer(&b[0]), uintptr(len(b)), getcallerpc(), funcPC(slicebytetostringtmp)) } if msanenabled && len(b) > 0 { msanread(unsafe.Pointer(&b[0]), uintptr(len(b))) } return *(*string)(unsafe.Pointer(&b)) }
該函數(shù)的“秘訣”就在于不為string新開(kāi)辟一塊內(nèi)存,而是直接使用slice的底層存儲(chǔ)。當(dāng)然使用這個(gè)函數(shù)的前提是:在原slice被修改后,這個(gè)string不能再被使用了。因此這樣的優(yōu)化是針對(duì)以下幾個(gè)特定場(chǎng)景的。
(1)string(b)用在map類(lèi)型的key中
(2)string(b)用在字符串連接語(yǔ)句中
(3)string(b)用在字符串比較中
Go編譯器對(duì)用在for-range循環(huán)中的string到[]byte的轉(zhuǎn)換也有優(yōu)化處理,它不會(huì)為[]byte進(jìn)行額外的內(nèi)存分配,而是直接使用string的底層數(shù)據(jù)??聪旅娴睦?/p>
func convert() { s := "中國(guó)歡迎您,北京歡迎您" sl := []byte(s) for _, v := range sl { _ = v } } func convertWithOptimize() { s := "中國(guó)歡迎您,北京歡迎您" for _, v := range []byte(s) { _ = v } } func main() { fmt.Println(testing.AllocsPerRun(1, convert)) fmt.Println(testing.AllocsPerRun(1, convertWithOptimize)) }
運(yùn)行;
$go run
1
0
從結(jié)果我們看到,convertWithOptimize函數(shù)將string到[]byte的轉(zhuǎn)換放在for-range循環(huán)中,Go編譯器對(duì)其進(jìn)行了優(yōu)化,節(jié)省了一次內(nèi)存分配操作。
以上就是詳解Golang中string的實(shí)現(xiàn)原理與高效使用的詳細(xì)內(nèi)容,更多關(guān)于Go string的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言常見(jiàn)數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)詳解
這篇文章主要為大家學(xué)習(xí)介紹了Go語(yǔ)言中的常見(jiàn)數(shù)據(jù)結(jié)構(gòu)(channal、slice和map)的實(shí)現(xiàn),文中的示例代碼簡(jiǎn)潔易懂,需要的可以參考一下2023-07-07Golang cron 定時(shí)器和定時(shí)任務(wù)的使用場(chǎng)景
Ticker是一個(gè)周期觸發(fā)定時(shí)的計(jì)時(shí)器,它會(huì)按照一個(gè)時(shí)間間隔往channel發(fā)送系統(tǒng)當(dāng)前時(shí)間,而channel的接收者可以以固定的時(shí)間間隔從channel中讀取事件,這篇文章主要介紹了Golang cron 定時(shí)器和定時(shí)任務(wù),需要的朋友可以參考下2022-09-09go開(kāi)源Hugo站點(diǎn)構(gòu)建三步曲之集結(jié)渲染
這篇文章主要為大家介紹了go開(kāi)源Hugo站點(diǎn)構(gòu)建三步曲之集結(jié)渲染詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Go?實(shí)現(xiàn)?Nginx?加權(quán)輪詢(xún)算法的方法步驟
本文主要介紹了Go?實(shí)現(xiàn)?Nginx?加權(quán)輪詢(xún)算法的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12