成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Go 1.20 相比 Go 1.19 有哪些值得注意的改動(dòng)?

開(kāi)發(fā) 前端
Rewrite?? 鉤子和 ??ProxyRequest?? 類型為 ??httputil.ReverseProxy?? 提供了更現(xiàn)代化、更安全、更靈活的定制方式,推薦在新的 Go 1.20+ 項(xiàng)目中使用 ??Rewrite?? 來(lái)替代 ??Director??。

https://go.dev/doc/go1.20

Go 1.20 值得關(guān)注的改動(dòng):

  1. 語(yǔ)言 Slice to Array 轉(zhuǎn)換: Go 1.20 擴(kuò)展了 Go 1.17 的功能,允許直接將 slice 轉(zhuǎn)換為固定大小的數(shù)組,例如使用 [4]byte(x) 替代 *(*[4]byte)(x)。
  2. unsafe 包更新: 新增 SliceData 、 String 和 StringData 函數(shù),與 Go 1.17 的 Slice 函數(shù)一起,提供了不依賴具體內(nèi)存布局來(lái)構(gòu)造和解構(gòu) slice 與 string 值的能力。
  3. 規(guī)范更新 結(jié)構(gòu)體與數(shù)組比較: 語(yǔ)言規(guī)范明確了結(jié)構(gòu)體按字段聲明順序、數(shù)組按索引順序逐個(gè)比較,并在遇到第一個(gè)不匹配時(shí)即停止,這澄清了潛在的歧義,但與實(shí)際實(shí)現(xiàn)行為一致。
  4. 泛型 comparable 約束放寬: 即使類型參數(shù)不是嚴(yán)格可比較的(運(yùn)行時(shí)比較可能 panic),它們現(xiàn)在也可以滿足 comparable 約束,允許將接口類型等用作泛型映射的鍵。
  5. Runtime 垃圾回收器(Garbage Collector)優(yōu)化: 通過(guò)重組內(nèi)部數(shù)據(jù)結(jié)構(gòu),減少了內(nèi)存開(kāi)銷(xiāo)并提高了 CPU 效率(整體 CPU 性能提升高達(dá) 2%),同時(shí)改善了 goroutine 輔助(goroutine assists)在某些情況下的行為穩(wěn)定性。
  6. 錯(cuò)誤處理 多錯(cuò)誤包裝(Wrapping multiple errors): 擴(kuò)展了錯(cuò)誤包裝功能,允許一個(gè)錯(cuò)誤通過(guò)實(shí)現(xiàn)返回 []error 的 Unwrap 方法來(lái)包裝多個(gè)錯(cuò)誤;errors.Is 、 errors.As 、 fmt.Errorf 的 %w 以及新增的 errors.Join 函數(shù)均已支持此特性。
  7. net/http 新增 ResponseController: 引入 net/http.ResponseController 類型,提供了一種比可選接口更清晰、更易于發(fā)現(xiàn)的方式來(lái)訪問(wèn)每個(gè)請(qǐng)求的擴(kuò)展功能,例如設(shè)置讀寫(xiě)截止時(shí)間。
  8. httputil.ReverseProxy 新增 Rewrite 鉤子: 新的 Rewrite 鉤子取代了原有的 Director 鉤子,提供了對(duì)入站和出站請(qǐng)求更全面的控制,并引入了 ProxyRequest.SetURL 和 ProxyRequest.SetXForwarded 等輔助方法,同時(shí)修復(fù)了潛在的安全問(wèn)題。

下面是一些值得展開(kāi)的討論:

Go 1.20 簡(jiǎn)化了從 Slice 到 Array 的轉(zhuǎn)換語(yǔ)法

Go 1.17 版本引入了一個(gè)特性,允許將 slice 轉(zhuǎn)換為指向數(shù)組的指針。例如,如果有一個(gè) slice x,你可以通過(guò) *(*[4]byte)(x) 的方式將其轉(zhuǎn)換為一個(gè)指向包含 4 個(gè)字節(jié)的數(shù)組的指針。這種轉(zhuǎn)換有其局限性,它得到的是一個(gè)指針。

Go 1.20 在此基礎(chǔ)上更進(jìn)一步,允許直接將 slice 轉(zhuǎn)換為一個(gè)數(shù)組值(而不是數(shù)組指針)。現(xiàn)在,對(duì)于一個(gè) slice x,你可以直接使用 [4]byte(x) 來(lái)獲得一個(gè)包含 4 個(gè)字節(jié)的數(shù)組。

這種轉(zhuǎn)換有一個(gè)前提條件:slice 的長(zhǎng)度必須大于或等于目標(biāo)數(shù)組的長(zhǎng)度。如果 slice x 的長(zhǎng)度 len(x) 小于目標(biāo)數(shù)組的長(zhǎng)度(在這個(gè)例子中是 4),那么在運(yùn)行時(shí)會(huì)發(fā)生 panic。轉(zhuǎn)換的結(jié)果數(shù)組將包含 slice x 的前 N 個(gè)元素,其中 N 是目標(biāo)數(shù)組的長(zhǎng)度。

讓我們看一個(gè)簡(jiǎn)單的例子:

package main

import "fmt"

func main() {
    s := []byte{'g', 'o', 'l', 'a', 'n', 'g'}

    // Go 1.20: Direct conversion from slice to array
    var a4 [4]byte = [4]byte(s) // a4 will be {'g', 'o', 'l', 'a'}
    fmt.Printf("Array a4: %v\n", a4)
    fmt.Printf("Array a4 as string: %s\n", string(a4[:]))

    var a6 [6]byte = [6]byte(s) // a6 will be {'g', 'o', 'l', 'a', 'n', 'g'}
    fmt.Printf("Array a6: %v\n", a6)
    fmt.Printf("Array a6 as string: %s\n", string(a6[:]))

    // Contrast with Go 1.17 style (slice to array pointer)
    ap4 := (*[4]byte)(s) // ap4 is a pointer to the first 4 bytes of s's underlying array
    fmt.Printf("Array pointer ap4: %v\n", *ap4)
    // Modify the array through the pointer - this affects the original slice's underlying data!
    ap4[0] = 'G'
    fmt.Printf("Original slice s after modifying via pointer: %s\n", string(s)) // Output: Golang

    // Note: Direct conversion creates a *copy* of the data
    a4_copy := [4]byte(s)
    a4_copy[0] = 'F' // Modify the copy
    fmt.Printf("Original slice s after modifying direct conversion copy: %s\n", string(s)) // Output: Golang (unaffected)
    fmt.Printf("Copied array a4_copy: %s\n", string(a4_copy[:]))                     // Output: Fola

    // Example that would panic at runtime
    shortSlice := []byte{'h', 'i'}
    var a3 [3]byte = [3]byte(shortSlice) // This line would cause a panic: runtime error
    _ = a3
    _ = shortSlice // Avoid unused variable error
}
Array a4: [103 111 108 97]
Array a4 as string: gola
Array a6: [103 111 108 97 110 103]
Array a6 as string: golang
Array pointer ap4: [103 111 108 97]
Original slice s after modifying via pointer: Golang
Original slice s after modifying direct conversion copy: Golang
Copied array a4_copy: Fola
panic: runtime error: cannot convert slice with length 2 to array or pointer to array with length 3

