位於新疆天山山脈南麓的塔里木油田是西氣東輸主氣源地,是中國天然氣增產的重要領域

位於新疆天山山脈南麓的塔里木油田是西氣東輸主氣源地,是中國天然氣增產的重要領域,今年塔里木油田新建天然氣產預估能突破30億立方米,可以滿足千萬戶居民日常生活用氣。   

天山南麓地層深處蘊藏著雄厚的天然氣資源,根據中石油塔里木油田勘探開發研究院院長表示,整個庫車(天山南麓)天然氣勘探面積是2.8萬平方公里,天然氣資源量是7萬億方。

一年來塔里木油田相繼在這裡發現了「中秋1」、「博孜9」兩個千億方級氣藏。目前天山南麓已建成「迪那」、「克拉-克深」、「博孜-大北」三個氣田群,具備年產240億方天然氣生產能力。還有21口重點探井和37口產能建設井正在鑽探,預計明年還將新增天然氣產能近40億立方米。 

 

本站聲明:網站內容來源於https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※影響示波器測試準確度的五大因素

※無毒橡膠墊片哪裡買的到?

※飲用桶裝水到底安不安全? 破解錯誤迷思!

※十大封口機人氣排行榜-烘焙必備幫手!

連續封口機購物網-不怕你比價,就怕你買貴!

※買不起高檔茶葉,精緻包裝茶葉罐,也能撐場面!

js數組方法大全(上)

# js數組方法大全(上)

記錄一下整理的js數組方法,免得每次要找方法都找不到。圖片有點多,注意流量,嘻嘻!

本期分享

  • join()
  • reverse()
  • sort()
  • concat()
  • slice()
  • splice()
  • push()
  • pop()
  • unshift()
  • shift()
  • toString()
  • toLocaleString()

下期分享

  • forEach()
  • map()
  • filer()
  • every()
  • some()
  • reduce()
  • reduceRight()
  • indexOf()
  • lastIndex()

join() —>用指定分割符將數組轉為字符串

  • 使用熱度:常用
  • 是否改變原始數組:否
  • 返回:按指定字符串進行分割后的字符串
  • 參數:
參數位置 參數類型 是否必選 作用
1 string 將數組轉為字符串,並用指定字符進行分割
  • 說明:不傳入參數的話,將數組元素轉為字符串,並用逗號進行分割
  • 實例如下:
var log=console.log;
var a=[1,2,3];
log(a.join());
log(a.join(" "));
log(a.join(""));
var b = new Array(10);
log(b.join('-'))

reverse() —>將數組元素顛倒

  • 使用熱度:不常用
  • 是否改變原始數組:是
  • 返回:將數組中的元素顛倒順序,返回逆序的數組。
  • 參數:無
  • 實例如下:
var log=console.log;
var a=[1,2,3];
a.reverse();
log(a);

sort() —>按指定要求對數組進行排序

  • 使用熱度:不常用
  • 是否改變原始數組:是
  • 返回:返回排序后的數組
  • 參數:
參數位置 參數類型 是否必選 作用
1 function 函數的兩個參數分別是數組對應的兩個元素,函數返回大於0,則第一個參數排在前面。函數返回一個小於0的數,則第一個參數排在後面。函數返回0,代表這兩個參數的排序無關緊要。
  • 說明:不傳入參數的時候,會將數組元素按字母表排序並返回,如果元素非字符串,將會臨時轉為字符串進行比較,如果元素中有undefined,則會甩到最後面。
  • 實例如下:
var log=console.log;
var a=[,'a','b',true];
a.sort()
log(a)
var b=[3,7,4,4,2]
b.sort(function(i,j){
    return i-j
})
log(b)

concat() —>將數組和其他元素合併返回新的數組

  • 使用熱度:常用
  • 是否改變原始數組:否
  • 返回:返回一個合併了的新數組
  • 參數:
參數位置 參數類型 是否必選 作用
1+ * 將原始數組的每個元素和每個參數合併到一個新的數組並返回
  • 說明:如果參數中有數組,這將數組拆分合併,而不是直接合併數組本身,但是不遞歸扁平化數組的數組。
  • 實例如下:
var log=console.log;
var a=[1,2,3];
var b=a.concat(4,5,6,[7,8,[9,10]]);
log(a);
log(b);

slice() —>截取數組一段進行返回

  • 使用熱度:常用
  • 是否改變原始數組:否
  • 返回:一個數組的一個片段或者子數組
  • 參數:
參數位置 參數類型 是否必選 作用
1 number 用來指定要返回的數組片段開始位置
2 number 用來指定要返回數組的結束位置,如不指定,則表示返回到數組末尾
  • 說明:如果參數是一個負數,則從數組倒數開始和結束。
  • 實例如下:
var log=console.log;
var a=[1,2,3,4,5,6];
var b =a.slice(1)
log(b)
var c=a.slice(1,-1)
log(c)
var d=a.slice(-3,-1)
log(d)

splice() —>刪除或者替代數組指定區域

  • 使用熱度:經常用
  • 是否改變原始數組:是
  • 返回:刪除的數組,如果未刪除則返回空數組
  • 參數:
參數位置 參數類型 是否必選 作用
1 number 用來指定插入或者刪除的起始位置
2 number 指定要刪除或者替代數量,如果不指定,這會刪除所有
3+ * 替代的元素
  • 實例如下:
var log=console.log;
var a=[1,2,3,4,5,6,7,8,9];
var b=a.splice(8);
log(a);
log(b);

var c=a.splice(5,1);
log(a);
log(c);

var d=a.splice(2,2,'a',[33,44]);
log(a);
log(d)

push() —>在數組元素後面增加元素

  • 使用熱度:頻繁使用
  • 是否改變原始數組:是
  • 返回:新數組的長度
  • 參數:
參數位置 參數類型 是否必選 作用
1+ * 在數組末尾增加一個或多個數組元素
  • 說明:在數組末尾增加一個元素
  • 實例如下:
var log=console.log;
var a=[1,2,3];
var b=a.push()
log(a)
log(b)

var c=a.push(4,5,6);
log(a)
log(c)

pop() —>刪除數組元素後面的一個元素

  • 使用熱度:不常用
  • 是否改變原始數組:是
  • 返回:被刪除的數組
  • 參數:無
  • 說明:刪除數組末尾的一個元素
  • 實例如下:
var log=console.log;
var a=[1,2,3,4,5,6];
var b=a.pop()
log(a)
log(b)

unshift() —>在數組前面增加元素

  • 使用熱度:常用
  • 是否改變原始數組:是
  • 返回:新數組的長度
  • 參數:
參數位置 參數類型 是否必選 作用
1+ * 在數組頭部增加一個或多個數組元素
  • 說明:當使用多個參數調用unshift方法的時候它的行為令人驚訝。參數是一次性插入的(就像splice方法),而非一次一個插入。這意味着最終的數組中插入的元素的順序和他們在參數列表中的順序一致。而假設元素是一次一個的插入。他們的順序應該是反過來的。
  • 實例如下:
var log=console.log;
var a=[1,2,3];
var b=a.unshift()
log(a)
log(b)

var c=a.unshift(4,5,6);
log(a)
log(c)

shift() —>刪除數組第一個元素

  • 使用熱度:不常用
  • 是否改變原始數組:是
  • 返回:被刪除的數組
  • 參數:無
  • 說明:刪除數組末尾的一個元素
  • 實例如下:
var log=console.log;
var a=[1,2,3,4,5,6];
var b=a.shift()
log(a)
log(b)

toString —>將數組轉為字符串

  • 使用熱度:常用
  • 是否改變原始數組:否
  • 返回:數組字符串
  • 參數:無
  • 說明:輸出不包括方括號或者其他任何形式的包裹數組值的分隔符;此方法與不使用任何參數調用join方法返回的字符串一樣。
  • 實例如下:
var log=console.log;
var a=["a",2,{"b":"c"},["d"]];
var b=a.toString()
log(a)
log(b)

toLocaleString() —>將數組使用本地化的方式轉為字符串

  • 使用熱度:不常用
  • 是否改變原始數組:否
  • 返回:
  • 參數:
參數位置 參數類型 是否必選 作用
1 string/array 縮寫語言代碼(BCP 47 language tag,例如:cmn-Hans-CN)的字符串或者這些字符串組成的數組
2 string/object 對字符串或數組處理的方式
  • 說明:在舊的瀏覽器實現中,會忽略這兩個參數,使用的語言環境和返回的字符串的形式完全取決於實現方式。
  • 實例如下:
var log=console.log
var a = [111,222,333];
var b=a.toLocaleString('ar-EG')
var c=a.toLocaleString('zh-Hans-CN-u-nu-hanidec')
log(a);
log(b);  
log(c);  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

碼出優美

  一份擁有良好可讀性和拓展性的代碼是項目里的良藥,它不僅看着舒服,改起來也方便,甚至還能重用,各模塊邏輯分明。“見碼知功底”,而要達到高手那種簡潔有力的境界,需要進行大量的總結和練習,今天我們就來談談如何寫出優美的代碼。

  

命名

  好的命名應該具有如下特徵:

  1,意思正確。這是最基本的要求,不要掛羊頭賣狗肉,詞不達意,要一眼就知道什麼意思。就算一眼看不出來,複製到有道詞典翻譯一下也能知道什麼意思;

  2,單複數分明。如果是一個數組,要麼加s/es結尾表明其是複數,要麼加入list表示它是一個數組。如cars,carList都可以表達一個車的列表;

  3,慎用縮略詞。縮略詞可以讓我們的命名更加簡潔,但是一!定!要!是!業!界!通!用!縮!略!詞!比如info原意為information,msg原意為message,fn原意function,conf原意config等等,這些縮略詞都是業內傳統了,大家都知道什麼意思,切記不要自己亂造縮略詞;

  4,有具體含義。根據業務場景去命名,而不是根據抽象命名;比如getUnreadMsgList,一看就知道是獲取未讀消息列表的意思,而getData這種說了跟沒說一樣,缺乏具體含義。

 

註釋

  有表達力的代碼是不需要註釋的。比如一個init函數,一看就知道是用於做一些初始化的工作,沒必要寫多餘的註釋來說明。

  但是有一些場景註釋是非常有必要的,下面幾種場景要添加註釋:

  1,一些針對特殊業務場景而訂製的特殊邏輯。比如當我們更新個人信息的時候,由於後端的問題,需要少傳一個諸如生日的信息,或者更新頭像時要多傳一個時間戳來供其他業務以後使用。這些莫名其妙的增刪屬性,如果不加以註釋,將導致後續自己都無法理解;

  2,可能會出現隱患的代碼。由於自身水平所限,或者本身技術上就無法實現,只能通過一些特殊技巧來仿製一些效果,往往會存在安全隱患,比如:用戶操作太快會出問題,網絡太慢會出問題,某個接口調不通這頁面會全掛了,一些特殊的操作會引起的暫時無法解決的bug等等場景,都需要註釋說明;

  3,涉及到某些高深的或者生僻的技術知識。這種也要註釋,以提醒自己和其他開發者。

  一般來說,註釋基本上都是在表達“這裏我為什麼這麼做”,很少有註釋會去表達“我是一個什麼玩意兒”,如果是後者的註釋,只能說明命名沒做好。

  

