慎寫指針類型的全局變量
簡述:
在 關于range二三事[1] 第二個case中,介紹了對于指針類型的 切片/map變量A 的循環(huán),要格外注意, 迭代出的value作用域是整個方法而非循環(huán)體內(nèi).
改進辦法:在循環(huán)體中引入中間變量,"暫存"下每次迭代的value的值
但對于這個A,如果是全局變量,則又極有可能出現(xiàn)問題:
package main
import (
"fmt"
)
type UserInfo struct {
Name string
Age int
}
var (
defaultInfo = UserInfo{Name: "fliter", Age: 26}
defaultInfoSli = []*UserInfo{&defaultInfo}
)
func main() {
for _, v := range defaultInfoSli {
tmp := v
//go func() {
tmp.Age = 100
//}()
}
//time.Sleep(1e9)
fmt.Println(defaultInfoSli[0].Age)
}
defaultInfoSli迭代出的v為指針類型,tmp仍為指針類型,對其賦值,會改變?nèi)肿兞?strong style="color:rgb(22,94,202);">defaultInfoSli的值
復現(xiàn):
在具體業(yè)務場景中,服務啟動時初始化(取數(shù)據(jù)庫或redis,或讀取配置文件,加載到內(nèi)存中)了一個全局變量.每個http請求過來,golang都會有一個新的協(xié)程去處理相關邏輯. 對于某個具體方法內(nèi)的變量,對每次請求都是獨立和隔離(每次請求都相當于一個個cellar,彼此之間不會有干涉和影響), 但對于永久存在內(nèi)存中的全局變量,如果有對其寫操作,每次請求都會影響該全局變量. 當出現(xiàn)并發(fā)請求如用戶x和y同時請求接口, 兩次請求都會改寫全局變量, 這時就很可能出現(xiàn)返回的x和y的數(shù)據(jù)錯亂
Demo如下:
package main
import (
"encoding/json"
"fmt"
"github.com/davecgh/go-spew/spew"
"log"
"net/http"
)
type BookInfo struct {
Title string
Rank int
Data interface{}
}
var (
defaultBook1 = BookInfo{Title: "水滸傳", Rank: 1}
defaultBook2 = BookInfo{Title: "三國演義", Rank: 2}
defaultBook3 = BookInfo{Title: "西游記", Rank: 3}
defaultBook4 = BookInfo{Title: "紅樓夢", Rank: 4}
DefaultBookSli = []*BookInfo{&defaultBook1, &defaultBook2, &defaultBook3, &defaultBook4}
)
type CommonParams struct {
ID int64
Name string
}
var (
ModuleHandlers = map[int]func(params *CommonParams) *BookInfo{
1: HandleTypeOne,
2: HandleTypeTwo,
3: HandleTypeThree,
4: HandleTypeFour,
}
)
func main() {
fmt.Println(DefaultBookSli)
http.HandleFunc("/index", deal) //設置訪問的路由
err := http.ListenAndServe(":80", nil) //設置監(jiān)聽的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func deal(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
//fmt.Println("name值為:", name)
par := &CommonParams{
ID: 0,
Name: name,
}
// 獲取相關數(shù)據(jù)
for _, v := range DefaultBookSli {
module := v
//fmt.Println("module is:", module)
//fmt.Println("排序為:", module.Rank)
m := ModuleHandlers[module.Rank](par "module.Rank")
// 填充模塊數(shù)據(jù)
if m.Data != nil {
module.Data = m.Data
}
// (如果需要),重寫模塊標題(在此不需要)
//if m.Title != "" {
// module.Title = m.Title
//}
}
//time.Sleep(1e9) //此處等待并不是因為協(xié)程,而是方便測試,不加這個等待,執(zhí)行100次秒速就完成. 加這個等待是為了方便模擬"幾個用戶同時請求"
//fmt.Println(DefaultBookSli[0].Rank)
spew.Dump(DefaultBookSli)
rsJson, _ := json.Marshal(DefaultBookSli)
//fmt.Println(string(rsJson)) //這個寫入到w的是輸出到客戶端的
fmt.Fprintf(w, string(rsJson)) //這個寫入到w的是輸出到客戶端的
}
func HandleTypeOne(p *CommonParams) *BookInfo {
res := ""
if p.Name == "施耐庵" {
res = "我叫施耐庵,我是作者!"
}
return &BookInfo{Data: res}
}
func HandleTypeTwo(p *CommonParams) *BookInfo {
res := ""
if p.Name == "羅貫中" {
res = "我叫羅貫中,我是作者!"
}
return &BookInfo{Data: res}
}
func HandleTypeThree(p *CommonParams) *BookInfo {
res := ""
if p.Name == "吳承恩" {
res = "我叫吳承恩,我是作者!"
}
return &BookInfo{Data: res}
}
func HandleTypeFour(p *CommonParams) *BookInfo {
res := ""
if p.Name == "曹雪芹" {
res = "我叫曹雪芹,我是作者!"
}
return &BookInfo{Data: res}
}
帶著參數(shù)x, 使用Postman進行串行調(diào)用[2]100次,
同時再訪問這個接口,帶參數(shù)y,此時可以發(fā)現(xiàn),出現(xiàn)了數(shù)據(jù)錯亂:
修改方案:
在module := v這一步,實際上module依然是指針類型.
可以module := *v,這樣module就不是指針類型,也就不會出現(xiàn)如上問題.
當時問題緊急,直接在里面新加了一個臨時變量,即:
// 獲取相關數(shù)據(jù)
for _, v := range DefaultBookSli {
module := v
var temModule = &BookInfo{
Title: module.Title,
Rank: module.Rank,
}
m := ModuleHandlers[temModule.Rank](par "temModule.Rank")
// 填充模塊數(shù)據(jù)
if m.Data != nil {
module.Data = m.Data
}
}
詳細過程參見 私有筆記 并發(fā)寫全局變量導致的數(shù)據(jù)錯亂問題[3],印象深刻的一次體驗
參考資料
[1]關于range二三事: https://dashen.tech/2018/10/03/%E5%85%B3%E4%BA%8Erange%E4%BA%8C%E4%B8%89%E4%BA%8B/
[2]Postman進行串行調(diào)用: https://www.cnblogs.com/stm32stm32/p/10434399.html
[3]并發(fā)寫全局變量導致的數(shù)據(jù)錯亂問題: https://note.youdao.com/web/#/file/WEB058d4e136c3ee281320806fd45e3b07a/note/WEB6c29aebf58ba0868bdeef60f7e6bac40/
想要了解Go更多內(nèi)容,歡迎掃描下方??關注公眾號, 回復關鍵詞 [實戰(zhàn)群] ,就有機會進群和我們進行交流
分享、在看與點贊Go