這個(gè)改動(dòng)使得代碼更簡(jiǎn)潔、易讀,特別是在需要數(shù)組值而不是指針的場(chǎng)景下。需要注意的是,直接轉(zhuǎn)換為數(shù)組會(huì)復(fù)制數(shù)據(jù),而轉(zhuǎn)換為數(shù)組指針則不會(huì),它只是創(chuàng)建一個(gè)指向 slice 底層數(shù)組對(duì)應(yīng)部分的指針。

unsafe 包新增 SliceData, String, StringData,完善了 Slice 和 String 的底層操作能力

Go 語(yǔ)言的 unsafe 包提供了一些低層次的操作,允許開(kāi)發(fā)者繞過(guò) Go 的類型安全和內(nèi)存安全檢查。雖然應(yīng)謹(jǐn)慎使用,但在某些高性能場(chǎng)景或與 C 語(yǔ)言庫(kù)交互時(shí)非常有用。

Go 1.20 在 unsafe 包中引入了三個(gè)新的函數(shù):SliceData、String 和 StringData。這些函數(shù),連同 Go 1.17 引入的 unsafe.Slice,提供了一套完整的、不依賴于 slice 和 string 內(nèi)部具體表示(這些表示在不同 Go 版本中可能變化)的構(gòu)造和解構(gòu)方法。

這意味著你可以編寫(xiě)更健壯的底層代碼,即使未來(lái) Go 改變了 slice 或 string 的內(nèi)部結(jié)構(gòu)(例如 SliceHeader 或 StringHeader 的字段),只要這些函數(shù)的語(yǔ)義不變,你的代碼依然能工作。

這四個(gè)核心函數(shù)的功能如下:

  1. SliceData(slice []T) *T :返回指向 slice 底層數(shù)組第一個(gè)元素的指針。如果 slice 為 nil,則返回 nil。它等價(jià)于 &slice[0],但即使 slice 為空(len(slice) == 0),只要容量不為零(cap(slice) > 0),它也能安全地返回底層數(shù)組的指針(而 &slice[0] 會(huì) panic)。
  2. StringData(str string) *byte :返回指向 string 底層字節(jié)數(shù)組第一個(gè)元素的指針。如果 string 為空,則返回 nil。
  3. Slice[T any](ptr *T, lenOrCap int) []T (Go 1.17 引入,Go 1.20 仍重要):根據(jù)給定的指向類型 T 的指針 ptr 和指定的容量/長(zhǎng)度 lenOrCap,創(chuàng)建一個(gè)新的 slice。這個(gè) slice 的長(zhǎng)度和容量都等于 lenOrCap,并且它的底層數(shù)組從 ptr 指向的內(nèi)存開(kāi)始。注意:早期版本中此函數(shù)可能接受兩個(gè)參數(shù) len 和 cap,但在 Go 1.17 穩(wěn)定版及后續(xù)版本中,通常簡(jiǎn)化為接受一個(gè) lenOrCap 參數(shù),表示長(zhǎng)度和容量相同。使用時(shí)請(qǐng)查閱對(duì)應(yīng) Go 版本的文檔確認(rèn)具體簽名。 假設(shè)這里我們使用 Go 1.17 引入的 Slice(ptr *ArbitraryType, cap IntegerType) []ArbitraryType 形式,它創(chuàng)建一個(gè)長(zhǎng)度和容量都為 cap 的切片。或者更通用的 Slice(ptr *T, len int) []T,如果 Go 版本支持(創(chuàng)建 len==cap 的切片)。為了演示,我們假設(shè)存在一個(gè)函數(shù)能創(chuàng)建指定 len 和 cap 的 slice。*更新:根據(jù) Go 1.17 及后續(xù)文檔,unsafe.Slice(ptr *T, len IntegerType) []T 是標(biāo)準(zhǔn)形式,創(chuàng)建的 slice 長(zhǎng)度和容量都為 len*。
  4. **String(ptr *byte, len int) string**:根據(jù)給定的指向字節(jié)的指針 ptr 和長(zhǎng)度 len,創(chuàng)建一個(gè)新的 string。

使用 unsafe 包需要開(kāi)發(fā)者深刻理解 Go 的內(nèi)存模型和潛在風(fēng)險(xiǎn)。這些新函數(shù)提供了一個(gè)更穩(wěn)定、面向未來(lái)的接口來(lái)執(zhí)行這些底層操作,減少了代碼因 Go 內(nèi)部實(shí)現(xiàn)細(xì)節(jié)變化而失效的可能性。

語(yǔ)言規(guī)范明確了結(jié)構(gòu)體和數(shù)組的比較規(guī)則:逐元素比較,遇首個(gè)差異即停止

Go 語(yǔ)言規(guī)范定義了哪些類型是可比較的(comparable)。結(jié)構(gòu)體(struct)類型是可比較的,如果它的所有字段類型都是可比較的。數(shù)組(array)類型也是可比較的,如果它的元素類型是可比較的。

在 Go 1.20 之前,規(guī)范中關(guān)于結(jié)構(gòu)體和數(shù)組如何進(jìn)行比較的描述存在一定的模糊性。一種可能的解讀是,比較兩個(gè)結(jié)構(gòu)體或數(shù)組時(shí),需要比較完它們所有的字段或元素,即使在中間已經(jīng)發(fā)現(xiàn)了不匹配。

Go 1.20 的規(guī)范明確了實(shí)際的比較行為,這也是 Go 編譯器一直以來(lái)的實(shí)現(xiàn)方式:

  1. 結(jié)構(gòu)體比較 :比較結(jié)構(gòu)體時(shí),會(huì)按照字段在 struct 類型定義中出現(xiàn)的順序,逐個(gè)比較字段的值。一旦遇到第一個(gè)不匹配的字段,比較立即停止,并得出兩者不相等的結(jié)果。如果所有字段都相等,則結(jié)構(gòu)體相等。
  2. 數(shù)組比較 :比較數(shù)組時(shí),會(huì)按照索引從 0 開(kāi)始遞增的順序,逐個(gè)比較元素的值。一旦遇到第一個(gè)不匹配的元素,比較立即停止,并得出兩者不相等的結(jié)果。如果所有元素都相等,則數(shù)組相等。