函數

  函數是代碼的靈魂,也是寫邏輯的載體,以下幾個要求是判斷一個開發者函數寫的好不好的標準。

  1,是不是單一職責。一個函數應該只做一件事,而這件事應該能通過函數名就可以清晰的展示。這是一個非常好的特性,一個辣雞的函數可能動輒幾百行代碼,各種邏輯堆積在一起,看得人頭腦發暈,甚至開發者自己都理不清楚;判斷這個函數是不是單一職責的技巧很簡單:看看它還能不能再拆分。

  2,有沒有層級之分函數與函數之間是有身份地位之分的,有負責整體大局觀的高級函數,也有專註細節的打工仔低級函數。如果不建立起層級結構,就容易迷失在細節的海洋里。比如:

  

 

 

  對於做一頓飯這個函數來說,他不需要關心諸如買菜時反覆挑選這種細節,它只需要知道,大概有三個大步驟就行了。所以這段邏輯會根據其地位拆分出不同等級的函數,假設沒有層級之分,那麼從第一步的“搭公交”一直寫到最後一步的“倒垃圾”,這東西會變得極難維護。

 

  3,承載的場景是否足夠簡單。有時候我們會遇到這樣一種場景:一個函數在很多地方都會用到,但是不同地方傳入的參數不一樣,這樣我們為了函數的通用性,就針對入參做了很多種場景的識別,導致入參非常多,裏面還需要根據不同的場景做邏輯上的細微調整。所以單單是取傳入參數都夠嗆了,一堆的if else或switch case。

  這個函數太難了,而且這已經違背了函數的單一職責原則,它在裏面做了多種場景的判斷,從而表現出不同的行為,但這些行為又不是完全不同,而是“大體相同”,如果重寫好像又會增加很多的重複的代碼。解決的方法是做更小粒度的拆分,將那些真正與業務脫鈎的部分抽離出來,而不同場景對應不同的處理函數,剛剛抽離的業務脫鈎函數正好作為這些不同場景函數公共部分!

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

【Go 入門學習】第一篇關於 Go 的博客–Go 爬蟲初體驗

一、寫在前面

  其實早就該寫這一篇博客了,為什麼一直沒有寫呢?還不是因為忙不過來(實際上只是因為太懶了)。不過好了,現在終於要開始寫這一篇博客了。在看這篇博客之前,可能需要你對 Go 這門語言有些基本的了解,比如基礎語法之類的。話不多說,進入正題。

 

二、Go 環境配置

1.安裝配置

  在學習一門語言時,第一步就是環境配置了,Go 也不例外,下面就是 Windows 下 Go 開發環境的配置過程了。

  首先你需要下載 Go 的安裝包,可以打開 Go 語言中文網下載,地址為:。

  下載完成后打開安裝(例如安裝到 E:\Go 目錄),然後配置環境變量,將安裝目錄下的 bin 目錄路徑加入環境變量中。這一步完成后打開命令行,輸入 go version,若出現版本信息則表明配置成功。

2.配置 GOPATH 和 GOROOT

  除了要將 bin 目錄加入到環境變量中,還要配置 GOPATH 和 GOROOT,步驟如下:

  在用戶變量中新建變量 GOPATH:

  

   在系統變量中新建變量 GOROOT:

  

 3. 選擇 IDE

  在 IDE 的選擇上,我比較推薦使用 jetbrains 家的 GoLand,功能強大,使用起來也很方便。

 

三、下載網頁

  下載網頁使用的是 Go 中原生的 http 庫,在使用前需要導包,和 Python 一樣用 import 導入即可。如果要發送 GET 請求,可以直接使用 http 中的 Get() 方法,例如:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "net/http"
 6 )
 7 
 8 func main () {
 9     html, err := http.Get("https://www.baidu.com/")
10     if err != nil {
11         fmt.Println(err)
12     }
13     fmt.Println(html)
14 }

  Get() 方法有兩個返回值,html 表示請求的結果,err 表示錯誤,這裏必須對 err 做判斷,Go 語言的錯誤機制就是這樣,這裏不多做解釋。

  這麼用起來確實很簡單,但是不能自己設置請求頭,如果要構造請求頭的話可以參考下面的例子:

1 req, _ := http.NewRequest("GET", url, nil)
2 // Set User-Agent
3 req.Header.Add("UserAgent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36")
4 client := &http.Client{}
5 resp, err := client.Do(req)

 

四、解析網頁

1.解析庫選擇

  Go 語言中可以用來解析網頁的庫也是很齊全的,XPath、CSS 選擇器和正則表達式都能用。這裏我用的是 htmlquery,這個庫使用的是 Xpath 選擇器。htmlquery 是用於 HTML 的 XPath 數據提取庫,可通過 XPath 表達式從 HTML 文檔中提取數據。Xpath 語法就不提了,畢竟用 Python 寫爬蟲的時候沒少用。

  先說下 htmlquery 的安裝吧,一般會推薦你使用如下命令安裝:

go get github.com/antchfx/htmlquery

  但是你懂的,出於某些原因就下載不下來,怎麼辦呢?對於這種能在 GitHub 上找到的庫直接 clone 到本地就行了,記得要複製到你的 GOAPTH 下

2.使用 htmlquery

  在使用 htmlquery 這個庫的時候,可能會報錯說缺少 golang.org\x\text,和上面的解決辦法一樣,去 GitHub 上找,然後 clone 下來。

  下面是 htmlquery 中經常使用的方法及相應含義:

func Parse(r io.Reader) (*html.Node, error):  返回給定 Reader 的 HTML 的解析樹。

func Find(top *html.Node, expr string) []*html.Node: 搜索與指定 XPath 表達式匹配的 html.Node。

func FindOne(top *html.Node, expr string) *html.Node: 搜索與指定 XPath 表達式匹配的 html.Node,並返回匹配的第一個元素,可簡單理解為 FindOne = Find[0]

func InnerText(n *html.Node) string: 返回對象的開始和結束標記之間的文本。
 
func SelectAttr(n *html.Node, name string) (val string): 返回指定名稱的屬性值。

func OutputHTML(n *html.Node, self bool) string: 返回包含標籤名稱的文本。

   下面是使用 htmlquery 解析網頁的代碼:

 1 // Used to parse html
 2 func parse(html string) {
 3     // Parse html
 4     root, _ := htmlquery.Parse(strings.NewReader(html))
 5     titleList := htmlquery.Find(root, `//*[@id="post_list"]/div/div[2]/h3/a/text()`)
 6     hrefList := htmlquery.Find(root, `//*[@id="post_list"]/div/div[2]/h3/a/@href`)
 7     authorList := htmlquery.Find(root, `//*[@id="post_list"]/div/div[2]/div/a/text()`)
 8 
 9     // Traverse the result
10     for i := range titleList {
11         blog := BlogInfo{}
12         blog.title = htmlquery.InnerText(titleList[i])
13         blog.href = htmlquery.InnerText(hrefList[i])
14         blog.author = htmlquery.InnerText(authorList[i])
15         fmt.Println(blog)
16     }
17 }

  需要注意的是由於在 Go 語言中不支持使用單引號來表示字符串,而要使用反引號“`”和雙引號來表示字符串。然後因為 Find() 方法返回的是一個數組,因而需要遍歷其中每一個元素,使用 for 循環遍歷即可。在 for 循環中使用到的 BlogInfo 是一個結構體,表示一個博客的基本信息,定義如下:

1 // Used to record blog information
2 type BlogInfo struct {
3     title string
4     href string
5     author string
6 }

 

五、Go 併發

     在 Go 語言中使用 go 關鍵字開啟一個新的 go 程,也叫 goroutine,開啟成功之後,go 關鍵字后的函數就將在開啟的 goroutine 中運行,並不會阻塞當前進程的執行,所以要用 Go 來寫併發還是很容易的。例如:

 1 baseUrl := "https://www.cnblogs.com/"
 2 for i := 2; i < 4; i ++ {
 3     url := baseUrl + "#p" + strconv.Itoa(i)
 4     // fmt.Println(url)
 5     go request(url)
 6 }
 7 
 8 // Wait for goroutine
 9 time.Sleep(2 * time.Second)
10 request(baseUrl)

  這裏除了在主進程中有一個 request(),還開啟了兩個 go 程來執行 request()。不過要注意的是,一旦主進程結束,其餘 Go 程也會結束,所以我這裏加了一個兩秒鐘的等待時間,用於讓 Go 程先結束。

 

六、體驗總結

  由於我本身才剛開始學習 Go,就還有很多東西沒有學到,所以這個初體驗其實還有很多沒寫到的地方,比如數據保存,去重問題等等,後面會去多看看 Go 的官方文檔。當然了,對我來說,要寫爬蟲的話還是會用 Python 來寫的,不過還是得花時間學習新知識,比如使用 Go 做開發,熟悉掌握 Go 語言就是我的下一目標了。

 

  完整代碼已上傳到 !

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

簡單看看@RequestBody註解原理

       又到了很無聊的時候了,於是隨便看看源碼假裝自己很努力的樣子,哈哈哈;

  記得上一篇博客隨便說了一下RequestBody的用法以及注意的問題,這個註解作為非常常用的註解,也是時候了解一波其中的原理了。

    溫馨提示:閱讀本篇博客,默認你之前大概看過springmvc源碼,懂得其中的基本流程

1.HttpMessageConverter接口

  這個接口就是@RequestBody和@ResponseBody這兩個註解的精髓,我們就先看看這個頂層接口定義了哪些方法:

public interface HttpMessageConverter<T> {

    //判斷當前轉換器是否可以解析前端傳過來的數據
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

    //判斷當前轉換器是否可以將後端數據解析為前端需要的格式
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    //當前轉換器能夠解析所有的數據類型
    List<MediaType> getSupportedMediaTypes();

    //這個方法就是讀取前端傳過來的數據
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

    //將後台數據轉換然後返回給前端
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

}

   

  我們從這個頂層接口中這幾個方法就大概能看到一些東西,可以肯定的就是在@RequestBody和@ResponseBody這兩個註解原理的內部轉換器應該都是實現了這個HttpMessageConverter,因為這個接口裡又是read,又是write;然後就是在上面我只是簡單說了前端傳過來的數據,返回給前端需要的格式這種模糊的說法,為什麼不直接說返回json格式的數據呢?

  哈哈,可能有的小夥伴會說,瑪德,你絕逼是怕說錯,才說這些模糊的說法;咳,當然有部分是這個意思,但是最大的原因就是上面方法參數中有個類MediaType,你打開看看就知道了,這裏定義了很多的可以解析的數據類型,比如”application/json”,”application/xml”,”image/gif”,”text/html”….等等,還有好多聽都沒聽過的;

  其實了解http請求的小夥伴應該已經看出來了,這裏這些數據類型就是下圖所示的這些;當然,我們暫時只關注json的,至於其他類型的怎麼解析有興趣的小夥伴可以研究研究;

 

 2.HandlerMethodArgumentResolver接口

  我們看看這個接口,看名字就知道應該是方法參數解析器,很明顯就是用於解析方法Controller中方法的參數的,還是簡單看看這個接口中的方法:

public interface HandlerMethodArgumentResolver {

    //該解析器是否支持解析Controller中方法中的參數,因為這裏參數類型可以是簡單類型,也可以是集合等類型
    boolean supportsParameter(MethodParameter parameter);

    //開始解析Http請求中的數據,解析出來的數據要和方法參數對應
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

   這個接口定義的方法作用其實就是將http請求中的參數對應到Controller中的參數

 

3.HandlerMethodReturnValueHandler接口 

public interface HandlerMethodReturnValueHandler {

    //這個方法判斷該處理器是否支持返回值類型,這裏的返回值就是controller方法執行后的返回值
    boolean supportsReturnType(MethodParameter returnType);

    //將controller方法的返回值進行解析成前端需要的格式,後續就會丟給前端
    void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

   這個接口的作用:比如一個Controller方法中的返回值是一個集合,那麼springmvc內部在將數據返回給前端之前,就會先拿到所有的返回值解析器,然後遍歷每一個,分別執行supportsReturnType方法,看看哪個解析器可以解析集合類型,找到解析器之後,然後再執行handleReturnValue方法解析就行了,其實2中的方法參數解析器也是這樣的一個步驟

 

4.ServletInvocableHandlerMethod類

  這個類是干什麼的呢?看過springmvc源碼的人應該知道一點,還是簡單說說吧,在springmvc中會將controller中的每個被RequestMapping註解修飾的方法(也可以叫做處理器)給封裝成ServletInvocableHandlerMethod類,封裝后想要執行該處理器方法只需要執行該類的invokeAndHandle方法;

  請注意:就是在invokeAndHandle這個方法中會調用:調用方法參數解析器——>執行處理器方法——–>調用返回值解析器、

  可以簡單看看源碼,請一定要了解springmvc的流程,因為我不會從頭到尾講一遍,我們直接從DispatcherServlet中的doDispatch方法的ha.handle(xxx)這裏說起,這裏主要是執行處理器適配器的handle方法,這裏具體的處理器適配器實現是:AbstractHandlerMethodAdapter

  

  我們進入AbstractHandlerMethodAdapter這個適配器的handle方法看看(^o^)/:

//可以看到這裏就是調用了handleInternal方法,而handleInternal方法未實現
public final ModelAndView handle(HttpServletRequest request,HttpServletResponse response, Object handler)throws Exception {
        return handleInternal(request, response, (HandlerMethod) handler);
    }


//這個方法在子類RequestMappingHandlerAdapter中實現
protected abstract ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response,HandlerMethod handlerMethod) throws Exception;

 

  接下來我們看看RequestMappingHandlerAdapter中實現的handleInternal方法(」゜ロ゜)」:

protected final ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response,HandlerMethod handlerMethod) throws Exception {

        //省略跟邏輯無關的代碼
                .......
               .........
        
        return invokeHandlerMethod(request, response, handlerMethod);
    }

 

  進入invokeHandlerMethod方法看看(´・_・`):

