欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Golang中string的實現(xiàn)原理與高效使用

 更新時間:2024年01月22日 08:06:21   作者:半芽灣  
在Go語言中,無論是字符串常量、字符串變量還是代碼中出現(xiàn)的字符串字面量,它們的類型都被統(tǒng)一設(shè)置為string,下面就跟隨小編一起來了解一下Golang中string的實現(xiàn)原理與高效使用吧

字符串類型是現(xiàn)代編程語言中最常使用的數(shù)據(jù)類型之一。在Go語言的先祖之一C語言當中,字符串類型并沒有被顯式定義,而是以字符串字面值常量或以'\0'結(jié)尾的字符類型(char)數(shù)組來呈現(xiàn)的。

const char * s = "hello world" 
char s[] = "hello gopher"

這給C程序員在使用字符串時帶來一些問題,諸如:

  • 類型安全性差;
  • 字符串操作要時時刻刻考慮結(jié)尾的'\0';
  • 字符串數(shù)據(jù)可變(主要指以字符數(shù)組形式定義的字符串類型);
  • 獲取字符串長度代價大(O(n)的時間復雜度);
  • 未內(nèi)置對非ASCII字符(如中文字符)的處理。

Go語言修復了C語言的這一“缺陷”,內(nèi)置了string類型,統(tǒng)一了對字符串的抽象。

在Go語言中,無論是字符串常量、字符串變量還是代碼中出現(xiàn)的字符串字面量,它們的類型都被統(tǒng)一設(shè)置為string。

Go的string類型設(shè)計充分吸取了C語言字符串設(shè)計的經(jīng)驗教訓,并結(jié)合了其他主流語言在字符串類型設(shè)計上的最佳實踐,最終呈現(xiàn)的string類型具有如下功能特點。

(1)string類型的數(shù)據(jù)是不可變的,一旦聲明了一個string類型的標識符,無論是常量還是變量,該標識符所指代的數(shù)據(jù)在整個程序的生命周期內(nèi)便無法更改。下面嘗試修改一下string數(shù)據(jù),看看能得到怎樣的結(jié)果。

func main() {
    // 原始字符串
    var s string = "hello"
    fmt.Println("original string:", s)

    // 切片化后試圖改變原字符串
    sl := []byte(s)
    sl[0] = 't'
    fmt.Println("slice:", string(sl))
    fmt.Println("after reslice, the original string is:", string(s))
}

該程序的運行結(jié)果如下:

$go run string.go
original string: hello
slice: tello
after reslice, the original string is: hello

(2)零值可用

Go string類型支持“零值可用”的理念。Go字符串無須像C語言中那樣考慮結(jié)尾'\0'字符,因此其零值為"",長度為0。

(3)獲取長度的時間復雜度是O(1)級別

Go string類型數(shù)據(jù)是不可變的,因此一旦有了初值,那塊數(shù)據(jù)就不會改變,其長度也不會改變。Go將這個長度作為一個字段存儲在運行時的string類型的內(nèi)部表示結(jié)構(gòu)中(后文有說明)。這樣獲取string長度的操作,即len(s)實際上就是讀取存儲在運行時中的那個長度值,這是一個代價極低的O(1)操作。

(4)支持通過+/+=操作符進行字符串連接

對開發(fā)者而言,通過+/+=操作符進行的字符串連接是體驗最好的字符串連接操作,Go語言支持這種操作:

s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"

(5)支持各種比較關(guān)系操作符:==、!= 、>=、<=、>和<

由于Go string是不可變的,因此如果兩個字符串的長度不相同,那么無須比較具體字符串數(shù)據(jù)即可斷定兩個字符串是不同的。如果長度相同,則要進一步判斷數(shù)據(jù)指針是否指向同一塊底層存儲數(shù)據(jù)。如果相同,則兩個字符串是等價的;如果不同,則還需進一步比對實際的數(shù)據(jù)內(nèi)容。

(6)對非ASCII字符提供原生支持

Go語言源文件默認采用的Unicode字符集。Unicode字符集是目前市面上最流行的字符集,幾乎囊括了所有主流非ASCII字符(包括中文字符)。Go字符串的每個字符都是一個Unicode字符,并且這些Unicode字符是以UTF-8編碼格式存儲在內(nèi)存當中的。我們來看一個例子:

// 

func main() {
    // 中文字符  Unicode碼點                 UTF8編碼
    //  中          U+4E2D                  E4B8AD
    //  國          U+56FD                  E59BBD
    //  歡          U+6B22                  E6ACA2
    //  迎          U+8FCE                  E8BF8E
    //  您          U+60A8                  E682A8
    s := "中國歡迎您"
    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
國 => 56FD => E59BBD
歡 => 6B22 => E6ACA2
迎 => 8FCE => E8BF8E
您 => 60A8 => E682A8

我們看到字符串變量s中存儲的文本是“中國歡迎您”五個漢字字符(非ASCII字符范疇),這里輸出了每個中文字符對應(yīng)的Unicode碼點(Code Point,見輸出結(jié)果的第二列),一個rune對應(yīng)一個碼點。UTF-8編碼是Unicode碼點的一種字符編碼形式,是最常用的一種編碼格式,也是Go默認的字符編碼格式。我們還可以使用其他字符編碼格式來映射Unicode碼點,比如UTF-16等。

在UTF-8中,大多數(shù)中文字符都使用三字節(jié)表示。[]byte(s)的轉(zhuǎn)型讓我們獲得了s底層存儲的“復制品”,從而得到每個漢字字符對應(yīng)的UTF-8編碼字節(jié)(見輸出結(jié)果的第三列)。

(7)原生支持多行字符串

C語言中要構(gòu)造多行字符串,要么使用多個字符串的自然拼接,要么結(jié)合續(xù)行符“\”,很難控制好格式:

#include <stdio.h>

char *s = "古藤老樹昏鴉\n"
"小橋流水人家\n"
"古道西風瘦馬\n"
"斷腸人在天涯";


int main() {
    printf("%s\n", s);
}

go語言方式:

const s = `古藤老樹昏鴉
小橋流水人家
古道西風瘦馬
斷腸人在天涯`;


func main () {
fmt.Println(s)
}

字符串內(nèi)部結(jié)構(gòu)

Go string類型上述特性的實現(xiàn)與Go運行時對string類型的內(nèi)部表示是分不開的。Go string在運行時表示為下面的結(jié)構(gòu):

// $GOROOT/src/runtime/string.go
type stringStruct struct {
    str unsafe.Pointer
    len int
}

我們看到string類型也是一個描述符,它本身并不真正存儲數(shù)據(jù),而僅是由一個指向底層存儲的指針和字符串的長度字段組成。我們結(jié)合一個string的實例化過程來看。下面是runtime包中實例化一個字符串對應(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
}

我們看到每個字符串類型變量/常量對應(yīng)一個stringStruct實例,經(jīng)過rawstring實例化后,stringStruct中的str指針指向真正存儲字符串數(shù)據(jù)的底層內(nèi)存區(qū)域,len字段存儲的是字符串的長度(這里是5);rawstring同時還創(chuàng)建了一個臨時slice,該slice的array指針也指向存儲字符串數(shù)據(jù)的底層內(nèi)存區(qū)域。注意,rawstring調(diào)用后,新申請的內(nèi)存區(qū)域還未被寫入數(shù)據(jù),該slice就是供后續(xù)運行時層向其中寫入數(shù)據(jù)("hello")用的。寫完數(shù)據(jù)后,該slice就可以被回收掉了

根據(jù)string在運行時的表示可以得到這樣一個結(jié)論:直接將string類型通過函數(shù)/方法參數(shù)傳入也不會有太多的損耗,因為傳入的僅僅是一個“描述符”,而不是真正的字符串數(shù)據(jù)。我們通過一個簡單的基準測試來驗證一下:

// 

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指針兩者的基準測試結(jié)果幾乎一模一樣,因此Gopher大可放心地直接使用string作為函數(shù)/方法參數(shù)類型。

高效構(gòu)造

前面提到過,Go原生支持通過+/+=操作符來連接多個字符串以構(gòu)造一個更長的字符串,并且通過+/+=操作符的字符串連接構(gòu)造是最自然、開發(fā)體驗最好的一種。但Go還提供了其他一些構(gòu)造字符串的方法,比如:

使用fmt.Sprintf;使用strings.Join;使用strings.Builder;使用bytes.Buffer。

在這些方法中哪種方法最為高效呢?我們使用基準測試的數(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)
    }
}