這個(gè)規(guī)范的明確化主要影響的是包含不可比較類型(如接口類型,其動(dòng)態(tài)值可能不可比較)時(shí)的 panic 行為。考慮以下情況:

package main

import "fmt"

type Data struct {
    ID   int
    Meta interface{} // interface{} or any
}

func main() {
    // Slices are not comparable
    slice1 := []int{1}
    slice2 := []int{1}

    d1 := Data{ID: 1, Meta: slice1}
    d2 := Data{ID: 2, Meta: slice2} // Different ID
    d3 := Data{ID: 1, Meta: slice2} // Same ID, different slice instance (but content might be same)
    d4 := Data{ID: 1, Meta: slice1} // Same ID, same slice instance

    // Comparison stops at the first differing field (ID)
    // The Meta field (interface{} holding a slice) is never compared.
    fmt.Printf("d1 == d2: %t\n", d1 == d2) // Output: false. Comparison stops after comparing ID (1 != 2). No panic.

    // Comparison proceeds to Meta field because IDs are equal (1 == 1).
    // Comparing interfaces containing slices will cause a panic.
    // fmt.Printf("d1 == d3: %t\n", d1 == d3) // This line would panic: runtime error: comparing uncomparable type []int

    // Comparison proceeds to Meta field.
    // Even though it's the *same* slice instance, the comparison itself panics.
    // fmt.Printf("d1 == d4: %t\n", d1 == d4) // This line would also panic: runtime error: comparing uncomparable type []int

    // Array comparison example
    type Info struct {
        Count int
    }
    // Arrays of comparable types are comparable
    a1 := [2]Info{{Count: 1}, {Count: 2}}
    a2 := [2]Info{{Count: 1}, {Count: 3}} // Differs at index 1
    a3 := [2]Info{{Count: 0}, {Count: 2}} // Differs at index 0

    fmt.Printf("a1 == a2: %t\n", a1 == a2) // Output: false. Stops after comparing a1[1] and a2[1].
    fmt.Printf("a1 == a3: %t\n", a1 == a3) // Output: false. Stops after comparing a1[0] and a3[0].

    _, _, _, _ = d1, d2, d3, d4
}

雖然這個(gè)規(guī)范的明確化沒(méi)有改變現(xiàn)有程序的行為(因?yàn)閷?shí)現(xiàn)早已如此),但它消除了規(guī)范層面的歧義,使得開(kāi)發(fā)者能更準(zhǔn)確地理解比較操作何時(shí)會(huì)因遇到不可比較類型而 panic。如果比較在遇到不可比較的字段或元素之前就因其他部分不匹配而停止,則不會(huì)發(fā)生 panic。

Go 1.20 放寬 comparable 約束,允許接口等類型作為類型參數(shù),即使運(yùn)行時(shí)比較可能 panic

Go 1.18 引入泛型時(shí),定義了一個(gè) comparable 約束。這個(gè)約束用于限定類型參數(shù)必須是可比較的類型。這對(duì)于需要將類型參數(shù)用作 map 的鍵或在代碼中進(jìn)行 == 或 != 比較的泛型函數(shù)和類型非常重要。

最初,comparable 約束要求類型參數(shù)本身必須是嚴(yán)格可比較的。這意味著像接口類型(interface types)這樣的類型不能滿足 comparable 約束。為什么?因?yàn)殡m然接口值本身可以用 == 比較(例如,比較它們是否都為 nil,或者是否持有相同的動(dòng)態(tài)值),但如果兩個(gè)接口持有不同的動(dòng)態(tài)類型,或者持有的動(dòng)態(tài)類型本身是不可比較的(如 slice、map、function),那么在運(yùn)行時(shí)比較它們會(huì)引發(fā) panic。由于這種潛在的運(yùn)行時(shí) panic,接口類型在 Go 1.18/1.19 中不被視為滿足 comparable 約束。

這帶來(lái)了一個(gè)問(wèn)題:開(kāi)發(fā)者無(wú)法輕松地創(chuàng)建以接口類型作為鍵的泛型 map 或 set。

Go 1.20 放寬了 comparable 約束的要求。現(xiàn)在,一個(gè)類型 T 滿足 comparable 約束,只要類型 T 的值可以用 == 或 != 進(jìn)行比較即可。這包括了接口類型,以及包含接口類型的復(fù)合類型(如結(jié)構(gòu)體、數(shù)組)。

關(guān)鍵變化在于,滿足 comparable 約束不再保證比較操作永遠(yuǎn)不會(huì) panic。它僅僅保證了 == 和 != 運(yùn)算符 可以 應(yīng)用于該類型的值。比較是否真的會(huì) panic 取決于運(yùn)行時(shí)的具體值。

這個(gè)改動(dòng)使得以下代碼在 Go 1.20 中成為可能:

package main

import "fmt"

// Generic map using comparable constraint for the key
type GenericMap[K comparable, V any] struct {
    m map[K]V
}

func NewGenericMap[K comparable, V any]() *GenericMap[K, V] {
    return &GenericMap[K, V]{m: make(map[K]V)}
}

func (gm *GenericMap[K, V]) Put(key K, value V) {
    gm.m[key] = value
}

func (gm *GenericMap[K, V]) Get(key K) (V, bool) {
    v, ok := gm.m[key]
    return v, ok
}

func main() {
    // Instantiate GenericMap with K=int (strictly comparable) - Works always
    intMap := NewGenericMap[int, string]()
    intMap.Put(1, "one")
    fmt.Println(intMap.Get(1)) // Output: one true

    // Instantiate GenericMap with K=any (interface{}) - Works in Go 1.20+
    // In Go 1.18/1.19, this would fail compilation because 'any'/'interface{}'
    // did not satisfy the 'comparable' constraint.
    anyMap := NewGenericMap[any, string]()

    // Use comparable types as keys - Works fine
    anyMap.Put(10, "integer")
    anyMap.Put("hello", "string")
    type MyStruct struct{ V int }
    anyMap.Put(MyStruct{V: 5}, "struct")

    fmt.Println(anyMap.Get(10))      // Output: integer true
    fmt.Println(anyMap.Get("hello")) // Output: string true
    fmt.Println(anyMap.Get(MyStruct{V: 5})) // Output: struct true

    // Attempt to use an uncomparable type (slice) as a key.
    // The Put operation itself will cause a panic during the map key comparison.
    keySlice := []int{1, 2}
    fmt.Println("Attempting to put slice key...")
    // The following line will panic in Go 1.20+
    // panic: runtime error: hash of unhashable type []int
    // or panic: runtime error: comparing uncomparable type []int
    // (depending on map implementation details)
    // anyMap.Put(keySlice, "slice")
    _ = keySlice // Avoid unused variable

    // Using interface values holding different uncomparable types also panics
    var i1 any = []int{1}
    var i2 any = map[string]int{}
    // The comparison i1 == i2 during map access would panic.
    // anyMap.Put(i1, "interface holding slice")
    // anyMap.Put(i2, "interface holding map")
    _ = i1
    _ = i2
}