private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,HandlerMethod handlerMethod) throws Exception {
        
        ServletWebRequest webRequest = new ServletWebRequest(request, response);

        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        //省略一些代碼

        //就是再這裏去執行Controller中的處理器方法
        requestMappingMethod.invokeAndHandle(webRequest, mavContainer);

        //此處省略好多代碼
    }

 

  繼續進入到invokeAndHandle方法內部看看╮(╯_╰)╭:

public final void invokeAndHandle(NativeWebRequest request, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
         //請注意,這裏面就是執行handler方法的位置
        Object returnValue = invokeForRequest(request, mavContainer, providedArgs);

        //省略一些代碼              

        try {
            //這裏就是執行我們前面說的返回值解析器
            returnValueHandlers.handleReturnValue(returnValue, getReturnType(), mavContainer, request);
        } 
       //省略一些代碼
    }

 

  看看invokeForRequest方法你就能看到有趣的東西ヽ(”`▽´)ノ

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
                //這裏就是從request中拿到參數,利用方法參數解析器進行解析,映射到方法參數中,這個方法就在下面
                Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        
               //省略一些代碼
        
                //這裏就是根據上一步將請求參數映射到處理器方法參數中,然後執行對應的處理器方法
               Object returnValue = doInvoke(args);
        
               //省略一些代碼
        
                return returnValue;
    }


private Object[] getMethodArgumentValues(NativeWebRequest request,@Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
          //這裏獲取匹配到的handler方法的參數數組,每個參數在之前都被封裝成了一個MethodParameter對象,然後再遍歷這個數組,將其中每個MethodParameter和http請求提供的參數進行比較,
至於怎麼比較,就會用到之前說的參數解析器的那個support方法
MethodParameter[] parameters = getMethodParameters(); Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = resolveProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (this.argumentResolvers.supportsParameter(parameter)) { try { args[i] = this.argumentResolvers.resolveArgument( parameter, mavContainer, request, this.dataBinderFactory); continue; } //省略一些代碼 return args; }

 

     其實到這裏,有木有感覺清晰一點了,那麼肯定有小夥伴要問了,說了半天,你還是沒有說json是怎麼解析的啊?

  不要急,我們先把大概的流程過一遍之後,後面的都是小問題,那麼,我們的轉換器是在哪裡轉換的呢?

  欲知後事如何,請往後面看

 

5.無題

  在4中我們重點看兩個地方,第一個地方:this.argumentResolvers.supportsParameter(parameter);第二個地方:this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

  5.1.RequestResponseBodyMethodProcessor

    第一個地方,我們點進去supportsParameter方法,

   

    

  然後我們進入getArgumentResolver方法內部(´□`川):

@Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
             //for循環遍歷所有的方法參數解析器,
            for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
                
//省略一些代碼
//判斷哪一個解析器支持解析request中的參數,這裏很關鍵,因為眾多解析器中其中有一個參數解析器是RequestResponseBodyMethodProcessor,
          //下面我們看看這個解析器的supportParameter方法
if (methodArgumentResolver.supportsParameter(parameter)) { result = methodArgumentResolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }

  

  RequestResponseBodyMethodProcessor的supportsParameter方法,這裏想必能看得懂吧,就是看Controller中的處理器方法中的參數前面有沒有RequestBody註解,有註解,那麼這個RequestResponseBodyMethodProcessor解析器就會生效

  對了,補充一點,這個解析器RequestResponseBodyMethodProcessor可是同時實現了方法參數解析器接口、返回參數解析器接口的哦,這說明了處理返回值解析器用的也是這個解析器

 

  5.2.執行argumentResolvers.resolveArgument()方法

//這個方法就到了最關鍵的地方了,注意了注意了
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 

  throws Exception { //獲取一個轉換器,用於讀取前端傳過來的json數據 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());

//獲取handler方法形參中的所有註解,例如@Valid。@PathVariable等 Annotation[] annotations = parameter.getParameterAnnotations();

     for (Annotation annot : annotations) {       //判斷如果是以valid開頭的註解,其實就是@Valid註解或者是@Validated註解,那麼就會去校驗是否符合規則嘛,這個不用多說         if(annot.annotationType().getSimpleName().startsWith("Valid")) { String name = Conventions.getVariableNameForParameter(parameter); WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); Object hints = AnnotationUtils.getValue(annot); binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); BindingResult bindingResult = binder.getBindingResult(); if (bindingResult.hasErrors()) { throw new MethodArgumentNotValidException(parameter, bindingResult); } } } return arg; }

  最後我們只需要輕輕點開readWithMessageConverters方法,就能看到更有意思的東西~^o^~

 

6.xxxConverter

  我們進入readWithMessageConverters這個方法,

@SuppressWarnings("unchecked")
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class<T> paramType) throws IOException,
            HttpMediaTypeNotSupportedException {
            //這裏回答了本篇最開始說的MediaType 到底是什麼東西,從哪裡獲取的,很明顯是從請求頭中的ContentType獲取的
             MediaType contentType = inputMessage.getHeaders().getContentType();
            if (contentType == null) {
                 contentType = MediaType.APPLICATION_OCTET_STREAM;
            }
            //獲取所有的HttpMessageConverter,遍歷,看看哪一個轉換器支持前端傳過來的數據類型
            for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
                 if (messageConverter.canRead(paramType, contentType)) {
//調用對應的轉換器的read方法去把前端傳過來的json字符串轉為java對象
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage); } } //省略一些代碼 }

  

   到了這裏肯定有人會說,那到底用的是哪一個轉換器呢?難道是要我們自己導入?還是默認已經導入轉換器了呢?

  當然是默認就為你初始化了一些轉換器了啊,如果你想自定義也行,而且仔細看看下圖中跟json有關的只有MappingJackson2HttpMessageConverter這個轉換器了,可想而知這個轉換器(實際上是這個轉換器的父類方法中才有具體的操作)中使用的就是開源的Jackson來將json字符串轉為java對象的,有興趣了解的可以使用一下Jackson自己嘗試一下;

  偷偷告訴你(҂ ˘ _ ˘ ),springboot默認已經導入了Jackson包,如果你後期想用其他的轉換器,只需要導入相關依賴就ok了;

  

 

 

7.結束

  這篇博客到這裏就差不多了,寫了好久,邊寫邊查資料,可以說每次看源碼都能學到新的東西,當然,我也沒有死磕精神,不是不想,主要是沒有那個水平,哈哈;

  其實還要寫還能寫,不是還有個@ResponseBody原理還沒說嗎?其實跟@RequestBody大同小異的,有興趣的可以在第4點最後的代碼中返回值參數處理器方法,這裏就是入口

  話說還有個問題沒有解決,本來我想找一下最後的那幾個轉換器初始化時機的,然後實在是找不出來啊,查了一下資料,都說是在處理器適配器的構造器中初始化這些轉換器的,哎,jdk1.8,我在適配器中找了好久,愣是沒找到,打斷點也調試不出來,把自己坑了好久;

  有沒有大哥知道初始化時機的,評論一下,謝謝了(ㄒoㄒ)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

Java nio 空輪詢bug到底是什麼

編者注:Java nio 空輪詢bug也就是Java nio在Linux系統下的epoll空輪詢問題。

epoll機制是Linux下一種高效的IO復用方式,相較於select和poll機制來說。其高效的原因是將基於事件的fd放到內核中來完成,在內核中基於紅黑樹+鏈表數據結構來實現,鏈表存放有事件發生的fd集合,然後在調用epoll_wait時返回給應用程序,由應用程序來處理這些fd事件。

使用IO復用,Linux下一般默認就是epoll,Java NIO在Linux下默認也是epoll機制,但是JDK中epoll的實現卻是有漏洞的,其中最有名的java nio epoll bug就是即使是關注的select輪詢事件返回數量為0,NIO照樣不斷的從select本應該阻塞的Selector.select()/Selector.select(timeout)中wake up出來,導致CPU 100%問題。如下圖所示:

那麼產生這個問題的原因是什麼的?其實在 上已經說明的很清楚了,比如下面是bug復現的一個場景:

A DESCRIPTION OF THE PROBLEM :
The NIO selector wakes up infinitely in this situation..
0. server waits for connection
1. client connects and write message
2. server accepts and register OP_READ
3. server reads message and remove OP_READ from interest op set
4. client close the connection
5. server write message (without any reading.. surely OP_READ is not set)
6. server's select wakes up infinitely with return value 0

上面的場景描述的問題就是連接出現了RST,因為poll和epoll對於突然中斷的連接socket會對返回的eventSet事件集合置為POLLHUP或者POLLERR,eventSet事件集合發生了變化,這就導致Selector會被喚醒,進而導致CPU 100%問題。根本原因就是JDK沒有處理好這種情況,比如SelectionKey中就沒定義有異常事件的類型。

class SelectionKey {
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
}

既然nio epoll bug存在,那麼能不能規避呢?答案是有的,比如netty就很巧妙的規避了這個問題,它的處理機制就是如果發生了這種情況,並且發生次數超過了SELECTOR_AUTO_REBUILD_THRESHOLD(默認512),則調用rebuildSelector()進行Selecttor重建,這樣就不用管之前發生了異常情況的那個連接了。因為重建也是根據SelectionKey事件對應的連接來重新註冊的。

該問題最早在 Java 6 發現,隨後很多版本聲稱解決了該問題,但實際上只是降低了該 bug 的出現頻率,目前從網上搜索到的資料显示,Java 8 還是存在該問題()。

最後一起來分析下,nio epoll bug不是linux epoll的問題,而是JDK自己實現epoll時沒有考慮這種情況,或者說因為其他系統不存在這個問題,Java為了封裝(比如SelectionKey 中的4個事件類型)的統一而沒去處理?

這裏思考下,如果想要從java nio層面上來解決這個問題,該如何做呢?

一種是nio事件類型SelectionKey新加一種”錯誤”類型,比如針對linux epoll中的epollhup和epollerr,如果出現這種事件,建議程序直接close socket,但這種方式相對來說對於目前的nio SelectionKey改動有點大,因為SelectionKey的定義目前是針對所有jdk平台的;還有一種是針對jdk nio 對epoll的封裝中,對於epoll的epollhup和epollerr事件,epoll封裝內部直接處理,比如close socket,但是這種方案也有一點尷尬的是,可能上層應用代碼還保留有出現問題的socket引用,這時最好是應用程序能夠感知這種情況來處理比較好。

Java nio空轉問題由來已久,一般程序中是通過新建Selector的方式來屏蔽掉了JDK5/6的這個問題,因此,對於開發者來講,還是盡量將JDK的版本更新到最新,或者使用NIO框架如Netty,Grizzly等進行研發,以免出更多的問題。

推薦閱讀

歡迎小夥伴關注【TopCoder】閱讀更多精彩好文。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

Uber Go 語言編碼規範

Uber Go 語言編碼規範

是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 、 等。2018 年年末 Uber 將內部的 開源到 GitHub,經過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。

## 版本

  • 當前更新版本:2019-11-13 版本地址:
  • 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
  • Please feel free to fork and PR if you find any updates, issues or improvement.

目錄

介紹

樣式 (style) 是支配我們代碼的慣例。術語樣式有點用詞不當,因為這些約定涵蓋的範圍不限於由 gofmt 替我們處理的源文件格式。

本指南的目的是通過詳細描述在 Uber 編寫 Go 代碼的注意事項來管理這種複雜性。這些規則的存在是為了使代碼庫易於管理,同時仍然允許工程師更有效地使用 Go 語言功能。

該指南最初由 和 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其他人的反饋進行了修改。

本文檔記錄了我們在 Uber 遵循的 Go 代碼中的慣用約定。其中許多是 Go 的通用準則,而其他擴展準則依賴於下面外部的指南:

所有代碼都應該通過golintgo vet的檢查並無錯誤。我們建議您將編輯器設置為:

  • 保存時運行 goimports
  • 運行 golintgo vet 檢查錯誤

您可以在以下 Go 編輯器工具支持頁面中找到更為詳細的信息:

指導原則

指向 interface 的指針

您幾乎不需要指向接口類型的指針。您應該將接口作為值進行傳遞,在這樣的傳遞過程中,實質上傳遞的底層數據仍然可以是指針。

接口實質上在底層用兩個字段表示:

  1. 一個指向某些特定類型信息的指針。您可以將其視為”type”。
  2. 數據指針。如果存儲的數據是指針,則直接存儲。如果存儲的數據是一個值,則存儲指向該值的指針。

如果希望接口方法修改基礎數據,則必須使用指針傳遞。

接收器 (receiver) 與接口

使用值接收器的方法既可以通過值調用,也可以通過指針調用。

例如,

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// 你只能通過值調用 Read
sVals[1].Read()

// 這不能編譯通過:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// 通過指針既可以調用 Read,也可以調用 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")

同樣,即使該方法具有值接收器,也可以通過指針來滿足接口。

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

//  下面代碼無法通過編譯。因為 s2Val 是一個值,而 S2 的 f 方法中沒有使用值接收器
//   i = s2Val

中有一段關於 的精彩講解。

零值 Mutex 是有效的

零值 sync.Mutexsync.RWMutex 是有效的。所以指向 mutex 的指針基本是不必要的。

Bad Good
“`go mu := new(sync.Mutex) mu.Lock() “` “`go var mu sync.Mutex mu.Lock() “`

如果你使用結構體指針,mutex 可以非指針形式作為結構體的組成字段,或者更好的方式是直接嵌入到結構體中。
如果是私有結構體類型或是要實現 Mutex 接口的類型,我們可以使用嵌入 mutex 的方法:

“`go type smap struct { sync.Mutex // only for unexported types(僅適用於非導出類型) data map[string]string } func newSMap() *smap { return &smap{ data: make(map[string]string), } } func (m *smap) Get(k string) string { m.Lock() defer m.Unlock() return m.data[k] } “` “`go type SMap struct { mu sync.Mutex // 對於導出類型,請使用私有鎖 data map[string]string } func NewSMap() *SMap { return &SMap{ data: make(map[string]string), } } func (m *SMap) Get(k string) string { m.mu.Lock() defer m.mu.Unlock() return m.data[k] } “`
為私有類型或需要實現互斥接口的類型嵌入。 對於導出的類型,請使用專用字段。

在邊界處拷貝 Slices 和 Maps

slices 和 maps 包含了指向底層數據的指針,因此在需要複製它們時要特別注意。

接收 Slices 和 Maps

請記住,當 map 或 slice 作為函數參數傳入時,如果您存儲了對它們的引用,則用戶可以對其進行修改。

Bad Good
“`go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := … d1.SetTrips(trips) // 你是要修改 d1.trips 嗎? trips[0] = … “` “`go func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := … d1.SetTrips(trips) // 這裏我們修改 trips[0],但不會影響到 d1.trips trips[0] = … “`

返回 slices 或 maps

同樣,請注意用戶對暴露內部狀態的 map 或 slice 的修改。

Bad Good
“`go type Stats struct { mu sync.Mutex counters map[string]int } // Snapshot 返回當前狀態。 func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() return s.counters } // snapshot 不再受互斥鎖保護 // 因此對 snapshot 的任何訪問都將受到數據競爭的影響 // 影響 stats.counters snapshot := stats.Snapshot() “` “`go type Stats struct { mu sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } // snapshot 現在是一個拷貝 snapshot := stats.Snapshot() “`

使用 defer 釋放資源

使用 defer 釋放資源,諸如文件和鎖。

Bad Good
“`go p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 當有多個 return 分支時,很容易遺忘 unlock “` “`go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // 更可讀 “`

Defer 的開銷非常小,只有在您可以證明函數執行時間處於納秒級的程度時,才應避免這樣做。使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道。尤其適用於那些不僅僅是簡單內存訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer

Channel 的 size 要麼是 1,要麼是無緩衝的

channel 通常 size 應為 1 或是無緩衝的。默認情況下,channel 是無緩衝的,其 size 為零。任何其他尺寸都必須經過嚴格的審查。考慮如何確定大小,是什麼阻止了 channel 在負載下被填滿並阻止寫入,以及發生這種情況時發生了什麼。

Bad Good
“`go // 應該足以滿足任何情況! c := make(chan int, 64) “` “`go // 大小:1 c := make(chan int, 1) // 或者 // 無緩衝 channel,大小為 0 c := make(chan int) “`

枚舉從 1 開始

在 Go 中引入枚舉的標準方法是聲明一個自定義類型和一個使用了 iota 的 const 組。由於變量的默認值為 0,因此通常應以非零值開頭枚舉。

Bad Good
“`go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 “` “`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 “`

在某些情況下,使用零值是有意義的(枚舉從零開始),例如,當零值是理想的默認行為時。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

錯誤類型

Go 中有多種聲明錯誤(Error) 的選項:

  • 對於簡單靜態字符串的錯誤
  • 用於格式化的錯誤字符串
  • 實現 Error() 方法的自定義類型
  • 用 的 Wrapped errors

返回錯誤時,請考慮以下因素以確定最佳選擇:

  • 這是一個不需要額外信息的簡單錯誤嗎?如果是這樣, 足夠了。
  • 客戶需要檢測並處理此錯誤嗎?如果是這樣,則應使用自定義類型並實現該 Error() 方法。
  • 您是否正在傳播下游函數返回的錯誤?如果是這樣,請查看本文後面有關錯誤包裝 部分的內容。
  • 否則 就可以了。

如果客戶端需要檢測錯誤,並且您已使用創建了一個簡單的錯誤 ,請使用一個錯誤變量。

Bad Good
“`go // package foo func Open() error { return errors.New(“could not open”) } // package bar func use() { if err := foo.Open(); err != nil { if err.Error() == “could not open” { // handle } else { panic(“unknown error”) } } } “` “`go // package foo var ErrCouldNotOpen = errors.New(“could not open”) func Open() error { return ErrCouldNotOpen } // package bar if err := foo.Open(); err != nil { if err == foo.ErrCouldNotOpen { // handle } else { panic(“unknown error”) } } “`

如果您有可能需要客戶端檢測的錯誤,並且想向其中添加更多信息(例如,它不是靜態字符串),則應使用自定義類型。

Bad Good
“`go func open(file string) error { return fmt.Errorf(“file %q not found”, file) } func use() { if err := open(); err != nil { if strings.Contains(err.Error(), “not found”) { // handle } else { panic(“unknown error”) } } } “` “`go type errNotFound struct { file string } func (e errNotFound) Error() string { return fmt.Sprintf(“file %q not found”, e.file) } func open(file string) error { return errNotFound{file: file} } func use() { if err := open(); err != nil { if _, ok := err.(errNotFound); ok { // handle } else { panic(“unknown error”) } } } “`