運行:

$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

從基準測試的輸出結(jié)果的第三列,即每操作耗時的數(shù)值來看:做了預初始化的strings.Builder連接構(gòu)建字符串效率最高;帶有預初始化的bytes.Buffer和strings.Join這兩種方法效率十分接近,分列二三位;未做預初始化的strings.Builder、bytes.Buffer和操作符連接在第三檔次;fmt.Sprintf性能最差,排在末尾。由此可以得出一些結(jié)論:在能預估出最終字符串長度的情況下,使用預初始化的strings.Builder連接構(gòu)建字符串效率最高;strings.Join連接構(gòu)建字符串的平均性能最穩(wěn)定,如果輸入的多個字符串是以[]string承載的,那么strings.Join也是不錯的選擇;使用操作符連接的方式最直觀、最自然,在編譯器知曉欲連接的字符串個數(shù)的情況下,使用此種方式可以得到編譯器的優(yōu)化處理;fmt.Sprintf雖然效率不高,但也不是一無是處,如果是由多種不同類型變量來構(gòu)建特定格式的字符串,那么這種方式還是最適合的。

高效轉(zhuǎn)換

在前面的例子中,我們看到了string到[]rune以及string到[]byte的轉(zhuǎn)換,這兩個轉(zhuǎn)換也是可逆的,也就是說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
中國歡迎您
中國歡迎您

無論是string轉(zhuǎn)slice還是slice轉(zhuǎn)string,轉(zhuǎn)換都是要付出代價的,這些代價的根源在于string是不可變的,運行時要為轉(zhuǎn)換后的類型分配新內(nèi)存。我們以byte slice與string相互轉(zhuǎn)換為例,看看轉(zhuǎn)換過程的內(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 := "中國歡迎您,北京歡迎您"
    _ = []byte(s)
}

func main() {
    fmt.Println(testing.AllocsPerRun(1, byteSliceToString))
    fmt.Println(testing.AllocsPerRun(1, stringToByteSlice))
}

運行:

go run

1
1

我們看到,針對“中國歡迎您,北京歡迎您”這個長度的字符串,在string與byte slice互轉(zhuǎn)的過程中都要有一次內(nèi)存分配操作。

在Go運行時層面,字符串與rune slice、byte slice相互轉(zhuǎn)換對應(yīng)的函數(shù)如下:

// $GOROOT/src/runtime/string.go
slicebytetostring: []byte -> string
slicerunetostring: []rune -> string
stringtoslicebyte: string -> []byte
stringtoslicerune: string -> []rune

以byte slice為例,看看slicebytetostring和stringtoslicebyte的實現(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
}