這個(gè)改變提高了泛型的靈活性,允許開(kāi)發(fā)者編寫(xiě)適用于更廣泛類型的泛型代碼,特別是涉及 map 鍵時(shí)。但開(kāi)發(fā)者需要意識(shí)到,當(dāng)使用非嚴(yán)格可比較的類型(如 any 或包含接口的結(jié)構(gòu)體)作為滿足 comparable 約束的類型參數(shù)時(shí),代碼中涉及比較的操作(如 map 查找、插入、刪除,或顯式的 ==/!=)可能會(huì)在運(yùn)行時(shí) panic。

Go 1.20 引入了對(duì)包裝多個(gè)錯(cuò)誤的原生支持

在 Go 1.13 中,通過(guò) errors.Unwrap、errors.Is、errors.As 以及 fmt.Errorf 的 %w 動(dòng)詞,引入了標(biāo)準(zhǔn)的錯(cuò)誤包裝(error wrapping)機(jī)制。這允許一個(gè)錯(cuò)誤 "包含" 另一個(gè)錯(cuò)誤,形成錯(cuò)誤鏈,方便追蹤錯(cuò)誤的根本原因。然而,該機(jī)制僅支持一個(gè)錯(cuò)誤包裝 單個(gè) 其他錯(cuò)誤。

在實(shí)際開(kāi)發(fā)中,有時(shí)一個(gè)操作可能因?yàn)槎鄠€(gè)獨(dú)立的原因而失敗,或者一個(gè)聚合操作中的多個(gè)子操作都失敗了。例如,嘗試將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)和文件系統(tǒng)都失敗了。在這種情況下,將多個(gè)錯(cuò)誤合并成一個(gè)錯(cuò)誤會(huì)很有用。

Go 1.20 擴(kuò)展了錯(cuò)誤處理機(jī)制,原生支持一個(gè)錯(cuò)誤包裝 多個(gè) 其他錯(cuò)誤。主要通過(guò)以下幾種方式實(shí)現(xiàn):

  • Unwrap() []error 方法

一個(gè)錯(cuò)誤類型可以通過(guò)實(shí)現(xiàn) Unwrap() []error 方法來(lái)表明它包裝了多個(gè)錯(cuò)誤。如果一個(gè)類型同時(shí)定義了 Unwrap() error 和 Unwrap() []error,那么 errors.Is 和 errors.As 將優(yōu)先使用 Unwrap() []error。

  • fmt.Errorf 的多個(gè) %w

fmt.Errorf 函數(shù)現(xiàn)在支持在格式字符串中多次使用 %w 動(dòng)詞。調(diào)用 fmt.Errorf("...%w...%w...", err1, err2) 將返回一個(gè)包裝了 err1 和 err2 的新錯(cuò)誤。這個(gè)返回的錯(cuò)誤實(shí)現(xiàn)了 Unwrap() []error 方法。

  • errors.Join(...error) error 函數(shù)

新增的 errors.Join 函數(shù)接受一個(gè)或多個(gè) error 參數(shù),并返回一個(gè)包裝了所有非 nil 輸入錯(cuò)誤的新錯(cuò)誤。如果所有輸入錯(cuò)誤都是 nil,errors.Join 返回 nil。返回的錯(cuò)誤也實(shí)現(xiàn)了 Unwrap() []error 方法。這是合并多個(gè)錯(cuò)誤的推薦方式。

  • errors.Is 和 errors.As 更新

這兩個(gè)函數(shù)現(xiàn)在能夠遞歸地檢查通過(guò) Unwrap() []error 暴露出來(lái)的所有錯(cuò)誤。errors.Is(multiErr, target) 會(huì)檢查 multiErr 本身以及它(遞歸地)解包出來(lái)的任何一個(gè)錯(cuò)誤是否等于 target。類似地,errors.As(multiErr, &targetVar) 會(huì)檢查 multiErr 或其解包鏈中的任何錯(cuò)誤是否可以賦值給 targetVar。

下面是一個(gè)結(jié)合生產(chǎn)場(chǎng)景的例子,演示如何使用這些新特性:

package main

import (
    "errors"
    "fmt"
    "os"
    "time"
    "syscall"
)

// 定義一些具體的錯(cuò)誤類型
var ErrDatabaseTimeout = errors.New("database timeout")
var ErrCacheFailed = errors.New("cache operation failed")
var ErrFileSystemReadOnly = errors.New("file system is read-only")

type NetworkError struct {
    Op  string
    Err error // Underlying network error
}

func (e *NetworkError) Error() string {
    return fmt.Sprintf("network error during %s: %v", e.Op, e.Err)
}

func (e *NetworkError) Unwrap() error {
    return e.Err // Implements single error unwrapping
}

// 模擬保存數(shù)據(jù)的操作,可能同時(shí)涉及多個(gè)系統(tǒng)
func saveData(data string) error {
    var errs []error // 用于收集所有發(fā)生的錯(cuò)誤

    // 模擬數(shù)據(jù)庫(kù)操作
    if time.Now().Second()%2 == 0 { // 假設(shè)偶數(shù)秒時(shí)數(shù)據(jù)庫(kù)超時(shí)
        errs = append(errs, ErrDatabaseTimeout)
    }

    // 模擬緩存操作
    if len(data) < 5 { // 假設(shè)數(shù)據(jù)太短時(shí)緩存失敗
        // 包裝一個(gè)更具體的網(wǎng)絡(luò)錯(cuò)誤
        netErr := &NetworkError{Op: "set cache", Err: errors.New("connection refused")}
        errs = append(errs, fmt.Errorf("%w: %w", ErrCacheFailed, netErr)) // 使用 %w 包裝原始錯(cuò)誤和網(wǎng)絡(luò)錯(cuò)誤
    }

    // 模擬文件系統(tǒng)操作
    if _, err := os.OpenFile("dummy.txt", os.O_WRONLY, 0666); err != nil {
        // 檢查是否是只讀文件系統(tǒng)錯(cuò)誤(僅為示例,實(shí)際檢查更復(fù)雜)
        if os.IsPermission(err) { // os.IsPermission is a common check
            errs = append(errs, ErrFileSystemReadOnly)
        } else {
            errs = append(errs, fmt.Errorf("failed to open file: %w", err)) // 包裝底層 os 錯(cuò)誤
        }
    }

    // 使用 errors.Join 將所有收集到的錯(cuò)誤合并成一個(gè)
    // 如果 errs 為空 (即沒(méi)有錯(cuò)誤發(fā)生), errors.Join 會(huì)返回 nil
    return errors.Join(errs...)
}