直接導出自定義錯誤類型時要小心,因為它們已成為程序包公共 API 的一部分。最好公開匹配器功能以檢查錯誤。

// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

錯誤包裝 (Error Wrapping)

一個(函數/方法)調用失敗時,有三種主要的錯誤傳播方式:

  • 如果沒有要添加的其他上下文,並且您想要維護原始錯誤類型,則返回原始錯誤。
  • 添加上下文,使用 以便錯誤消息提供更多上下文 , 可用於提取原始錯誤。
    Use fmt.Errorf if the callers do not need to detect or handle that specific error case.

  • 如果調用者不需要檢測或處理的特定錯誤情況,使用 。

建議在可能的地方添加上下文,以使您獲得諸如“調用服務 foo:連接被拒絕”之類的更有用的錯誤,而不是諸如“連接被拒絕”之類的模糊錯誤。

在將上下文添加到返回的錯誤時,請避免使用“failed to”之類的短語來保持上下文簡潔,這些短語會陳述明顯的內容,並隨着錯誤在堆棧中的滲透而逐漸堆積:

Bad Good
“`go s, err := store.New() if err != nil { return fmt.Errorf( “failed to create new store: %s”, err) } “` “`go s, err := store.New() if err != nil { return fmt.Errorf( “new store: %s”, err) } “`
“` failed to x: failed to y: failed to create new store: the error “` “` x: y: new store: the error “`

但是,一旦將錯誤發送到另一個系統,就應該明確消息是錯誤消息(例如使用err標記,或在日誌中以”Failed”為前綴)。

另請參見 . 不要只是檢查錯誤,要優雅地處理錯誤

處理類型斷言失敗

的單個返回值形式針對不正確的類型將產生 panic。因此,請始終使用“comma ok”的慣用法。

Bad Good
“`go t := i.(string) “` “`go t, ok := i.(string) if !ok { // 優雅地處理錯誤 } “`

不要 panic

在生產環境中運行的代碼必須避免出現 panic。panic 是 級聯失敗的主要根源 。如果發生錯誤,該函數必須返回錯誤,並允許調用方決定如何處理它。

Bad Good
“`go func foo(bar string) { if len(bar) == 0 { panic(“bar must not be empty”) } // … } func main() { if len(os.Args) != 2 { fmt.Println(“USAGE: foo “) os.Exit(1) } foo(os.Args[1]) } “` “`go func foo(bar string) error { if len(bar) == 0 { return errors.New(“bar must not be empty”) } // … return nil } func main() { if len(os.Args) != 2 { fmt.Println(“USAGE: foo “) os.Exit(1) } if err := foo(os.Args[1]); err != nil { panic(err) } } “`

panic/recover 不是錯誤處理策略。僅當發生不可恢復的事情(例如:nil 引用)時,程序才必須 panic。程序初始化是一個例外:程序啟動時應使程序中止的不良情況可能會引起 panic。

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

即使在測試代碼中,也優先使用t.Fatal或者t.FailNow而不是 panic 來確保失敗被標記。

Bad Good
“`go // func TestFoo(t *testing.T) f, err := ioutil.TempFile(“”, “test”) if err != nil { panic(“failed to set up test”) } “` “`go // func TestFoo(t *testing.T) f, err := ioutil.TempFile(“”, “test”) if err != nil { t.Fatal(“failed to set up test”) } “`

使用 go.uber.org/atomic

使用 包的原子操作對原始類型 (int32, int64等)進行操作,因為很容易忘記使用原子操作來讀取或修改變量。

通過隱藏基礎類型為這些操作增加了類型安全性。此外,它包括一個方便的atomic.Bool類型。

Bad Good
“`go type foo struct { running int32 // atomic } func (f* foo) start() { if atomic.SwapInt32(&f.running, 1) == 1 { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running == 1 // race! } “` “`go type foo struct { running atomic.Bool } func (f *foo) start() { if f.running.Swap(true) { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running.Load() } “`

性能

性能方面的特定準則只適用於高頻場景。

優先使用 strconv 而不是 fmt

將原語轉換為字符串或從字符串轉換時,strconv速度比fmt快。

Bad Good
“`go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } “` “`go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } “`
“` BenchmarkFmtSprint-4 143 ns/op 2 allocs/op “` “` BenchmarkStrconv-4 64.2 ns/op 1 allocs/op “`

避免字符串到字節的轉換

不要反覆從固定字符串創建字節 slice。相反,請執行一次轉換並捕獲結果。

Bad Good
“`go for i := 0; i < b.N; i++ { w.Write([]byte(“Hello world”)) } “` “`go data := []byte(“Hello world”) for i := 0; i < b.N; i++ { w.Write(data) } “`
“` BenchmarkBad-4 50000000 22.2 ns/op “` “` BenchmarkGood-4 500000000 3.25 ns/op “`

盡量初始化時指定 Map 容量

在盡可能的情況下,在使用 make() 初始化的時候提供容量信息

make(map[T1]T2, hint)

make() 提供容量信息(hint)嘗試在初始化時調整 map 大小,
這減少了在將元素添加到 map 時增長和分配的開銷。
注意,map 不能保證分配 hint 個容量。因此,即使提供了容量,添加元素仍然可以進行分配。

Bad Good
“`go m := make(map[string]os.FileInfo) files, _ := ioutil.ReadDir(“./files”) for _, f := range files { m[f.Name()] = f } “` “`go files, _ := ioutil.ReadDir(“./files”) m := make(map[string]os.FileInfo, len(files)) for _, f := range files { m[f.Name()] = f } “`
`m` 是在沒有大小提示的情況下創建的; 在運行時可能會有更多分配。 `m` 是有大小提示創建的;在運行時可能會有更少的分配。

規範

一致性

本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;

但是最重要的是,保持一致.

一致性的代碼更容易維護、是更合理的、需要更少的學習成本、並且隨着新的約定出現或者出現錯誤后更容易遷移、更新、修復 bug

相反,一個單一的代碼庫會導致維護成本開銷、不確定性和認知偏差。所有這些都會直接導致速度降低、
代碼審查痛苦、而且增加 bug 數量

將這些標準應用於代碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程序通過將多個樣式引入到同一代碼中,違反了上述關注點。

相似的聲明放在一組

Go 語言支持將相似的聲明放在一個組內。

Bad Good
“`go import “a” import “b” “` “`go import ( “a” “b” ) “`

這同樣適用於常量、變量和類型聲明:

Bad Good
“`go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 “` “`go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) “`

僅將相關的聲明放在一組。不要將不相關的聲明放在一組。

Bad Good
“`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ENV_VAR = “MY_ENV” ) “` “`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const ENV_VAR = “MY_ENV” “`

分組使用的位置沒有限制,例如:你可以在函數內部使用它們:

Bad Good
“`go func f() string { var red = color.New(0xff0000) var green = color.New(0x00ff00) var blue = color.New(0x0000ff) … } “` “`go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) … } “`

import 分組

導入應該分為兩組:

  • 標準庫
  • 其他庫

默認情況下,這是 goimports 應用的分組。

Bad Good
“`go import ( “fmt” “os” “go.uber.org/atomic” “golang.org/x/sync/errgroup” ) “` “`go import ( “fmt” “os” “go.uber.org/atomic” “golang.org/x/sync/errgroup” ) “`

包名

當命名包時,請按下面規則選擇一個名稱:

  • 全部小寫。沒有大寫或下劃線。
  • 大多數使用命名導入的情況下,不需要重命名。
  • 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
  • 不用複數。例如net/url,而不是net/urls
  • 不要用“common”,“util”,“shared”或“lib”。這些是不好的,信息量不足的名稱。

另請參閱 和 .

函數名

我們遵循 Go 社區關於使用 的約定。有一個例外,為了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested.

導入別名

如果程序包名稱與導入路徑的最後一個元素不匹配,則必須使用導入別名。

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

在所有其他情況下,除非導入之間有直接衝突,否則應避免導入別名。

Bad Good
“`go import ( “fmt” “os” nettrace “golang.net/x/trace” ) “` “`go import ( “fmt” “os” “runtime/trace” nettrace “golang.net/x/trace” ) “`

函數分組與順序

  • 函數應按粗略的調用順序排序。
  • 同一文件中的函數應按接收者分組。

因此,導出的函數應先出現在文件中,放在struct, const, var定義的後面。

在定義類型之後,但在接收者的其餘方法之前,可能會出現一個 newXYZ()/NewXYZ()

由於函數是按接收者分組的,因此普通工具函數應在文件末尾出現。

Bad Good
“`go func (s *something) Cost() { return calcCost(s.weights) } type something struct{ … } func calcCost(n []int) int {…} func (s *something) Stop() {…} func newSomething() *something { return &something{} } “` “`go type something struct{ … } func newSomething() *something { return &something{} } func (s *something) Cost() { return calcCost(s.weights) } func (s *something) Stop() {…} func calcCost(n []int) int {…} “`

減少嵌套

代碼應通過盡可能先處理錯誤情況/特殊情況並儘早返回或繼續循環來減少嵌套。減少嵌套多個級別的代碼的代碼量。

Bad Good
“`go for _, v := range data { if v.F1 == 1 { v = process(v) if err := v.Call(); err == nil { v.Send() } else { return err } } else { log.Printf(“Invalid v: %v”, v) } } “` “`go for _, v := range data { if v.F1 != 1 { log.Printf(“Invalid v: %v”, v) continue } v = process(v) if err := v.Call(); err != nil { return err } v.Send() } “`

不必要的 else

如果在 if 的兩個分支中都設置了變量,則可以將其替換為單個 if。

Bad Good
“`go var a int if b { a = 100 } else { a = 10 } “` “`go a := 10 if b { a = 100 } “`

頂層變量聲明

在頂層,使用標準var關鍵字。請勿指定類型,除非它與表達式的類型不同。

Bad Good
“`go var _s string = F() func F() string { return “A” } “` “`go var _s = F() // 由於 F 已經明確了返回一個字符串類型,因此我們沒有必要顯式指定_s 的類型 // 還是那種類型 func F() string { return “A” } “`

如果表達式的類型與所需的類型不完全匹配,請指定類型。

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F 返回一個 myError 類型的實例,但是我們要 error 類型

對於未導出的頂層常量和變量,使用_作為前綴

在未導出的頂級varsconsts, 前面加上前綴_,以使它們在使用時明確表示它們是全局符號。

例外:未導出的錯誤值,應以err開頭。

基本依據:頂級變量和常量具有包範圍作用域。使用通用名稱可能很容易在其他文件中意外使用錯誤的值。

Bad Good
“`go // foo.go const ( defaultPort = 8080 defaultUser = “user” ) // bar.go func Bar() { defaultPort := 9090 … fmt.Println(“Default port”, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted. } “` “`go // foo.go const ( _defaultPort = 8080 _defaultUser = “user” ) “`

結構體中的嵌入

嵌入式類型(例如 mutex)應位於結構體內的字段列表的頂部,並且必須有一個空行將嵌入式字段與常規字段分隔開。

Bad Good
“`go type Client struct { version int http.Client } “` “`go type Client struct { http.Client version int } “`