想要更高效地進行轉(zhuǎn)換,唯一的方法就是減少甚至避免額外的內(nèi)存分配操作。我們看到運行時實現(xiàn)轉(zhuǎn)換的函數(shù)中已經(jīng)加入了一些避免每種情況都要分配新內(nèi)存操作的優(yōu)化(如tmpBuf的復用)。slice類型是不可比較的,而string類型是可比較的,因此在日常Go編碼中,我們會經(jīng)常遇到將slice臨時轉(zhuǎn)換為string的情況。Go編譯器為這樣的場景提供了優(yōu)化。在運行時中有一個名為slicebytetostringtmp的函數(shù)就是協(xié)助實現(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新開辟一塊內(nèi)存,而是直接使用slice的底層存儲。當然使用這個函數(shù)的前提是:在原slice被修改后,這個string不能再被使用了。因此這樣的優(yōu)化是針對以下幾個特定場景的。

(1)string(b)用在map類型的key中

(2)string(b)用在字符串連接語句中

(3)string(b)用在字符串比較中

Go編譯器對用在for-range循環(huán)中的string到[]byte的轉(zhuǎn)換也有優(yōu)化處理,它不會為[]byte進行額外的內(nèi)存分配,而是直接使用string的底層數(shù)據(jù)??聪旅娴睦?/p>

func convert() {
    s := "中國歡迎您,北京歡迎您"
    sl := []byte(s)
    for _, v := range sl {
        _ = v
    }
}
func convertWithOptimize() {
    s := "中國歡迎您,北京歡迎您"
    for _, v := range []byte(s) {
        _ = v
    }
}

func main() {
    fmt.Println(testing.AllocsPerRun(1, convert))
    fmt.Println(testing.AllocsPerRun(1, convertWithOptimize))
}

運行;

$go run 
1
0

從結(jié)果我們看到,convertWithOptimize函數(shù)將string到[]byte的轉(zhuǎn)換放在for-range循環(huán)中,Go編譯器對其進行了優(yōu)化,節(jié)省了一次內(nèi)存分配操作。

以上就是詳解Golang中string的實現(xiàn)原理與高效使用的詳細內(nèi)容,更多關(guān)于Go string的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解Go中指針的原理與引用

    詳解Go中指針的原理與引用

    在?Go?中,指針是強大而重要的功能,它允許開發(fā)人員直接處理內(nèi)存地址并實現(xiàn)高效的數(shù)據(jù)操作,本文主要帶大家了解下指針在?Go?中的工作原理以及對于編寫高效、高性能代碼的重要性,希望對大家有所幫助
    2023-09-09
  • golang 格式化輸入輸出操作

    golang 格式化輸入輸出操作

    這篇文章主要介紹了golang 格式化輸入輸出操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • GO語言實現(xiàn)的端口掃描器分享

    GO語言實現(xiàn)的端口掃描器分享

    這篇文章主要介紹了GO語言實現(xiàn)的端口掃描器分享,本文直接給出實現(xiàn)代碼,代碼中包含大量注釋,需要的朋友可以參考下
    2014-10-10
  • Go語言常見數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)詳解

    Go語言常見數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)詳解

    這篇文章主要為大家學習介紹了Go語言中的常見數(shù)據(jù)結(jié)構(gòu)(channal、slice和map)的實現(xiàn),文中的示例代碼簡潔易懂,需要的可以參考一下
    2023-07-07
  • Golang cron 定時器和定時任務(wù)的使用場景

    Golang cron 定時器和定時任務(wù)的使用場景

    Ticker是一個周期觸發(fā)定時的計時器,它會按照一個時間間隔往channel發(fā)送系統(tǒng)當前時間,而channel的接收者可以以固定的時間間隔從channel中讀取事件,這篇文章主要介紹了Golang cron 定時器和定時任務(wù),需要的朋友可以參考下
    2022-09-09
  • Golang獲取目錄下的文件及目錄信息操作

    Golang獲取目錄下的文件及目錄信息操作

    這篇文章主要介紹了Golang獲取目錄下的文件及目錄信息操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • go開源Hugo站點構(gòu)建三步曲之集結(jié)渲染

    go開源Hugo站點構(gòu)建三步曲之集結(jié)渲染

    這篇文章主要為大家介紹了go開源Hugo站點構(gòu)建三步曲之集結(jié)渲染詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • golang使用mapstructure解析json

    golang使用mapstructure解析json

    mapstructure?是一個?Go?庫,用于將通用映射值解碼為結(jié)構(gòu),這篇文章主要來和大家介紹一下golang如何使用mapstructure解析json,需要的可以參考下
    2023-12-12
  • go語言中for?range使用方法及避坑指南

    go語言中for?range使用方法及避坑指南

    Go中的for range組合可以和方便的實現(xiàn)對一個數(shù)組或切片進行遍歷,但是在某些情況下使用for range時很可能就會被"坑",下面這篇文章主要給大家介紹了關(guān)于go語言中for?range使用方法及避坑指南的相關(guān)資料,需要的朋友可以參考下
    2022-09-09
  • Go?實現(xiàn)?Nginx?加權(quán)輪詢算法的方法步驟

    Go?實現(xiàn)?Nginx?加權(quán)輪詢算法的方法步驟

    本文主要介紹了Go?實現(xiàn)?Nginx?加權(quán)輪詢算法的方法步驟,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-12-12

最新評論