func main() {
    err := saveData("dat") // "dat" is short, likely triggers cache error
    if err != nil {
        fmt.Printf("Failed to save data:\n%v\n\n", err) // errors.Join 產(chǎn)生的錯(cuò)誤會(huì)自動(dòng)格式化,顯示所有子錯(cuò)誤

        // 現(xiàn)在我們可以檢查這個(gè)聚合錯(cuò)誤中是否包含特定的錯(cuò)誤類型或值

        // 檢查是否包含數(shù)據(jù)庫(kù)超時(shí)錯(cuò)誤
        if errors.Is(err, ErrDatabaseTimeout) {
            // errors.Is 會(huì)遍歷 err 解包出來(lái)的所有錯(cuò)誤(通過(guò) errors.Join 的 Unwrap() []error)
            // 如果找到 ErrDatabaseTimeout,則返回 true
            fmt.Println("Detected: Database Timeout")
        }

        // 檢查是否包含文件系統(tǒng)只讀錯(cuò)誤
        if errors.Is(err, ErrFileSystemReadOnly) {
            // 同樣,errors.Is 會(huì)檢查所有被 Join 的錯(cuò)誤
            fmt.Println("Detected: File System Read-Only")
        }

        // 提取具體的 NetworkError 類型
        var netErr *NetworkError
        if errors.As(err, &netErr) {
            // errors.As 會(huì)遍歷 err 解包出來(lái)的所有錯(cuò)誤
            // 如果找到一個(gè)類型為 *NetworkError 的錯(cuò)誤,就將其賦值給 netErr 并返回 true
            fmt.Printf("Detected Network Error: Op=%s, Underlying=%v\n", netErr.Op, netErr.Err)
            // 我們甚至可以進(jìn)一步檢查 NetworkError 內(nèi)部包裝的錯(cuò)誤
            if errors.Is(netErr, syscall.ECONNREFUSED) { // 假設(shè)底層是 connection refused
                fmt.Println("   Network error specifically was: connection refused")
            }
        }

        // 也可以檢查原始的 Cache 失敗錯(cuò)誤
        if errors.Is(err, ErrCacheFailed) {
            // 這會(huì)找到 fmt.Errorf("%w: %w", ErrCacheFailed, netErr) 中包裝的 ErrCacheFailed
            fmt.Println("Detected: Cache Failed (may have underlying network error)")
        }
    } else {
        fmt.Println("Data saved successfully!")
    }

    // 演示 fmt.Errorf 與多個(gè) %w
    err1 := errors.New("error one")
    err2 := errors.New("error two")
    multiWError := fmt.Errorf("operation failed: %w; also %w", err1, err2)
    fmt.Printf("\nError from multiple %%w:\n%v\n", multiWError)
    if errors.Is(multiWError, err1) && errors.Is(multiWError, err2) {
        // multiWError 實(shí)現(xiàn) Unwrap() []error,包含 err1 和 err2
        fmt.Println("Multiple %w error contains both err1 and err2.")
    }
}
Failed to save data:
database timeout
cache operation failed: network error during set cache: connection refused
failed to open file: open dummy.txt: no such file or directory

Detected: Database Timeout
Detected Network Error: Op=set cache, Underlying=connection refused
Detected: Cache Failed (may have underlying network error)

Error from multiple %w:
operation failed: error one; also error two
Multiple %w error contains both err1 and err2.

(注意: 上述代碼中的 syscall.ECONNREFUSED 部分可能需要根據(jù)你的操作系統(tǒng)進(jìn)行調(diào)整或替換為更通用的網(wǎng)絡(luò)錯(cuò)誤檢查方式,這里僅作 errors.Is 嵌套使用的演示)

這個(gè)多錯(cuò)誤包裝功能使得錯(cuò)誤處理更加靈活和富有表現(xiàn)力,特別是在需要聚合來(lái)自不同子系統(tǒng)或并發(fā)操作的錯(cuò)誤時(shí),能夠提供更完整的失敗上下文,同時(shí)保持了與現(xiàn)有 errors.Is 和 errors.As 的兼容性。

net/http 引入 ResponseController 以提供更清晰、可發(fā)現(xiàn)的擴(kuò)展請(qǐng)求處理控制

在 Go 的 net/http 包中,http.ResponseWriter 接口是 HTTP handler 處理請(qǐng)求并構(gòu)建響應(yīng)的核心。然而,隨著 HTTP 協(xié)議和服務(wù)器功能的發(fā)展,有時(shí)需要對(duì)請(qǐng)求處理過(guò)程進(jìn)行更精細(xì)的控制,而這些控制功能超出了 ResponseWriter 接口的基本定義(如 Write, WriteHeader, Header)。

過(guò)去,net/http 包通常通過(guò)定義 可選接口(optional interfaces)來(lái)添加這些擴(kuò)展功能。例如,如果 handler 需要主動(dòng)將緩沖的數(shù)據(jù)刷新到客戶端,它可以檢查其接收到的 ResponseWriter 是否也實(shí)現(xiàn)了 http.Flusher 接口,如果實(shí)現(xiàn)了,就調(diào)用其 Flush() 方法。其他例子包括 http.Hijacker(用于接管 TCP 連接)和 http.Pusher(用于 HTTP/2 server push)。

這種依賴可選接口的模式有幾個(gè)缺點(diǎn):

  1. 不易發(fā)現(xiàn) :開(kāi)發(fā)者需要知道這些可選接口的存在,并在文檔或代碼中查找它們。
  2. 使用笨拙 :每次使用都需要進(jìn)行類型斷言(if hj, ok := w.(http.Hijacker); ok { ... }),使得代碼略顯冗長(zhǎng)。
  3. 擴(kuò)展性問(wèn)題 :隨著新功能的增加,可選接口的數(shù)量可能會(huì)不斷增多。