使用字段名初始化結構體

初始化結構體時,幾乎始終應該指定字段名稱。現在由 強制執行。

Bad Good
“`go k := User{“John”, “Doe”, true} “` “`go k := User{ FirstName: “John”, LastName: “Doe”, Admin: true, } “`

例外:如果有 3 個或更少的字段,則可以在測試表中省略字段名稱。

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

本地變量聲明

如果將變量明確設置為某個值,則應使用短變量聲明形式 (:=)。

Bad Good
“`go var s = “foo” “` “`go s := “foo” “`

但是,在某些情況下,var 使用關鍵字時默認值會更清晰。例如,聲明空切片。

Bad Good
“`go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } “` “`go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } “`

nil 是一個有效的 slice

nil 是一個有效的長度為 0 的 slice,這意味着,

  • 您不應明確返回長度為零的切片。應該返回nil 來代替。

    Bad Good
    “`go if x == “” { return []int{} } “` “`go if x == “” { return nil } “`
  • 要檢查切片是否為空,請始終使用len(s) == 0。而非 nil

    Bad Good
    “`go func isEmpty(s []string) bool { return s == nil } “` “`go func isEmpty(s []string) bool { return len(s) == 0 } “`
  • 零值切片(用var聲明的切片)可立即使用,無需調用make()創建。

    Bad Good
    “`go nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } “` “`go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } “`

小變量作用域

如果有可能,盡量縮小變量作用範圍。除非它與 的規則衝突。

Bad Good
“`go err := ioutil.WriteFile(name, data, 0644) if err != nil { return err } “` “`go if err := ioutil.WriteFile(name, data, 0644); err != nil { return err } “`

如果需要在 if 之外使用函數調用的結果,則不應嘗試縮小範圍。

Bad Good
“`go if data, err := ioutil.ReadFile(name); err == nil { err = cfg.Decode(data) if err != nil { return err } fmt.Println(cfg) return nil } else { return err } “` “`go data, err := ioutil.ReadFile(name) if err != nil { return err } if err := cfg.Decode(data); err != nil { return err } fmt.Println(cfg) return nil “`

避免參數語義不明確(Avoid Naked Parameters)

函數調用中的意義不明確的參數可能會損害可讀性。當參數名稱的含義不明顯時,請為參數添加 C 樣式註釋 (/* ... */)

Bad Good
“`go // func printInfo(name string, isLocal, done bool) printInfo(“foo”, true, true) “` “`go // func printInfo(name string, isLocal, done bool) printInfo(“foo”, true /* isLocal */, true /* done */) “`

對於上面的示例代碼,還有一種更好的處理方式是將上面的 bool 類型換成自定義類型。將來,該參數可以支持不僅僅局限於兩個狀態(true/false)。

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady = iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

func printInfo(name string, region Region, status Status)

使用原始字符串字面值,避免轉義

Go 支持使用 ,也就是 ” ` ” 來表示原生字符串,在需要轉義的場景下,我們應該盡量使用這種方案來替換。

可以跨越多行並包含引號。使用這些字符串可以避免更難閱讀的手工轉義的字符串。

Bad Good
“`go wantError := “unknown name:\”test\”” “` “`go wantError := `unknown error:”test”` “`

初始化 Struct 引用

在初始化結構引用時,請使用&T{}代替new(T),以使其與結構體初始化一致。

Bad Good
“`go sval := T{Name: “foo”} // inconsistent sptr := new(T) sptr.Name = “bar” “` “`go sval := T{Name: “foo”} sptr := &T{Name: “bar”} “`

初始化 Maps

對於空 map 請使用 make(..) 初始化, 並且 map 是通過編程方式填充的。
這使得 map 初始化在表現上不同於聲明,並且它還可以方便地在 make 后添加大小提示。

Bad Good
“`go var ( // m1 讀寫安全; // m2 在寫入時會 panic m1 = map[T1]T2{} m2 map[T1]T2 ) “` “`go var ( // m1 讀寫安全; // m2 在寫入時會 panic m1 = make(map[T1]T2) m2 map[T1]T2 ) “`
聲明和初始化看起來非常相似的。 聲明和初始化看起來差別非常大。

在盡可能的情況下,請在初始化時提供 map 容量大小,詳細請看 。

另外,如果 map 包含固定的元素列表,則使用 map literals(map 初始化列表) 初始化映射。

Bad Good
“`go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 “` “`go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } “`

基本準則是:在初始化時使用 map 初始化列表 來添加一組固定的元素。否則使用 make (如果可以,請盡量指定 map 容量)。

字符串 string format

如果你為Printf-style 函數聲明格式字符串,請將格式化字符串放在外面,並將其設置為const常量。

這有助於go vet對格式字符串執行靜態分析。

Bad Good
“`go msg := “unexpected values %v, %v\n” fmt.Printf(msg, 1, 2) “` “`go const msg = “unexpected values %v, %v\n” fmt.Printf(msg, 1, 2) “`

命名 Printf 樣式的函數

聲明Printf-style 函數時,請確保go vet可以檢測到它並檢查格式字符串。

這意味着您應盡可能使用預定義的Printf-style 函數名稱。go vet將默認檢查這些。有關更多信息,請參見 。

如果不能使用預定義的名稱,請以 f 結束選擇的名稱:Wrapf,而不是Wrapgo vet可以要求檢查特定的 Printf 樣式名稱,但名稱必須以f結尾。

$ go vet -printfuncs=wrapf,statusf

另請參閱 .

編程模式

表驅動測試

當測試邏輯是重複的時候,通過 使用 table 驅動的方式編寫 case 代碼看上去會更簡潔。

Bad Good
“`go // func TestSplitHostPort(t *testing.T) host, port, err := net.SplitHostPort(“192.0.2.0:8000”) require.NoError(t, err) assert.Equal(t, “192.0.2.0”, host) assert.Equal(t, “8000”, port) host, port, err = net.SplitHostPort(“192.0.2.0:http”) require.NoError(t, err) assert.Equal(t, “192.0.2.0”, host) assert.Equal(t, “http”, port) host, port, err = net.SplitHostPort(“:8000”) require.NoError(t, err) assert.Equal(t, “”, host) assert.Equal(t, “8000”, port) host, port, err = net.SplitHostPort(“1:8”) require.NoError(t, err) assert.Equal(t, “1”, host) assert.Equal(t, “8”, port) “` “`go // func TestSplitHostPort(t *testing.T) tests := []struct{ give string wantHost string wantPort string }{ { give: “192.0.2.0:8000”, wantHost: “192.0.2.0”, wantPort: “8000”, }, { give: “192.0.2.0:http”, wantHost: “192.0.2.0”, wantPort: “http”, }, { give: “:8000”, wantHost: “”, wantPort: “8000”, }, { give: “1:8”, wantHost: “1”, wantPort: “8”, }, } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { host, port, err := net.SplitHostPort(tt.give) require.NoError(t, err) assert.Equal(t, tt.wantHost, host) assert.Equal(t, tt.wantPort, port) }) } “`

很明顯,使用 test table 的方式在代碼邏輯擴展的時候,比如新增 test case,都會顯得更加的清晰。

我們遵循這樣的約定:將結構體切片稱為tests。 每個測試用例稱為tt。此外,我們鼓勵使用givewant前綴說明每個測試用例的輸入和輸出值。

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

功能選項

功能選項是一種模式,您可以在其中聲明一個不透明 Option 類型,該類型在某些內部結構中記錄信息。您接受這些選項的可變編號,並根據內部結構上的選項記錄的全部信息採取行動。

將此模式用於您需要擴展的構造函數和其他公共 API 中的可選參數,尤其是在這些功能上已經具有三個或更多參數的情況下。

Bad Good
“`go // package db func Connect( addr string, timeout time.Duration, caching bool, ) (*Connection, error) { // … } // Timeout and caching must always be provided, // even if the user wants to use the default. db.Connect(addr, db.DefaultTimeout, db.DefaultCaching) db.Connect(addr, newTimeout, db.DefaultCaching) db.Connect(addr, db.DefaultTimeout, false /* caching */) db.Connect(addr, newTimeout, false /* caching */) “` “`go type options struct { timeout time.Duration caching bool } // Option overrides behavior of Connect. type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) { o.timeout = t }) } func WithCaching(cache bool) Option { return optionFunc(func(o *options) { o.caching = cache }) } // Connect creates a connection. func Connect( addr string, opts …Option, ) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(&options) } // … } // Options must be provided only if needed. db.Connect(addr) db.Connect(addr, db.WithTimeout(newTimeout)) db.Connect(addr, db.WithCaching(false)) db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout), ) “`

還可以參考下面資料:

本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯繫,必改之。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

golang 服務詭異499、504網絡故障排查

  • 事故經過
  • 排查
  • 總結

事故經過

11-01 12:00 中午午飯期間,手機突然收到業務網關非200異常報警,平時也會有一些少量499或者網絡抖動問題觸發報警,但是很快就會恢復(目前配置的報警閾值是5%,閾值跟當時的採樣窗口qps有直接關係)。

報警當時非200佔比已經過10%並且在持續升高,根據歷史規律應該很快就會恢復,我們稍微觀察了幾分鐘(一邊吃着很香的餃子一邊看着手機),但是過了幾分鐘故障沒有恢復而且佔比升高了突破50%,故障逐漸升級(故障如果不在固定時間內解決會逐漸升級,故障群每次升級都會逐層拉更高level的boss進來)手機持續報警震動已經發燙了,故障佔比已經快100%,影響面突然變大。

此時提現系統也開始報警,大量打款訂單擠壓(打款訂單擠壓突破一定閾值才會報警,所以不是實時),工位同事也反應支付系統也有少量連接錯誤,突然感覺情況複雜了,迅速停止吃飯,趕緊回公司排查。

回到工位時間差不多12:40左右,快速查看監控大盤,基本都是499、504錯誤,此類錯誤都是因為網絡超時導致。集群中的兩台機器均有錯,而且qps也比較平均,可以排除某台機器問題。

RT99線基本5s,而且連續橫盤,這5s是觸發了上游sidecar proxy調用超時主動斷開了,真正的RT時間可能更長。

故障還未見恢復,業務運維協助一起排查,此時故障群已經升級到技術中心老大,壓力瞬間大的一筆。

查看網關係統日誌,大量調用我們內部的兩個系統報出“下游服務器超時”錯誤,根據日誌信息可以判斷網絡問題導致超時,但是我們調用的是內網服務,如果是網絡問題為什麼只有我們的系統受到影響。

在12:51到13:02之間錯誤佔比情況有所好轉,但是之後錯誤佔比繼續升高。

此時業務運維同步其他部門有大量302報警,時間線有點吻合,此時時間差不多13:30。但是別的部門的系統和我們的系統沒有任何關係,太多的疑問大家開始集中坐到一起排查問題。

他們嘗試做了版本回滾未見好轉,然後嘗試將訪問返回302域名切到內網故障立馬恢復,此時正好14:00。根據他們的反饋在做實驗放量,導致在12:00的時候有一波流量高峰,但是這一波流量高峰對我的系統鏈路衝擊在哪裡,一臉懵逼,疑點重重。

