Go 語言開發技巧合輯,全網整理最全的一篇
一、Go 語言字符串切片排序
要對 Go 語言中的字符串切片進行排序,可以使用 sort 包中的 Strings 函數。
以下是對一個字符串切片進行升序排序的示例代碼:
package main
import (
"fmt"
"sort"
)
func main() {
fruits := []string{"banana", "apple", "orange", "pear"}
sort.Strings(fruits)
fmt.Println(fruits) // 輸出 ["apple", "banana", "orange", "pear"]
}
在上面的代碼中,Strings 函數對 fruits 字符串切片進行升序排序,輸出結果爲 ["apple", "banana", "orange", "pear"]。
如果要按照降序排列,可以使用 sort.Reverse 函數和 sort.StringSlice 類型進行排序:
package main
import (
"fmt"
"sort"
)
func main() {
fruits := []string{"banana", "apple", "orange", "pear"}
sort.Sort(sort.Reverse(sort.StringSlice(fruits)))
fmt.Println(fruits) // 輸出 ["pear", "orange", "banana", "apple"]
}
在上面的代碼中,sort.StringSlice 將 fruits 字符串切片轉換爲 sort.Interface 類型,然後使用 sort.Reverse 將其反轉,最後使用 sort.Sort 方法進行排序,輸出結果爲 ["pear", "orange", "banana", "apple"]。
如果要按照自定義的排序規則進行排序,可以使用 sort.Slice 函數和比較函數作爲參數。例如,可以按照字符串長度對字符串切片進行排序
package main
import (
"fmt"
"sort"
)
func main() {
fruits := []string{"banana", "apple", "orange", "pear"}
sort.Slice(fruits, func(i, j int) bool {
return len(fruits[i]) < len(fruits[j])
})
fmt.Println(fruits) // 輸出 ["pear", "apple", "banana", "orange"]
}
在上面的代碼中,sort.Slice 函數接受一個比較函數作爲參數,該函數比較字符串的長度。按照字符串長度升序排序後,輸出結果爲 ["pear", "apple", "banana", "orange"]。
二、Gin 獲取所有 post 數據
要在 Gin 中獲取所有的 POST 數據,可以使用 Context 對象的 PostForm 屬性數據。
以下是一個簡單的示例代碼,演示如何獲取所有的 POST 數據:
data := make([]string, 0)
form := ctx.Request.PostForm
for key, value := range form {
if key != "Sign" {
data = append(data, fmt.Sprintf("%v", value[0]))
}
}
fmt.Printf("data: %v\n", data)
在上面的代碼中,通過 PostForm 獲取的 POST 數據的。
在實際開發中,可以根據 POST 數據的鍵值對進行逐個獲取,也可以使用 PostForm 方法獲取單個 POST 數據。例如:
r.POST("/test", func(c *gin.Context) {
name := c.PostForm("name")
age := c.PostForm("age")
fmt.Println(name, age)
c.JSON(200, gin.H{
"message": "success",
})
})
在上面的代碼中,我們使用 PostForm 方法逐個獲取 POST 數據。如果 POST 數據中包含 name 和 age 這兩個鍵值對,則將其打印到控制檯。
三、GO 語言變量類型轉換
Go 語言變量類型轉換
在 Go 語言中,變量類型轉換可以通過在變量名前面放置需要轉換的類型的名稱,使用圓括號將其括起來實現。
Go 語言不存在隱式類型轉換,因此所有的類型轉換都必須顯式的聲明:
valueOfTypeB = typeB(valueOfTypeA)
類型 B 的值 = 類型 B(類型 A 的值)
例如,假設有一個 int 類型的變量 x,我們想將其轉換爲 float64 類型,可以使用以下語法:
var x int = 10
var y float64 = float64(x)
類型轉換隻能在定義正確的情況下轉換成功,例如從一個取值範圍較小的類型轉換到一個取值範圍較大的類型(將 int16 轉換爲 int32)。當從一個取值範圍較大的類型轉換到取值範圍較小的類型時(將 int32 轉換爲 int16 或將 float32 轉換爲 int),會發生精度丟失(截斷)的情況。
字符串類型轉換
將一個字符串轉換成另一個類型,可以使用以下語法:
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
以上代碼將字符串變量 str 轉換爲整型變量 num。
注意,strconv.Atoi 函數返回兩個值,第一個是轉換後的整型值,第二個是可能發生的錯誤,我們可以使用空白標識符 _ 來忽略這個錯誤
。以下實例將字符串轉換爲整數 :
func main() {
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("轉換錯誤:", err)
} else {
fmt.Printf("字符串 '%s' 轉換爲整數爲:%d\n", str, num)
}
}
以下實例將整數轉換爲字符串:
num := 123
str := strconv.Itoa(num)
fmt.Printf("整數 %d 轉換爲字符串爲:'%s'\n", num, str)
以下實例將字符串轉換爲浮點數:
str := "3.14"
num, err := strconv.ParseFloat(str, 64)
if err != nil {
fmt.Println("轉換錯誤:", err)
} else {
fmt.Printf("字符串 '%s' 轉爲浮點型爲:%f\n", str, num)
}
使用 fmt 包中的 Sprintf 函數將變量轉換爲字符串
在 Go 語言中,還可以使用 fmt 包中的 Sprintf 函數將變量轉換爲字符串類型,例如:
import "fmt"
func main() {
var num int = 42
str := fmt.Sprintf("%d", num)
fmt.Printf("The number is %s\n", str)
}
在上述代碼中,我們將一個整數變量 num 轉換爲字符串類型,並將其賦值給 str 變量。在這裏,我們使用 fmt.Sprintf 函數來將整數格式化爲字符串類型。Sprintf 函數的作用是將格式化的字符串寫入指定的變量中。
需要注意的是,對於複雜的數據類型,如結構體、切片等,需要自定義實現 String() 函數來將其轉換爲字符串。這個函數是由 fmt 包在打印時自動調用的,例如:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s is %d years old", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 25}
fmt.Println(p) // Output: Alice is 25 years old
}
在上述代碼中,我們定義了一個 Person 結構體,併爲其定義了 String() 函數來實現轉換爲字符串。當我們打印 Person 類型的變量時,fmt 包會自動調用 String() 函數來將其轉換爲字符串。
四、GO 語言類型斷言
類型斷言概述
類型斷言(Type Assertion)是一個使用在接口值上的操作,用於檢查接口類型變量所持有的值是否實現了期望的接口或者具體的類型。
在 Go 語言中類型斷言的語法格式如下:
value, ok := x.(T)
其中,x 表示一個接口的類型,T 表示一個具體的類型(也可爲接口類型)。
該斷言表達式會返回 x 的值(也就是 value)和一個布爾值(也就是 ok),可根據該布爾值判斷 x 是否爲 T 類型:
如果 T 是具體某個類型,類型斷言會檢查 x 的動態類型是否等於具體類型 T。如果檢查成功,類型斷言返回的結果是 x 的動態值,其類型是 T。
如果 T 是接口類型,類型斷言會檢查 x 的動態類型是否滿足 T。如果檢查成功,x 的動態值不會被提取,返回值是一個類型爲 T 的接口值。
無論 T 是什麼類型,如果 x 是 nil 接口值,類型斷言都會失敗。
示例:
package main
import (
"fmt"
)
func main() {
var x interface{}
x = 1
value, ok := x.(int)
fmt.Print(value, ",", ok)
}
// 運行結果 1, true
需要注意
如果不接收第二個參數也就是上面代碼中的 ok,斷言失敗時會直接造成一個 panic。如果 x 爲 nil 同樣也會 panic。
示例:
package main
import (
"fmt"
)
func main() {
var x interface{}
x = "Hello"
value := x.(int)
fmt.Println(value)
}
運行結果如下:panic: interface conversion: interface {} is string, not int
類型斷言還可以配合 switch 使用,示例代碼如下:package main
import (
"fmt"
)
func main() {
var a int
a = 10
getType(a)
}
func getType(a interface{}) {
switch a.(type) {
case int:
fmt.Println("the type of a is int")
case string:
fmt.Println("the type of a is string")
case float64:
fmt.Println("the type of a is float")
default:
fmt.Println("unknown type")
}
}
// 運行結果 : the type of a is int
五、Gin 通過中間件批量過濾 POST GET 數據
Gin 批量過濾 POST 數據
func SafePOST(ctx *gin.Context, newString string, needReplaceString ...string) {
if ctx.Request.Method == "POST" {
// 讀取請求體中的POST數據
bodyBytes, err := io.ReadAll(ctx.Request.Body)
if err != nil {
ctx.Abort()
return
}
bodyString, err := url.QueryUnescape(string(bodyBytes))
if err != nil {
ctx.Abort()
return
}
for _, v := range needReplaceString {
bodyString = strings.ReplaceAll(bodyString, v, newString)
bodyString = strings.ReplaceAll(bodyString, v, newString)
}
ctx.Request.Body = io.NopCloser(bytes.NewReader([]byte(bodyString)))
}
}
在路由中間件內或者路由函數內執行 SafePOST 函數即可。
Gin 批量過濾 GET 數據
func SafeQuery(ctx *gin.Context, newString string, needReplaceString ...string) {
if ctx.Request.URL.RawQuery == "" {
return
}
rawQuery, err := url.QueryUnescape(string(ctx.Request.URL.RawQuery))
if err != nil {
ctx.Abort()
return
}
for _, v := range needReplaceString {
rawQuery = strings.ReplaceAll(rawQuery, v, newString)
}
ctx.Request.URL.RawQuery = rawQuery
}
在路由中間件內或者路由函數內執行 SafeQuery 函數即可。
六、GO 語言生成一個永不過期的 JWT 令牌
在 JWT 中設置一個永不過期的令牌(token)是可行的,你可以使用 jwt.StandardClaims 結構體的 ExpiresAt 字段來設置過期時間。將過期時間設置爲一個未來的極大值或者將其留空,就可以創建一個永不過期的 JWT。
以下是一個示例,演示如何在 Go 語言中創建一個永不過期的 JWT:
package main
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
func main() {
// 創建一個JWT
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["username"] = "johndoe"
claims["exp"] = time.Now().AddDate(100, 0, 0).Unix() // 過期時間設置爲100年後
tokenString, err := token.SignedString([]byte("secret"))
if err != nil {
fmt.Println("Error creating JWT:", err)
return
}
fmt.Println("JWT:", tokenString)
}
在這個示例中,我們將 JWT 的過期時間設置爲 100 年後,這幾乎等同於不設置過期時間。如果你想要一個真正的永不過期的 JWT,可以將過期時間設置爲 0 或者不設置過期時間,這樣 JWT 就不會自動過期。
需要注意的是
雖然設置永不過期的 JWT 可以方便地進行身份驗證和授權,但也增加了安全風險。如果你需要在 JWT 中存儲敏感信息,請考慮將其加密,以確保數據的安全性。
七、GORM 配置詳解
GORM 配置介紹
GORM 是一個非常流行的 Go 語言 ORM 框架,可以幫助開發者快速地搭建和管理數據庫連接。在使用 GORM 時,需要進行一些配置,以適配應用程序和數據庫的需求。下面是 GORM 的幾個常用配置參數:
Dialect
指定數據庫類型。GORM 支持多種數據庫類型,例如 MySQL、PostgreSQL、SQLite 等等,可以通過 Dialect 參數進行配置。
DNS
指定數據庫連接信息。DNS 參數用於配置數據庫的連接信息,包括主機名、端口號、數據庫名稱、用戶名、密碼等。
MaxIdleConns
指定最大空閒連接數。MaxIdleConns 參數用於指定數據庫連接池中最大的空閒連接數,即連接池中最多可以保留多少個空閒連接。
MaxOpenConns
指定最大打開連接數。MaxOpenConns 參數用於指定連接池中最大的連接數量,即連接池中最多可以打開多少個連接。
在設置 MaxOpenConns 參數時,需要考慮到應用程序的實際併發情況和數據庫的處理能力。通常情況下,建議將 MaxOpenConns 設置爲應用程序的最大併發數加上一定的緩衝值,以保證數據庫連接池中有足夠的連接資源。
具體來說,如果應用程序的最大併發數爲 N,可以將 MaxOpenConns 設置爲 N+10~50 左右的值,這取決於應用程序的實際情況和數據庫的性能。在實際使用中,可以通過壓力測試和性能監控等手段,來確定合適的 MaxOpenConns 值。
需要注意的是,如果 MaxOpenConns 設置得太小,可能會導致應用程序在高併發的情況下無法獲得足夠的連接資源,從而影響應用程序的性能。而如果 MaxOpenConns 設置得太大,則可能會導致數據庫連接數過多,從而消耗過多的系統資源,甚至會導致數據庫崩潰。因此,在設置 MaxOpenConns 時需要慎重考慮,並結合實際情況進行合理的配置。
ConnMaxLifetime
指定連接最大存活時間。ConnMaxLifetime 參數用於指定連接在連接池中最長的存活時間,即連接在連接池中的最長存活時間,超過這個時間連接將會被關閉並重新創建。
Logger
指定日誌記錄器。Logger 參數用於指定 GORM 的日誌記錄器,可以選擇不同級別的日誌記錄方式,例如打印 SQL 語句、打印錯誤信息等等。
NamingStrategy
指定命名策略。NamingStrategy 參數用於指定 GORM 的命名策略,例如表名、列名的命名方式等等。
八、GO 語言當獲取一個不存在的 map 元素時返回什麼
概述
在 GO 語言中,當我們當獲取一個不存在的 map 元素時會返回什麼呢?我們通過下面的例子進行總結 :
例 1
map1 := map[string]string{
"name": "lesscode",
"class": "code001",
}
res := map1["test"]
fmt.Printf("res: %v--%T\n", res, res)
// 輸出 空字符串:
// res: --string
例 2
map1 := map[string]int{
"key1": 1,
"key2": 2,
}
res := map1["name"]
fmt.Printf("res: %v %T\n", res, res)
// 輸出 int 類型的 0
// res: 0 int
例 3
type Person struct {
Name string
}
func main() {
map1 := map[string]Person{
"key1": {Name: "test"},
}
res := map1["name"]
fmt.Printf("res: %v %T\n", res, res)
}
通過上面的例子我們可以總結 :
當我們當獲取一個不存在的 map 元素時會返回對應 map 類型的” 空元素”。
九、GO 語言正則表達式 regexp 包用法
正則表達式概述
正則表達式是一種進行模式匹配和文本操縱的複雜而又強大的工具。
Go 通過 regexp 包爲正則表達式提供了官方支持,其採用 RE2 語法,除了 \ c、\C 外,Go 語言和 Perl、Python 等語言的正則基本一致。
正則表達式語法規則
正則表達式是由普通字符(例如字符 a 到 z)以及特殊字符(稱爲 "元字符")構成的文字序列,可以是單個的字符、字符集合、字符範圍、字符間的選擇或者所有這些組件的任意組合。
下面的表格中列舉了構成正則表達式的一些語法規則及其含義。
1) 字符
一般字符 匹配自身 abc abc
. 匹配任意除換行符"\n"外的字符, 在 DOTALL 模式中也能匹配換行符 a.c abc ;
\ 轉義字符,使後一個字符改變原來的意思;
[...] 字符集(字符類),對應的位置可以是字符集中任意字符。
字符集中的字符可以逐個列出,也可以給出範圍,如 [abc] 或 [a-c],
第一個字符如果是 ^ 則表示取反,如 [^abc] 表示除了abc之外的其他字符。 a[bcd]e abe 或 ace 或 ade
\d 數字:[0-9] a\dc a1c
\D 非數字:[^\d] a\Dc abc
\s 空白字符:[<空格>\t\r\n\f\v] a\sc a c
\S 非空白字符:[^\s] a\Sc abc
\w 單詞字符:[A-Za-z0-9] a\wc abc
\W 非單詞字符:[^\w] a\Wc a c
2) 數量詞(用在字符或 (...) 之後)
* 匹配前一個字符 0 或無限次 abc* ab 或 abccc
+ 匹配前一個字符 1 次或無限次 abc+ abc 或 abccc
? 匹配前一個字符 0 次或 1 次 abc? ab 或 abc
{m} 匹配前一個字符 m 次 ab{2}c abbc
{m,n} 匹配前一個字符 m 至 n 次,m 和 n 可以省略,若省略 m,則匹配 0 至 n 次;
若省略 n,則匹配 m 至無限次 ab{1,2}c abc 或 abbc
3) 邊界匹配
^ 匹配字符串開頭,在多行模式中匹配每一行的開頭 ^abc abc
$ 匹配字符串末尾,在多行模式中匹配每一行的末尾 abc$ abc
\A 僅匹配字符串開頭 \Aabc abc
\Z 僅匹配字符串末尾 abc\Z abc
\b 匹配 \w 和 \W 之間 a\b!bc a!bc
\B [^\b] a\Bbc abc
4) 邏輯、分組
| 代表左右表達式任意匹配一個,優先匹配左邊的表達式 abc|def abc 或 def
(...) 括起來的表達式將作爲分組,分組將作爲一個整體,可以後接數量詞 (abc){2} abcabc
(?P<name>...) 分組,功能與 (...) 相同,但會指定一個額外的別名 (?P<id>abc){2} abcabc
\<number> 引用編號爲 <number> 的分組匹配到的字符串 (\d)abc\1 1abe1 或 5abc5
(?P=name) 引用別名爲 <name> 的分組匹配到的字符串 (?P<id>\d)abc(?P=id) 1abe1 或 5abc5
5) 特殊構造(不作爲分組)
(?:...) (…) 的不分組版本,用於使用 "|" 或後接數量詞 (?:abc){2} abcabc
(?iLmsux) iLmsux 中的每個字符代表一種匹配模式,只能用在正則表達式的開頭,可選多個 (?i)abc AbC
(?#...) # 後的內容將作爲註釋被忽略。 abc(?#comment)123 abc123
(?=...) 之後的字符串內容需要匹配表達式才能成功匹配 a(?=\d) 後面是數字的 a
(?!...) 之後的字符串內容需要不匹配表達式才能成功匹配 a(?!\d) 後面不是數字的 a
(?<=...) 之前的字符串內容需要匹配表達式才能成功匹配 (?<=\d)a 前面是數字的a
(?<!...) 之前的字符串內容需要不匹配表達式才能成功匹配 (?<!\d)a 前面不是數字的a
Regexp 包的使用
下面通過幾個示例來演示一下 regexp 包的使用。
package main
import (
"fmt"
"regexp"
)
func main() {
str := "abcdefg13456abcdefg13456"
reg := regexp.MustCompile("a.*3")
// FindString
res := reg.FindString(str)
fmt.Printf("res: %v\n", res)
// FindAllString
resArray := reg.FindAllString(str, -1)
fmt.Printf("res: %v\n", resArray)
// 禁止貪婪匹配
reg2 := regexp.MustCompile("(?U)a.*3")
resArray = reg2.FindAllString(str, -1)
fmt.Printf("res: %v\n", resArray)
// 替換示例
str = reg2.ReplaceAllString(str, "--")
fmt.Printf("str: %v\n", str)
}
十、Go 語言切片反序
切片反序概念
切片反序是指將一個切片中的元素按照相反的順序重新排列,即將原來的最後一個元素放置在第一個位置,原來的第一個元素放置在最後一個位置,以此類推。
使用 sort.Slice() 對切片進行排序
sort.Slice() 函數是 Go 語言中一個非常有用的排序函數。該函數可以對切片進行排序,同時也可以自定義排序規則。
sort.Slice() 函數的運行原理如下:
1 接收一個待排序的切片和一個 less 函數作爲參數。less 函數接收兩個參數,用於比較兩個元素的大小,並返回 bool 類型的結果。
2 sort.Slice() 函數會使用快速排序算法對切片進行排序。
3 在排序的過程中,sort.Slice() 函數會調用 less 函數來比較切片中的元素大小。
4 排序完成後,sort.Slice() 函數返回排好序的切片。
示例
package main
import (
"fmt"
"sort"
)
func main() {
// 定義一個切片
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 將切片反序
sort.Slice(nums, func(i, j int) bool { return i > j })
// 打印反序後的切片
fmt.Println(nums)
}
// 輸出
// [10 9 8 7 6 5 4 3 2 1]
十一、GO 獲取內網 IP
可以通過 net.InterfaceAddrs() 來獲取內網相關數據,從而獲得內網 IP :
package main
import (
"fmt"
"net"
"os"
)
// 獲取內網 IP
func GetIntranetIp() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
IntranetIp := ""
for _, address := range addrs {
// 檢查ip地址判斷是否迴環地址
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
IntranetIp = ipnet.IP.String()
break
}
}
}
return IntranetIp
}
func main() {
ip := GetIntranetIp()
fmt.Printf("ip: %v\n", ip)
}
十二、Go 語言回調函數
回調函數
回調函數是一種通過將函數作爲參數傳遞給其他函數,並在需要時由其他函數調用的技術。
在 Go 語言中實現回調函數
要使用回調函數,需要先定義一個函數類型,然後將該類型的函數作爲參數傳遞給其他函數。
以下是一個簡單的回調函數的示例:
package main
import (
"fmt"
)
// 定義一個函數類型
type Callback func(int) int
// 執行回調函數的函數
func process(data int, callback Callback) int {
// 調用回調函數
result := callback(data)
return result
}
func main() {
// 通過匿名函數實現回調函數
result := process(5, func(i int) int {
return i + 2
})
fmt.Println(result) // 輸出 7
}
動態參數形式的回調函數
package main
import (
"fmt"
)
// 定義一個函數類型
type Callback func(...any) int
// 執行回調函數的函數
func process(data int, callback Callback) int {
// 調用回調函數
result := callback(data)
return result
}
func main() {
// 通過匿名函數實現回調函數
result := process(5, func(args ...any) int {
num, ok := args[0].(int)
if ok {
return num + 3
} else {
return 0
}
})
fmt.Println(result) // 輸出 8
}
十三、Go 語言追加內容到文件中
Go 語言追加內容到文件中
在 Go 語言中,可以使用 os 和 io/ioutil 包來追加內容到文件。示例代碼:
package main
import (
"fmt"
"os"
)
func main() {
// 打開文件,如果文件不存在則創建
file, err := os.OpenFile("demo.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// 要追加的內容
content := "新的內容\n"
// 追加內容到文件
_, err = file.WriteString(content)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("執行成功")
}
在上面的演示示例中 :
我們使用 os.OpenFile 函數打開一個名爲 demo.txt 的文件。
通過指定 os.O_APPEND 標誌,聲明在文件末尾追加內容而不是覆蓋原有內容。
另外,我們還指定了 os.O_WRONLY 標誌以只寫方式打開文件,以及 os.O_CREATE 標誌以創建文件(如果文件不存在)。
文件權限設置爲 0777。
最後,我們定義了要追加的內容,並使用 file.WriteString 方法將其寫入文件。
十四、gin SetTrustedProxies 的作用
在 Gin 框架中,SetTrustedProxies 是一個設置函數,用於指定哪些代理可以信任併發送請求頭 X-Forwarded-For 和 X-Real-IP。
當使用 Gin 構建 API 服務器時,通常部署在反向代理(例如 Nginx)後面。客戶端的請求通過反向代理轉發到 Gin 服務器,Gin 服務器通過 X-Forwarded-For 和 X-Real-IP 獲取客戶端的真實 IP 地址。
默認情況下,Gin 會信任所有的代理服務器,這意味着所有的代理服務器都會被 Gin 認爲是可信任的,並將它們的 IP 地址作爲 X-Forwarded-For 和 X-Real-IP 的值。這可能會導致安全問題,因爲攻擊者可以通過僞造代理服務器的 IP 地址來攻擊你的服務器。
爲了解決這個問題,Gin 提供了 SetTrustedProxies 方法,可以指定哪些代理可以信任。這個方法接收一個字符串切片參數,參數中的每個字符串都代表一個可以信任的代理服務器 IP 地址。例如:
func main() {
router := gin.Default()
// 設置 Gin 只信任本機的代理服務器
router.SetTrustedProxies([]string{"127.0.0.1"})
// ...
}
在上面的例子中,Gin 只信任本機的代理服務器,即只有來自 127.0.0.1 的請求會被 Gin 認爲是可信任的,並將它們的 IP 地址作爲 X-Forwarded-For 和 X-Real-IP 的值。其他代理服務器的 IP 地址會被 Gin 忽略。這樣可以有效地防止攻擊者通過僞造代理服務器的 IP 地址來攻擊你的服務器。
十五、使用 go pprof 工具進行性能分析
pprof 介紹
pprof 是 Go 的性能分析工具,在程序運行過程中,可以記錄程序的運行信息,可以是 CPU 使用情況、內存使用情況、goroutine 運行情況等,當需要性能調優或者定位 Bug 時候,這些記錄的信息是相當重要。
go 對於 profiling 支持的比較好,標準庫就提供了 profile 庫 runtime/pprof 和 net/http/pprof,而且也提供了很多好用的可視化工具來輔助開發者做 profiling。
對於在線服務,對於一個 HTTP Server,訪問 pprof 提供的 HTTP 接口,獲得性能數據。
性能數據獲取
pprof 的應用場景主要分爲兩種:
- 服務型應用,例如 web 服務器等各種服務類型端的性能分析;
- 工具型應用,例如一些命令行工具,執行完畢後直接退出的應用;
1. 工具型應用性能分析示例
工具型應用性能分析基於 "runtime/pprof" 包,將性能數據存放在文件中 :
package main
import (
"fmt"
"os"
"runtime/pprof"
)
func main() {
// 創建 cup 數據記錄文件
cpuProfile, err := os.Create("./cpu_profile")
if err != nil {
fmt.Printf("創建文件失敗:%s", err.Error())
return
}
defer cpuProfile.Close()
// 創建內存數據記錄文件
memProfile, err := os.Create("./mem_profile")
if err != nil {
fmt.Printf("創建文件失敗:%s", err.Error())
return
}
defer memProfile.Close()
//採集CPU信息
pprof.StartCPUProfile(cpuProfile)
defer pprof.StopCPUProfile()
//採集內存信息
pprof.WriteHeapProfile(memProfile)
// 執行一個邏輯
for i := 0; i < 9999; i++ {
fmt.Println("pprof 工具型測試")
}
println("-- mian done --")
}
使用 go tool pprof 命令查看數據
執行上面的代碼後,會產生 2 個性能數據文件 : cpu_profile 和 mem_profile,可以使用 go tool pprof 命令查看數據 :
# 01. 執行命令
# cup 使用數據
go tool pprof .\cpu_profile
# 內存使用數據
# go tool pprof .\mem_profile
# 02. 回顯 :
PS ...\goDemo> go tool pprof .\cpu_profile
File: main.exe
Build ID: ...\main.exe2023-10-11 13:54:05.4308623 +0800 CST
Type: cpu
Time: Oct 11, 2023 at 1:54pm (CST)
Duration: 979.59ms, Total samples = 510ms (52.06%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
# 03. 輸入命令繼續
top5
# 數據內容 :
flat flat% sum% cum cum%
510ms 100% 100% 510ms 100% runtime.cgocall
0 0% 100% 510ms 100% fmt.Fprintln
0 0% 100% 510ms 100% fmt.Println
0 0% 100% 510ms 100% internal/poll.(*FD).Write
0 0% 100% 510ms 100% internal/poll.(*FD).writeConsole
列信息字段作用 :
flat:函數在 CPU 上運行的時間
flat%:函數在CPU上運行時間的百分比
sum%:是從第一行到當前行所有函數累加使用 CPU 的比例,如第二行sum=53.85=30.77+23.08
cum:這個函數以及子函數運行所佔用的時間,應該大於等於flat
cum%:這個函數以及子函數運行所佔用的比例,應該大於等於flat%
最後一列:函數的名字
以 web 界面形式查看數據
您還可以開啓一個 web 服務,以 web 形式查看性能數據 :
go tool pprof -http=:8808 .\mem_profile
注意 : 需要安裝 graphviz
graphviz
Graphviz 是一款由 AT&T Research 和 Lucent Bell 實驗室開源的可視化圖形工具,可以很方便的用來繪製結構化的圖形網絡,支持多種格式輸出。Graphviz 輸入是一個用 dot 語言編寫的繪圖腳本,通過對輸入腳本的解析,分析出其中的點、邊及子圖,然後根據屬性進行繪製。Graphviz layout 以簡單的文本語言描述圖形,並以實用的格式製作圖表,如用於網頁的 images 和 SVG ;用於放入在其它文件中或顯示在交互式圖形瀏覽器中的 PDF 和 Postscript 。
安裝 graphviz
官網 : https://www.graphviz.org/
下載及安裝 graphviz : https://www.graphviz.org/download/
再次運行性能數據查看命令
go tool pprof -http=:8808 .\mem_profile
會打開一個瀏覽器窗口,以圖形界面形式展示性能數據 :
1. 服務型應用性能分析示例
對於服務類型的應用,主要在服務內部匿名引入 net/http/pprof 包,然後通過 HTTP 訪問 pprof 頁面。
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
http.HandleFunc("/", Test)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("ListenAndServe Err:", err.Error())
return
}
}
func Test(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "")
}
上面的代碼開啓了一個 HTTP 服務,端口爲 8080,可以通過瀏覽器訪問 :
http://localhost:8080/ 進行測試。
查看服務性能數據 :
執行 http://localhost:8080/debug/pprof / 可以看到畫像信息:
項目功能描述
allocs:過去所有內存分配的採樣
block:導致同步基元阻塞的堆棧跟蹤
cmdline:當前程序的命令行調用
goroutine:堆棧所有當前goroutine的跟蹤。使用debug=2作爲查詢參數,以與未恢復的死機相同的格式導出。
heap:活動對象的內存分配的採樣。您可以指定gc GET參數以在獲取堆樣本之前運行gc。
mutex:爭用互斥體持有者的堆棧跟蹤
profile:CPU配置文件。您可以在seconds GET參數中指定持續時間。獲取配置文件後,使用go tool pprof命令來調查該配置文件。
threadcreate:導致創建新操作系統線程的堆棧跟蹤
trace:當前程序執行的跟蹤。您可以在seconds GET參數中指定持續時間。獲取跟蹤文件後,使用go tool trace命令來調查跟蹤。
記錄指定時間段內的性能數據 :
瀏覽器訪問 : http://localhost:8080/debug/pprof/profile?seconds=10
10 秒( 可以通過參數設置監控時長 )後 (期間可以訪問網站首頁或者使用 ab 進行壓測) 會自動下載得到一個性能文件 : profile。
在 profile 同目錄下執行 :
go tool pprof .\profile
# 回顯
Type: cpu
...
# 繼續輸入
top
# 顯示
Showing top 10 nodes out of 146
flat flat% sum% cum cum%
1700ms 28.05% 28.05% 1710ms 28.22% runtime.cgocall
560ms 9.24% 37.29% 560ms 9.24% runtime.stdcall1
450ms 7.43% 44.72% 460ms 7.59% runtime.stdcall6
350ms 5.78% 50.50% 1910ms 31.52% github.com/cnlesscode/graceMessageQueue/kernel.SaveMsgToDisk
310ms 5.12% 55.61% 320ms 5.28% runtime.mapaccess1_faststr
120ms 1.98% 57.59% 150ms 2.48% runtime.chanrecv
110ms 1.82% 59.41% 570ms 9.41% runtime.netpoll
80ms 1.32% 60.73% 80ms 1.32% runtime.siftupTimer
70ms 1.16% 61.88% 70ms 1.16% runtime.casgstatus
70ms 1.16% 63.04% 80ms 1.32% runtime.lock2
# 使用 list 命令查看耗時函數
list github.com/cnlesscode/graceMessageQueue/kernel.SaveMsgToDisk
以 web 形式查看數據
go tool pprof -http=:9090 .\profile
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rv-TFF1Hf2TGcybPQp4z1Q