為了解決這些問(wèn)題,Go 1.20 引入了 net/http.ResponseController 類型。這是一個(gè)新的結(jié)構(gòu)體,旨在提供一個(gè)統(tǒng)一的、更清晰、更易于發(fā)現(xiàn)的方式來(lái)訪問(wèn)附加的、針對(duì)每個(gè)請(qǐng)求(per-request)的響應(yīng)控制功能。

你可以通過(guò) http.NewResponseController(w ResponseWriter) 來(lái)獲取與給定 ResponseWriter 關(guān)聯(lián)的 ResponseController 實(shí)例。然后,你可以調(diào)用 ResponseController 上的方法來(lái)執(zhí)行擴(kuò)展操作。

Go 1.20 同時(shí)通過(guò) ResponseController 引入了兩個(gè)新的控制功能:

  1. SetReadDeadline(time time.Time) error :設(shè)置此請(qǐng)求的底層連接的讀取截止時(shí)間。這對(duì)于需要長(zhǎng)時(shí)間運(yùn)行的 handler(例如,流式上傳或 WebSocket)想要覆蓋服務(wù)器的全局讀取超時(shí)(Server.ReadTimeout)非常有用。
  2. SetWriteDeadline(time time.Time) error :設(shè)置此請(qǐng)求的底層連接的寫(xiě)入截止時(shí)間。這對(duì)于 handler 需要發(fā)送大量數(shù)據(jù)或進(jìn)行流式響應(yīng),并希望覆蓋服務(wù)器的全局寫(xiě)入超時(shí)(Server.WriteTimeout)很有用。將截止時(shí)間設(shè)置為空的 time.Time{} (即零值) 表示禁用超時(shí)。

以下是如何使用 ResponseController 設(shè)置寫(xiě)入截止時(shí)間的示例,改編自官方文檔:

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

// 模擬一個(gè)需要發(fā)送大量數(shù)據(jù)的 handler
func bigDataHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Handling request for big data...")

    // 獲取與 ResponseWriter 關(guān)聯(lián)的 ResponseController
    rc := http.NewResponseController(w)

    // 假設(shè)我們要發(fā)送大量數(shù)據(jù),可能超過(guò)服務(wù)器的默認(rèn) WriteTimeout
    // 我們可以為這個(gè)特定的請(qǐng)求禁用寫(xiě)入超時(shí)
    // 將截止時(shí)間設(shè)置為空的 time.Time (零值) 即可禁用
    err := rc.SetWriteDeadline(time.Time{})
    if err != nil {
        // 如果設(shè)置截止時(shí)間失敗 (例如,底層連接不支持或已關(guān)閉)
        // 記錄錯(cuò)誤并可能返回一個(gè)內(nèi)部服務(wù)器錯(cuò)誤
        fmt.Printf("Error setting write deadline: %v\n", err)
        http.Error(w, "Failed to set write deadline", http.StatusInternalServerError)
        return
    }
    fmt.Println("Write deadline disabled for this request.")

    // 設(shè)置響應(yīng)頭
    w.Header().Set("Content-Type", "text/plain")
    w.WriteHeader(http.StatusOK)

    // 模擬發(fā)送大量數(shù)據(jù)
    for i := 0; i < 10; i++ {
        _, err := io.WriteString(w, fmt.Sprintf("This is line %d of a large response.\n", i+1))
        if err != nil {
            // 如果寫(xiě)入過(guò)程中發(fā)生錯(cuò)誤 (例如,連接被客戶端關(guān)閉)
            fmt.Printf("Error writing response data: %v\n", err)
            // 此時(shí)可能無(wú)法再向客戶端發(fā)送錯(cuò)誤,但應(yīng)記錄日志
            return
        }
        // 模擬耗時(shí)操作
        time.Sleep(200 * time.Millisecond)
    }

    fmt.Println("Finished sending big data.")
}

// 模擬一個(gè)需要從客戶端讀取可能很慢的數(shù)據(jù)流的 handler
func slowUploadHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Handling slow upload...")

    // 獲取 ResponseController (雖然這里主要控制讀取,但通過(guò) ResponseWriter 獲取)
    rc := http.NewResponseController(w)

    // 假設(shè)服務(wù)器有 ReadTimeout,但我們預(yù)期這個(gè)上傳可能很慢
    // 我們可以延長(zhǎng)讀取截止時(shí)間,比如設(shè)置為 1 分鐘后
    deadline := time.Now().Add(1 * time.Minute)
    err := rc.SetReadDeadline(deadline)
    if err != nil {
        fmt.Printf("Error setting read deadline: %v\n", err)
        http.Error(w, "Failed to set read deadline", http.StatusInternalServerError)
        return
    }
    fmt.Printf("Read deadline set to %v for this request.\n", deadline)

    // 現(xiàn)在可以安全地從 r.Body 讀取,直到截止時(shí)間
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        // 檢查是否是超時(shí)錯(cuò)誤
        if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
            fmt.Println("Read timed out as expected.")
            http.Error(w, "Read timed out", http.StatusRequestTimeout)
        } else {
            fmt.Printf("Error reading request body: %v\n", err)
            http.Error(w, "Error reading body", http.StatusInternalServerError)
        }
        return
    }

    fmt.Printf("Received %d bytes.\n", len(bodyBytes))
    fmt.Fprintln(w, "Upload received successfully!")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/bigdata", bigDataHandler)
    mux.HandleFunc("/upload", slowUploadHandler)

    // 創(chuàng)建一個(gè)帶有默認(rèn)超時(shí)的服務(wù)器 (例如 5 秒)
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second, // 默認(rèn)讀取超時(shí)
        WriteTimeout: 5 * time.Second, // 默認(rèn)寫(xiě)入超時(shí)
    }

    fmt.Println("Server starting on :8080...")
    fmt.Println("Try visiting http://localhost:8080/bigdata")
    fmt.Println("Try sending a POST request with a slow body to http://localhost:8080/upload")
    fmt.Println("Example using curl for slow upload:")
    fmt.Println(`  curl -X POST --data-binary @- http://localhost:8080/upload <<EOF`)
    fmt.Println(`  This is some data that will be sent slowly.`)
    fmt.Println(`  <You might need to wait or pipe slow input here>`)
    fmt.Println(`  EOF`)

    err := server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        fmt.Printf("Server error: %v\n", err)
    }
}

// 注意: SetReadDeadline/SetWriteDeadline 通常作用于底層的 net.Conn。
// 對(duì)于 HTTP/2,行為可能更復(fù)雜,因?yàn)橐粋€(gè)連接上有多路復(fù)用的流。
// 對(duì)于 HTTP/3 (QUIC),這些可能不適用或有不同的機(jī)制。
// 查閱具體 Go 版本的 net/http 文檔了解詳細(xì)語(yǔ)義。