本次故障持續時間太長,報警整整報了兩個小時,故障群每三種報警一次並且電話通知,報警電話幾十個,微信報警群“災難”級別的信息更多,嚴重程度可想而知。

排查

雖然故障是因為別的部門放量導致,但是還是有太多疑問沒有答案,下次還會再出現。作為技術人員,線上環境是非常神聖的地方是禁區,一定要找到每次故障的 root cause,否則沒辦法給自己一個交代,我們開始逐層剝洋蔥。

我們來梳理下疑問點:

1.302是什麼原因,為什麼做了域名切換就整體恢復了?
2.兩邊的系統在鏈路上有什麼交集?如果應用鏈路沒有交集,那麼在網絡鏈路上是否有交集?
3.我們業務網關中的“下游服務器超時”為什麼其他系統沒有影響?對日誌的解讀或者描述是否有歧義?
4.504是觸發sidecar proxy 超時斷開連接,網關服務設置的超時為什麼沒起作用?

1.302是什麼原因,為什麼做了域名切換就整體恢復了?

經過我們的運維和阿里雲專家的排查,出現大量302是因為訪問的域名觸發DDos/CC高防策略。由於訪問的域名配置了DDos/CC高防策略,大量請求觸發了其中一條規則導致拒絕請求(具體觸發了什麼規則就不方便透露),所以會返回302,通過添加白名單可以解決被誤殺的情況。
(從合理性角度講內部調用不應該走到外網,有一部分是歷史遺留問題。)

2.兩邊的系統在鏈路上有什麼交集?如果應用鏈路沒有交集,那麼在網絡鏈路上是否有交集?

所有人焦點都集中在高防上,認為網關故障就是因為也走到了被高防的地址上,但是我們的網關配置里根本沒有這個高防地址,而且我們內部系統是不會有外網地址的。

排查提現系統問題,提現系統的配置里確實有用到被高防的外網地址,認為提現打款擠壓也是因為走到了高防地址,但是這個高防地址只是一個旁路作用,不會影響打款流程。但是配置里確實有配置到,所以有理由判斷肯定使用到了才會影響,這在當時確實是個很重要的線索,是個突破口。

根據這個線索認為網關係統雖然本身沒有調用到高防地址,但是調用的下游也有可能會走到才會導致整個鏈路出現雪崩的問題。

通過大量排查下游服務,翻代碼、看日誌,基本上在應用層調用鏈路沒有找到任何線索。開始在網絡層面尋找線索,由於是內網調用所以路線是比較簡單的,client->slb->gateway->slb->sidecar proxy->ecs,幾個下游被調用系統請求一切正常,slb、sidecar proxy監控也一切正常,應用層、網絡層都沒有找到答案。

sidecar proxy 因為沒有打開日誌所以看不到請求(其實有一部分調用沒有直連還是通過slb、vtm中轉),從監控上看下游的 sidecar proxy 也一切正常,如果網路問題肯定是連鎖反應。

百般無解之後,開始仔細檢查當天出現故障的所有系統日誌(由於現在流行Microservice所以服務比較多,錯誤日誌量也比較大),在排查到支付系統的渠道服務時發現有一些線索,在事故發生期間有一些少量的 connection reset by peer,這個錯誤基本上多數出現在連接池化技術中使用了無效連接,或者下游服務器發生重啟導致。但是在事故當時並沒有發布。

通過對比前一周日誌沒有發生此類錯誤,那很有可能是很重要的線索,聯繫阿里雲開始幫忙排查當時ecs實例在鏈路上是否有問題,驚喜的是阿里雲反饋在事故當時出現 nat網關 限流丟包,一下子疑問全部解開了。

限流丟包才是引起我們系統大量錯誤的主要原因,所以整個故障原因是這樣的,由於做活動放量導致高防302和出網限流丟包,而我們系統受到影響都是因為需要走外網,提現打款需要用到支付寶、微信等支付渠道,而支付系統也是需要出外網用到支付寶、微信、銀聯等支付渠道。
(由於當時我們並沒有nat網關的報警導致我們都一致認為是高防攔截了流量。)

問題又來了,為什麼網關調用內部系統會出現問題,但是答案已經很明顯。簡單的檢查了下其中一個調用會走到外網,網關的接口會調用下游三個服務,其中第一個服務調用就是會出外網。

這個問題是找到了,但是為什麼下游設置的超時錯誤一個沒看見,而且“下游服務器超時”的錯誤日誌stack trace 堆棧信息是內網調用,這個還是沒搞明白。

3.我們業務網關中的“下游服務器超時”為什麼其他系統沒有影響?對日誌的解讀或者描述是否有歧義?

通過分析代碼,這個日誌的輸出並不是直接調用某個服務發生超時timeout,而是 go Context.Done() channel 的通知,我們來看下代碼:

func Send(ctx context.Context, serverName, method, path string, in, out interface{}) (err error) {
    e := make(chan error)
    go func() {
        opts := []utils.ClientOption{
            utils.WithTimeout(time.Second * 1),
        }
        if err = utils.HttpSend(method, path, in, out, ops, opts...); err != nil {
            e <- err
            return
        }
        e <- nil
    }()

    select {
    case err = <-e:
        return
    case <-ctx.Done():
        err = errors.ErrClientTimeOut
        return
    }
}

Send 的方法通過 goroutine 啟動一個調用,然後通過 select channel 感知http調用的結果,同時通過 ctx.Done() 感知本次上游http連接的 canceled

err = errors.ErrClientTimeOut
ErrClientTimeOut         = ErrType{64012, "下游服務器超時"}

這裏的 errors.ErrClientTimeOut 就是日誌“下游服務器超時”的錯誤對象。

很奇怪,為什麼調用下游服務器沒有超時錯誤,明明設置了timeout時間為1s。

        opts := []utils.ClientOption{
                    utils.WithTimeout(time.Second * 1),
                }
        if err = utils.HttpSend(method, path, in, out, ops, opts...); err != nil {
            e <- err
            return
        }

這個 utils.HttpSend 是有設置調用超時的,為什麼一條調用超時錯誤日誌沒有,跟蹤代碼發現雖然opts對象傳給了utils.HttpSend方法,但是裏面卻沒有設置到 __http.Client__對象上。

client := &http.Client{}
    // handle option
    {
        options := defaultClientOptions
        for _, o := range opts {
            o(&options)
        }
        for _, o := range ops {
            o(req)
        }
        
        //set timeout
        client.Timeout = options.timeout

    }

    // do request
    {
        if resp, err = client.Do(req); err != nil {
            err = err502(err)
            return
        }
        defer resp.Body.Close()
    }

就是缺少一行 client.Timeout = options.timeout 導致http調用未設置超時時間。加上之後調用一旦超時會拋出 “net/http: request canceled (Client.Timeout exceeded while awaiting headers)” timeout 錯誤。

問題我們大概知道了,就是因為我們沒有設置下游服務調用超時時間,導致上游連接超時關閉了,繼而觸發context.canceled事件。

上層調用會逐個同步進行。

    couponResp, err := client.Coupon.GetMyCouponList(ctx, r)
    // 不返回錯誤 降級為沒有優惠券
    if err != nil {
        logutil.Logger.Error("get account coupon  faield",zap.Any("err", err))
    }
    coins, err := client.Coin.GetAccountCoin(ctx, cReq.UserID)
    // 不返回錯誤 降級為沒有金幣
    if err != nil {
        logutil.Logger.Error("get account coin faield",zap.Any("err", err))
    }
    subCoins, err := client.Coin.GetSubAccountCoin(ctx, cReq.UserID)
    // 不返回錯誤 降級為沒有金幣
    if err != nil {
        logutil.Logger.Error("get sub account coin faield",zap.Any("err", err))
    }

client.Coupon.GetMyCouponList 獲取優惠券
client.Coin.GetAccountCoin 獲取金幣賬戶
client.Coin.GetSubAccountCoin 獲取金幣子賬戶

這三個方法內部都會調用Send方法,這個接口邏輯就是獲取用戶名下所有的現金抵扣權益,並且在超時時間內做好業務降級。但是這裏處理有一個問題,就是沒有識別Send方法返回的錯誤類型,其實連接斷了之後程序再往下走已經沒有意義也就失去了Context.canceld的意義。
(go和其他主流編程語言在線程(Thread)概念上有一個很大的區別,go是沒有線程概念的(底層還是通過線程在調度),都是goroutine。go也是完全隱藏routine的,你無法通過類似Thread Id 或者 Thread local線程本地存儲等技術,所有的routine都是通過context.Context對象來協作,比如在java 里要想取消一個線程必須依賴Thread.Interrupt中斷,同時要捕獲和傳遞中斷信號,在go里需要通過捕獲和傳遞Context信號。)

4.504是觸發sidecar proxy 超時斷開連接,網關服務器設置的超時為什麼沒起作用?

sidecar proxy 斷開連接有三個場景:

1.499同時會關閉下游連接
2.504超時直接關閉下游連接
3.空閑超過60s關閉下游連接

事故當時499、504 sidecar proxy 主動關閉連接,網關服務Context.Done()方法感知到連接取消拋出異常,上層方法輸出日誌“下游服務器超時”。那為什麼我們網關服務器本身的超時沒起作用。

http/server.Server對象有四個超時參數我們並沒有設置,而且這一類參數通常會被忽視,作為一個服務器本身對所有進來的請求是有最長服務要求,我們一般關注比較多的是下游超時會忽視服務本身的超時設置。

type Server struct {
    // ReadTimeout is the maximum duration for reading the entire
    // request, including the body.
    //
    // Because ReadTimeout does not let Handlers make per-request
    // decisions on each request body's acceptable deadline or
    // upload rate, most users will prefer to use
    // ReadHeaderTimeout. It is valid to use them both.
    ReadTimeout time.Duration

    // ReadHeaderTimeout is the amount of time allowed to read
    // request headers. The connection's read deadline is reset
    // after reading the headers and the Handler can decide what
    // is considered too slow for the body.
    ReadHeaderTimeout time.Duration

    // WriteTimeout is the maximum duration before timing out
    // writes of the response. It is reset whenever a new
    // request's header is read. Like ReadTimeout, it does not
    // let Handlers make decisions on a per-request basis.
    WriteTimeout time.Duration

    // IdleTimeout is the maximum amount of time to wait for the
    // next request when keep-alives are enabled. If IdleTimeout
    // is zero, the value of ReadTimeout is used. If both are
    // zero, ReadHeaderTimeout is used.
    IdleTimeout time.Duration
}

這些超時時間都會通過setDeadline計算成絕對時間點設置到netFD對象(Network file descriptor.)上。
由於沒有設置超時時間所以相當於所有的連接關閉都是通過sidecar proxy觸發傳遞下來的。

我們已經知道 sidecar proxy 關閉連接的1、2兩種原因,第3種情況出現在http長連接上,但是這類連接關閉是無感知的。

默認的tcpKeepAliveListener對象的keepAlive是3分鐘。

func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
    tc, err := ln.AcceptTCP()
    if err != nil {
        return nil, err
    }
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(3 * time.Minute)
    return tc, nil
}

我們服務host是使用endless框架,默認也是3分鐘,這其實是個約定90s,過小會影響上游代理。

func (el *endlessListener) Accept() (c net.Conn, err error) {
    tc, err := el.Listener.(*net.TCPListener).AcceptTCP()
    if err != nil {
        return
    }

    tc.SetKeepAlive(true)                  // see http.tcpKeepAliveListener
    tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener

    c = endlessConn{
        Conn:   tc,
        server: el.server,
    }

    el.server.wg.Add(1)
    return
}

sidecar proxy 的超時是60s,就算我們要設置keepAlive的超時時間也要大於60s,避免sidecar proxy使用了我們關閉的連接。
(但是這在網絡不穩定的情況下會有問題,如果發生HA Failover 然後在一定小概率的心跳窗口內,服務狀態並沒有傳遞到註冊中心,導致sidecar proxy重用了之前的http長連接。這其實也是個權衡,如果每次都檢查連接狀態一定會影響性能。)

這裡有個好奇問題,http是如何感知到四層tcp的狀態,如何將Context.cancel的事件傳遞上來的,我們來順便研究下。

type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // cancelCtx cancels the connection-level context.
    cancelCtx context.CancelFunc
}
func (c *conn) serve(ctx context.Context) {
    
    // HTTP/1.x from here on.
    
    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for {
        w, err := c.readRequest(ctx)

        if !w.conn.server.doKeepAlives() {
            // We're in shutdown mode. We might've replied
            // to the user without "Connection: close" and
            // they might think they can send another
            // request, but such is life with HTTP/1.1.
            return
        }

        if d := c.server.idleTimeout(); d != 0 {
            c.rwc.SetReadDeadline(time.Now().Add(d))
            if _, err := c.bufr.Peek(4); err != nil {
                return
            }
        }
        c.rwc.SetReadDeadline(time.Time{})
    }
}
// handleReadError is called whenever a Read from the client returns a
// non-nil error.
//
// The provided non-nil err is almost always io.EOF or a "use of
// closed network connection". In any case, the error is not
// particularly interesting, except perhaps for debugging during
// development. Any error means the connection is dead and we should
// down its context.
//
// It may be called from multiple goroutines.
func (cr *connReader) handleReadError(_ error) {
    cr.conn.cancelCtx()
    cr.closeNotify()
}
// checkConnErrorWriter writes to c.rwc and records any write errors to c.werr.
// It only contains one field (and a pointer field at that), so it
// fits in an interface value without an extra allocation.
type checkConnErrorWriter struct {
    c *conn
}

func (w checkConnErrorWriter) Write(p []byte) (n int, err error) {
    n, err = w.c.rwc.Write(p)
    if err != nil && w.c.werr == nil {
        w.c.werr = err
        w.c.cancelCtx()
    }
    return
}

其實tcp的狀態不是通過主動事件觸發告訴上層http的,而是每當http主動去發現。

read時使用connReader來感知tcp狀態,writer時使用checkConnErrorWriter對象來感知tcp狀態,然後通過server.conn對象中的cancelCtx來遞歸傳遞。

type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // cancelCtx cancels the connection-level context.
    cancelCtx context.CancelFunc
}

總結

此次故障排查了整整两天半,很多點是需要去反思和優化的。

1.所有的網絡調用沒有拋出最原始error信息。(經過加工之後的日誌會嚴重誤導人。)
2.超時時間的設置未能起到作用,未經過完整的壓測和故障演練,所以超時時間很容易無效。
3.內外網域名沒有隔離,需要區分內外網調用,做好環境隔離。
4.http服務器本身的超時沒有設置,如果程序內部出現問題導致處理超時,併發會把服務器拖垮。
5.對雲上的調用鏈路和網絡架構需要非常熟悉,這樣才能快速定位問題。

其實系統一旦上雲之後整個網絡架構變得複雜,干擾因素太多,排查也會面臨比較大的依賴,監控告警覆蓋面和基數也比較大很難察覺到個別業務線。(其實有些問題根本找不到答案。)
所有無法復現的故障是最難排查的,因為只能事後靠證據一環環解釋,涉及到網絡問題情況就更加複雜。

作者:王清培(趣頭條 Tech Leader)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

孟美岐私服穿搭都能引起女粉絲的瘋狂效仿

不得不說現在的小女孩真的太會穿衣搭配了,尤其是那些年紀輕輕的女明星,她們在穿衣搭配上的水平完全不輸給非常火熱的穿搭博主,比如說火箭101的孟美岐就是一個特別會穿衣搭配的人。

她每一次的私服穿搭都能引起女粉絲的瘋狂效仿,而這其中拋開追星的成分來說,孟美岐的私服搭配也確實非常適合普通人去穿着,也非常的好看,那麼在秋季孟美岐又為大家帶來了哪些值得參考的穿衣搭配呢?

先給大家看一套襯衣搭配,襯衣是非常多的女生都特別喜歡穿着的款式,尤其是在秋季,無論是單穿、內搭還是外搭都非常的方便,所以很多女生多多少少都會幾件襯衣,可是大部分的女生的襯衣都是普通的款式,沒有什麼新意,而孟美岐的這襯衣不僅顏色是非常亮眼的一種顏色,而且款式上也非常的有心意,無論是單穿還是外搭都非常的好看,非常的亮眼。

如果你覺得這件襯衣的顏色略有誇張,那麼可以選擇較為傳統也較為簡單的白色襯衣,那麼白色襯衣應該如何去搭配呢?那你穿着白色襯衣的時候,一定要選擇那種沒有任何多餘圖案和花紋的純白色襯衣,這才是最好看的白色襯衣。而且這種款式的白色襯衣也不需要多餘的裁剪修飾,僅僅有一部分修身的處理就可以了,當你穿着純白色襯衣的時候搭配一頂黑色貝雷帽,同樣非常的清新可愛。

那麼有沒有款式比較潮流的襯衣呢?當然也是有的。作為一個潮流達人,孟美岐在襯衣的選擇上也體現出了她的潮流選擇,就比如說這件襯衣,這是一件類似塗鴉風格的襯衣襯衣上的圖案非常的有街頭藝術氣息,非常的潮流。而為了將這件襯衣穿出街頭休閑風的感覺,她選擇了一件裁剪非常有特點牛仔褲,然後讓襯衣的下擺壓進牛仔褲當中,不但非常的時尚,而且非常的顯瘦。

說完了襯衣再來說一說T恤方面,在秋季如何穿着T恤進行搭配呢?當你穿着T恤時,T恤本身的顏色和款式其實並不重要,如何去搭配才非常的重要,比如說當你選擇一件黑色T恤時,下身就可以選擇一條款式非常怪異的拼接休閑褲,這樣穿着不但足夠亮眼,足夠潮流,而且整體的色彩感和層次感也非常的豐富。

同樣的穿搭方式,你也可以選擇另外的一種褲子,比如說,最近特別流行的格子花紋的休閑褲,幾乎每個女明星人手一條,孟美岐則非常的仔細,當她穿着這條是褲子的時候,不但選擇了將T恤壓進休閑褲的潮流穿法,更是為這條休閑搭配上了非常傳統的黑色皮帶,這條皮帶的風格和休閑褲的風格非常的統一,從這裏來看孟美岐對於穿搭是真的下了功夫的。

當然了,作為內搭的黑色T恤來說,你也可以在裁剪風格上進行一個改變,比如說你可以選擇露肚臍的款式。在秋季因為外套較為厚實,包裹的也比較嚴,所以很難顯現出少女的清新可愛感,這時你就可以選擇一件黑色露臍T恤,在重重的包裹之中露出一部分的肚臍和腰部的肌膚,看起來非常的清新可愛。

而在白色T恤方面,穿法就略有不同了。第一種穿搭方式就是整體使用休閑混搭的穿搭方式,內搭是一件純白色的T恤,外搭是整套的混搭工裝風,整體穿起來非常的有氣質,非常的酷,當你這樣穿着時,身體的線條非常的重要,所以需要將白色的T恤壓緊褲子當中,來營造整體的強烈線條感和纖細的腰部。

而第二種穿搭方式則充滿了女人味,那就是選擇超長款的T恤將T恤當做裙子來穿,這身搭配內搭是一件寬鬆版的T恤,外搭是一件粉色的女款西裝,因為會露出整條腿,所以看起來非常的成熟性感,這種穿搭方式是職場女白領非常喜歡的一種穿搭方式。

其實在秋季的日常搭配中,只要注意顏色不要有過大的衝突,基本上就可以了。你可以選擇略微保守的穿法,將自己包裹的很嚴,也可以選擇略微成熟的穿法露出一部分的肌膚,對於女生來說,在秋季的穿搭中需要你根據孟美岐的穿搭,再結合現有的身材條件與單品搭配出自己的效果才是最好的。

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

※你應該要知道的電子煙懶人包!

電子煙有爭議?真相解密

※全台最大電子煙交易平台?

秋冬必備減齡日常穿搭

又到了不知道穿什麼的秋冬,

看着大街上“短袖與棉襖齊飛,短裙共秋褲一色”,

我們不能怕冷裹成球!更不能穿少凍感冒!

不想天天穿沉悶的通勤裝了

怎麼穿才能減齡又日常,溫柔又可愛呢?

和小白一起來看看吧~

背心毛衣 可是秋冬必不可少的單品了,

內搭襯衫、連衣裙、長袖等等什麼都可以,

冷了也可以再加個外套,

靈活應對氣溫~

而且它自帶學院風,

讓你穿上就是滿滿的學生氣,

顯得青春又活潑!

說到學院風又不得不提牛角扣大衣

它絕對是秋冬必備的外套,

中長款的設計無論是搭配裙裝,還是褲裝都OK

甜美、休閑隨意切換,

減齡效果滿分!

休閑的可愛當然少不了衛衣!

衛衣這種東西男女老少都能穿,

如何穿出少女感呢?

秘訣就是下半身要搭配短裙,

是不是看起來非常青春可愛?

怕冷的話可以偷偷加上光腿神器和靴子哦

都說女生到了一定的年紀就會喜歡粉色,

這可不是沒有道理的。

日常雖然不適合死亡芭比熒光粉,

但臟粉淺粉等飽和度低一點的粉色

在冬天真的是很溫柔可愛呀!!

而且粉色超顯嫩

這個冬天就做個甜甜的美少女!

說完顏色,衣服的材質也很重要哦

在寒冷的冬天,

柔軟的材質、毛茸茸的絨毛不僅穿着舒服,

也能讓你看起來更溫柔、更軟萌!

左右的對比非常明顯了吧

我們也可以選擇一些柔軟或者暖色系的單品,

中和一下一身的色彩

本站聲明:網站內容來源於http://www.shelive.net/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

※你應該要知道的電子煙懶人包!

電子煙有爭議?真相解密

※全台最大電子煙交易平台?