ResponseController 提供了一個(gè)更健壯、面向未來(lái)的機(jī)制來(lái)添加和使用 net/http 的擴(kuò)展功能。預(yù)期未來(lái)更多類似 SetReadDeadline 的細(xì)粒度控制將通過(guò) ResponseController 的方法來(lái)提供,而不是增加新的可選接口。

httputil.ReverseProxy 獲得新的 Rewrite 鉤子,取代 Director,提供更安全、靈活的請(qǐng)求轉(zhuǎn)發(fā)定制

net/http/httputil.ReverseProxy 是 Go 標(biāo)準(zhǔn)庫(kù)中用于構(gòu)建反向代理服務(wù)器的核心組件。它接收客戶端(入站)請(qǐng)求,并將其轉(zhuǎn)發(fā)給一個(gè)或多個(gè)后端(出站)服務(wù)器。開(kāi)發(fā)者可以通過(guò)設(shè)置 ReverseProxy 結(jié)構(gòu)體的字段來(lái)自定義其行為。

在 Go 1.20 之前,最主要的定制點(diǎn)是 Director 字段。Director 是一個(gè)函數(shù) func(*http.Request),它在請(qǐng)求被轉(zhuǎn)發(fā) 之前 被調(diào)用。Director 的職責(zé)是修改傳入的 *http.Request 對(duì)象,使其指向目標(biāo)后端服務(wù)器,并進(jìn)行必要的頭部調(diào)整等。然而,Director 的設(shè)計(jì)存在一些局限和潛在的安全問(wèn)題:

  1. 只操作出站請(qǐng)求 :Director 函數(shù)只接收即將發(fā)送到后端的請(qǐng)求對(duì)象。這個(gè)對(duì)象通常是入站請(qǐng)求的一個(gè)淺拷貝(shallow copy)。Director 無(wú)法直接訪問(wèn) 原始 的入站請(qǐng)求對(duì)象。
  2. 安全風(fēng)險(xiǎn) (Issue #50580) :ReverseProxy 在調(diào)用 Director之后,但在實(shí)際發(fā)送請(qǐng)求 之前,會(huì)執(zhí)行一些默認(rèn)的頭部清理和設(shè)置操作(例如,移除 hop-by-hop headers,設(shè)置 X-Forwarded-* 頭等)。惡意的客戶端可以通過(guò)構(gòu)造特殊的入站請(qǐng)求頭,使得 Director 添加的某些頭信息(如自定義的認(rèn)證頭)在后續(xù)的清理步驟中被意外移除或覆蓋,從而繞過(guò)檢查。
  3. NewSingleHostReverseProxy 的局限 :這是一個(gè)常用的輔助函數(shù),用于創(chuàng)建一個(gè)將所有請(qǐng)求轉(zhuǎn)發(fā)到單個(gè)后端主機(jī)的 ReverseProxy。但它設(shè)置 Director 的方式有時(shí)不能正確處理 Host 頭部,并且不方便進(jìn)行除目標(biāo) URL 之外的更多自定義。

Go 1.20 引入了一個(gè)新的鉤子函數(shù) Rewrite,旨在取代 Director,并解決上述問(wèn)題。

Rewrite 鉤子

  • 類型 : func(*httputil.ProxyRequest)
  • ProxyRequest 結(jié)構(gòu)體 : 這是一個(gè)新引入的結(jié)構(gòu)體,包含兩個(gè)字段:

In *http.Request: 指向原始的、未經(jīng)修改的入站請(qǐng)求。

Out *http.Request: 指向即將發(fā)送到后端的請(qǐng)求(初始時(shí)是 In 的淺拷貝)。Rewrite 函數(shù)應(yīng)該修改 Out。

  • 優(yōu)勢(shì) :
  • 訪問(wèn)原始請(qǐng)求 : Rewrite 可以同時(shí)訪問(wèn)入站和出站請(qǐng)求,使得決策可以基于原始請(qǐng)求的真實(shí)信息。
  • 更安全的時(shí)機(jī) : Rewrite 在 ReverseProxy 的默認(rèn)頭部處理邏輯 之后 執(zhí)行(或者說(shuō),Rewrite 完全取代了 Director 和部分默認(rèn)邏輯)。這意味著 Rewrite 設(shè)置的頭部(如 Out.Header.Set(...))不會(huì)被代理的后續(xù)步驟意外更改。
  • 更強(qiáng)大的控制 : 結(jié)合 ProxyRequest 提供的輔助方法,可以更方便、正確地完成常見(jiàn)任務(wù)。

ProxyRequest 的輔助方法

Go 1.20 同時(shí)為 ProxyRequest 添加了幾個(gè)實(shí)用的方法:

  1. SetURL(target *url.URL) :
  • 將出站請(qǐng)求 r.Out.URL 設(shè)置為 target。
  • 重要 : 它還會(huì)正確地設(shè)置出站請(qǐng)求的 Host 字段 (r.Out.Host = target.Host) 和 Host 頭部 (r.Out.Header.Set("Host", target.Host))。
  • 這有效地取代了 NewSingleHostReverseProxy 的核心功能,并且做得更正確。
  1. SetXForwarded() :
  • 這是一個(gè)便捷方法,用于在出站請(qǐng)求 r.Out 上設(shè)置標(biāo)準(zhǔn)的 X-Forwarded-For, X-Forwarded-Host, 和 X-Forwarded-Proto 頭部。
  • 重要 : 當(dāng)你提供 Rewrite 鉤子時(shí),ReverseProxy默認(rèn)不再自動(dòng)添加 這些 X-Forwarded-* 頭部。如果你的后端服務(wù)依賴這些頭部,你 必須 在 Rewrite 函數(shù)中顯式調(diào)用 r.SetXForwarded()。

示例用法

以下是如何使用 Rewrite 鉤子來(lái)創(chuàng)建一個(gè)將請(qǐng)求轉(zhuǎn)發(fā)到指定后端,并設(shè)置 X-Forwarded-* 頭部以及一個(gè)自定義頭部的 ReverseProxy:

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
)

func main() {
    // 后端服務(wù)器的 URL
    backendURL, err := url.Parse("http://localhost:8081") // 假設(shè)后端服務(wù)運(yùn)行在 8081 端口
    if err != nil {
        log.Fatal("Invalid backend URL")
    }

    // 創(chuàng)建一個(gè)簡(jiǎn)單的后端服務(wù)器用于測(cè)試
    go startBackendServer()

    // 創(chuàng)建 ReverseProxy 并設(shè)置 Rewrite 鉤子
    proxy := &httputil.ReverseProxy{
        Rewrite: func(r *httputil.ProxyRequest) {
            // 1. 將出站請(qǐng)求的目標(biāo)設(shè)置為后端 URL
            // SetURL 會(huì)同時(shí)設(shè)置 r.Out.URL 和 r.Out.Host,并設(shè)置 Host header
            r.SetURL(backendURL)

            // 2. (可選) 如果需要 X-Forwarded-* 頭部,必須顯式調(diào)用 SetXForwarded
            // 如果不調(diào)用,這些頭部將不會(huì)被添加到出站請(qǐng)求中
            r.SetXForwarded()

            // 3. (可選) 添加或修改其他出站請(qǐng)求頭部
            r.Out.Header.Set("X-My-Proxy-Header", "Hello from proxy")

            // 4. (可選) 可以基于入站請(qǐng)求 r.In 進(jìn)行決策
            log.Printf("Rewriting request: In=%s %s, Out=%s %s",
                r.In.Method, r.In.URL.Path,
                r.Out.Method, r.Out.URL.String()) // 注意 r.Out.URL 已經(jīng)被 SetURL 修改

            // 注意:不需要再手動(dòng)設(shè)置 r.Out.Host 或 Host header,SetURL 已處理
            // 注意:默認(rèn)情況下,ReverseProxy 會(huì)處理 hop-by-hop headers,這里無(wú)需手動(dòng)處理
        },
        // 如果需要自定義錯(cuò)誤處理
        ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
            log.Printf("Proxy error: %v", err)
            http.Error(rw, "Proxy Error", http.StatusBadGateway)
        },
    }

    // 設(shè)置 Director 為 nil,因?yàn)槲覀兪褂昧?Rewrite
    // proxy.Director = nil // 這行不是必需的,因?yàn)樵O(shè)置 Rewrite 優(yōu)先

    // 啟動(dòng)代理服務(wù)器
    log.Println("Starting reverse proxy on :8080, forwarding to", backendURL)
    if err := http.ListenAndServe(":8080", proxy); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

// 簡(jiǎn)單的后端 HTTP 服務(wù)器
func startBackendServer() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Printf("[Backend] Received request: %s %s", r.Method, r.URL.String())
        log.Printf("[Backend] Host header: %s", r.Host)
        log.Printf("[Backend] X-Forwarded-For: %s", r.Header.Get("X-Forwarded-For"))
        log.Printf("[Backend] X-Forwarded-Host: %s", r.Header.Get("X-Forwarded-Host"))
        log.Printf("[Backend] X-Forwarded-Proto: %s", r.Header.Get("X-Forwarded-Proto"))
        log.Printf("[Backend] X-My-Proxy-Header: %s", r.Header.Get("X-My-Proxy-Header"))
        log.Printf("[Backend] User-Agent: %s", r.Header.Get("User-Agent"))

        w.Header().Set("Content-Type", "text/plain")
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("Hello from backend!"))
    })
    log.Println("Starting backend server on :8081")
    if err := http.ListenAndServe(":8081", mux); err != nil {
        log.Printf("Backend server error: %v", err)
        os.Exit(1) // 如果后端無(wú)法啟動(dòng),則退出
    }
}

User-Agent 行為變更

另外一個(gè)相關(guān)的改動(dòng)是:如果入站請(qǐng)求 沒(méi)有User-Agent 頭部,ReverseProxy(無(wú)論是否使用 Rewrite)在 Go 1.20 中將 不再 為出站請(qǐng)求自動(dòng)添加一個(gè)默認(rèn)的 User-Agent 頭部。如果需要確保出站請(qǐng)求總是有 User-Agent,你需要在 Rewrite 鉤子中檢查并設(shè)置它。

// 在 Rewrite 函數(shù)中添加:
if r.Out.Header.Get("User-Agent") == "" {
    r.Out.Header.Set("User-Agent", "MyCustomProxy/1.0")
}

總而言之,Rewrite 鉤子和 ProxyRequest 類型為 httputil.ReverseProxy 提供了更現(xiàn)代化、更安全、更靈活的定制方式,推薦在新的 Go 1.20+ 項(xiàng)目中使用 Rewrite 來(lái)替代 Director。

責(zé)任編輯:武曉燕 來(lái)源: Piper蛋窩
相關(guān)推薦

2025-04-29 08:03:18

2025-04-17 08:00:48

2025-04-14 08:06:04

2025-04-25 08:01:12

Go應(yīng)用程序部署

2025-04-15 08:00:53

2025-04-18 08:07:12

2025-04-28 08:00:56

2025-05-06 05:00:00

2025-05-06 08:00:35

2025-05-06 00:00:08

2025-04-21 00:05:00

2025-04-23 08:02:40

2025-04-21 08:00:56

2025-04-24 09:01:46

2025-04-27 08:00:35

2025-04-27 00:00:01

Go 1.16Go 1.15接口

2025-04-21 00:00:00

Go 開(kāi)發(fā)Go 語(yǔ)言Go 1.9

2025-04-22 08:02:23

2025-04-14 00:00:04

2025-04-11 08:02:38

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 国产乱码精品一品二品 | 国产精品高潮呻吟久久 | 亚洲 欧美 激情 另类 校园 | 污视频在线免费观看 | 一区二区三区视频 | 亚洲视频在线一区 | 91精品国产欧美一区二区 | av中文字幕在线 | 亚洲精品视频在线播放 | 精品久久久久久久 | av一区二区三区 | 艹逼网 | av免费看片| 91免费观看在线 | 91精品国产综合久久婷婷香蕉 | 国产丝袜一区二区三区免费视频 | 久久精品国产久精国产 | 亚洲逼院| a久久 | 日韩成人一区 | 亚洲自拍偷拍免费视频 | 日日夜夜精品免费视频 | 二区中文字幕 | 精品国产一区二区三区免费 | 欧美不卡在线 | 天堂国产| 久久久久久高潮国产精品视 | 日韩中文字幕在线 | 成人午夜免费网站 | 中文字幕乱码一区二区三区 | 91久久精品国产91久久性色tv | 毛片毛片毛片毛片 | 91色啪| 欧美精品免费观看二区 | 色婷婷久久久久swag精品 | 成人国产一区二区三区精品麻豆 | 国产a级毛毛片 | 欧美激情一区二区 | 一级在线免费观看 | 中文字幕免费视频 | 久久69精品久久久久久国产越南 |