jwt 實踐應用以及特殊案例思考

JSON Web Token 是 出的一份標準,使用 JSON 來傳遞數據,用於判定用戶是否登錄狀態。

jwt 之前,使用 session 來做用戶認證。

以下代碼均使用 javascript 編寫。

  • 原文鏈接:

session

傳統判斷是否登錄的方式是使用 session + token。

token 是指在客戶端使用 token 作為用戶狀態憑證,瀏覽器一般存儲在 localStorage 或者 cookie 中。

session 是指在服務器端使用 redis 或者 sql 類數據庫,存儲 user_id 以及 token 的鍵值對關係,基本工作原理如下。

在服務器端使用 sessions 存儲鍵值對

const sessions = {
  "ABCED1": 10086,
  "CDEFA0": 10010
}

每次客戶端請求帶權限數據時攜帶 token,在服務器端根據 token 與 sessions 獲取 user_id, 完成認證過程

function getUserIdByToken (token) {
  return sessions[token]
}

如果存儲在 cookie 中就是經常聽到的 session + cookie 的登錄方案。其實存儲在 cookie,localStorage 甚至 IndexedDB 或者 WebSQL 各有利弊,核心思想一致。

關於 cookie 以及 token 優缺點,在 中有討論。

如果不使用 cookie,可以採取 localStorage + Authorization 的方式進行認證,更加無狀態化

// http 的頭,每次請求權限接口時,需要攜帶 Authorization Header
const headers = {
  Authorization: `Bearer ${localStorage.get('token')}`
}

推薦一個前端的存儲庫 ,使用 IndexedDB,WebSQL 以及 IndexedDB 做鍵值對存儲。

無狀態登錄

session 需要在數據庫中保持用戶及token對應信息,所以叫 有狀態。

試想一下,如何在數據庫中不保持用戶狀態也可以登錄。

第一種方法: 前端直接傳 user_id 給服務端

缺點也特別特別明顯,容易被用戶篡改成任意 user_id,權限設置形同虛設。不過思路正確,接着往下走。

改進: 對 user_id 進行對稱加密

服務端對 user_id 進行對稱加密后,作為 token 返回客戶端,作為用戶狀態憑證。比上邊略微強點,但由於對稱加密,選擇合適的算法以及密鑰比較重要

改進: 對 user_id 不需要加密,只需要進行簽名,保證不被篡改

這便是 jwt 的思想:user_id,加密算法和簽名組成 token 一起存儲到客戶端,每當客戶端請求接口時攜帶 token,服務器根據 token 解析出加密算法與 user_id 來判斷簽名是否一致。

Json Web Token

jwt 根據 Header,Payload 以及 Signature 三個部分由 . 拼接而成。

Header

Header 由非對稱加密算法和類型組成,如下

const header = {
  // 加密算法
  alg: 'HS256',
  type: 'jwt'
}

Payload

Payload 中由 以及需要通信的數據組成。這些數據字段也叫 Claim。

Registered Claim 中比較重要的是 "exp" Claim 表示過期時間,在用戶登錄時會設置過期時間。

const payload = {
  // 表示 jwt 創建時間
  iat: 1532135735,

  // 表示 jwt 過期時間
  exp: 1532136735,

  // 用戶 id,用以通信
  user_id: 10086
}

Signature

Signature 由 Header,Payload 以及 secretOrPrivateKey 計算而成。secretOrPrivateKey 作為敏感數據存儲在服務器端,可以考慮使用 vault secret 或者 k8s secret

對於 secretOrPrivateKey,如果加密算法採用 HMAC,則為字符串,如果採用 RSA 或者 ECDSA,則為 PrivateKey。

// 由 HMACSHA256 算法進行簽名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)

// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign

從生成 jwt 規則可知客戶端可以解析出 payload,因此不要在 payload 中攜帶敏感數據,比如用戶密碼

校驗過程

在生成規則中可知,jwt 前兩部分是對 header 以及 payload 的 base64 編碼。

當服務器收到客戶端的 token 后,解析前兩部分得到 header 以及 payload,並使用 header 中的算法與 secretOrPrivateKey 進行簽名,判斷與 jwt 中攜帶的簽名是否一致。

帶個問題,如何判斷 token 過期?

應用

由上可知,jwt 並不對數據進行加密,而是對數據進行簽名,保證不被篡改。除了在登錄中可以用到,在進行郵箱校驗,圖形驗證碼和短信驗證碼時也可以用到。

圖形驗證碼

在登錄時,輸入密碼錯誤次數過多會出現圖形驗證碼。

圖形驗證碼的原理是給客戶端一個圖形,並且在服務器端保存與這個圖片配對的字符串,以前也大都通過 session 來實現。

可以把驗證碼配對的字符串作為 secret,進行無狀態校驗。

const jwt = require('jsonwebtoken')

// 假設驗證碼為字符驗證碼,字符為 ACDE,10分鐘失效
const token = jwt.sign({}, secrect + 'ACDE', { expiresIn: 60 * 10 })

const codeImage = getImageFromString('ACDE')

// 給前端的響應
const res = {
  // 驗證碼圖片的 token,從中可以校驗前端發送的驗證碼
  token,
  // 驗證碼圖片
  codeImage,
}

短信驗證碼與圖形驗證碼同理

郵箱校驗

現在網站在註冊成功後會進行郵箱校驗,具體做法是給郵箱發一個鏈接,用戶點開鏈接校驗成功。

// 把郵箱以及用戶id綁定在一起
const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })

// 在此鏈接校驗驗證碼
const link = `https://example.com/code=${code}`

無狀態 VS 有狀態

關於無狀態和有狀態,在其它技術方向也有對比,比如 React 的 stateLess component 以及 stateful component,函數式編程中的副作用可以理解為狀態,http 也是一個無狀態協議,需要靠 header 以及 cookie 攜帶狀態。

在用戶認證這裏,有無狀態是指是否依賴外部數據存儲,如 mysql,redis 等。

案例

思考以下幾個關於登錄的問題如何使用 session 以及 jwt 實現,來更加清楚 jwt 的使用場景

當用戶註銷時,如何使該 token 失效

因為 jwt 無狀態,不保存用戶設備信息,沒法單純使用它完成以上問題,可以再利用數據庫保存一些狀態完成。

  • session: 只需要把 user_id 對應的 token 清掉即可
  • jwt: 使用 redis,維護一張黑名單,用戶註銷時把該 token 加入黑名單,過期時間與 jwt 的過期時間保持一致。

如何允許用戶只能在一個設備登錄,如微信

  • session: 使用 sql 類數據庫,對用戶數據庫表添加 token 字段並加索引,每次登陸重置 token 字段,每次請求需要權限接口時,根據 token 查找 user_id
  • jwt: 假使使用 sql 類數據庫,對用戶數據庫表添加 token 字段(不需要添加索引),每次登陸重置 token 字段,每次請求需要權限接口時,根據 jwt 獲取 user_id,根據 user_id 查用戶表獲取 token 判斷 token 是否一致。另外也可以使用計數器的方法,如下一個問題。

對於這個需求,session 稍微簡單些,畢竟 jwt 也需要依賴數據庫。

如何允許用戶只能在最近五個設備登錄,如諸多播放器

  • session: 使用 sql 類數據庫,創建 token 數據庫表,有 id, token, user_id 三個字段,user 與 token 表為 1:m 關係。每次登錄添加一行記錄。根據 token 獲取 user_id,再根據 user_id 獲取該用戶有多少設備登錄,超過 5 個,則刪除最小 id 一行。
  • jwt: 使用計數器,使用 sql 類數據庫,在用戶表中添加字段 count,默認值為 0,每次登錄 count 字段自增1,每次登錄創建的 jwt 的 Payload 中攜帶數據 current_count 為用戶的 count 值。每次請求權限接口時,根據 jwt 獲取 count 以及 current_count,根據 user_id 查用戶表獲取 count,判斷與 current_count 差值是否小於 5

對於這個需求,jwt 略簡單些,而使用 session 還需要多維護一張 token 表。

如何允許用戶只能在最近五個設備登錄,而且使某一用戶踢掉除現有設備外的其它所有設備,如諸多播放器

  • session: 在上一個問題的基礎上,刪掉該設備以外其它所有的token記錄。
  • jwt: 在上一個問題的基礎上,對 count + 5,並對該設備重新賦值為新的 count。

如何显示該用戶登錄設備列表 / 如何踢掉特定用戶

  • session: 在 token 表中新加列 device
  • jwt: 需要服務器端保持設備列表信息,做法與 session 一樣,使用 jwt 意義不大

總結

從以上問題得知,如果不需要控制登錄設備數量以及設備信息,無狀態的 jwt 是一個不錯的選擇。一旦涉及到了設備信息,就需要對 jwt 添加額外的狀態支持,增加了認證的複雜度,此時選用 session 是一個不錯的選擇。

jwt 不是萬能的,是否採用 jwt,需要根據業務需求來確定。

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

【其他文章推薦】

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

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

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

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

※小三通物流營運型態?

※快速運回,大陸空運推薦?

挪威擬修法禁售汽油車

做為全球電動車市佔率最高的國家,挪威政府對於推行採用電動車不遺餘力。外媒報導,挪威打算進一步修法,目標是在2025年時達成全面禁售汽油車。

挪威目前有24%的汽車屬電動車,去年登記在案的電動車超過五萬輛,且首都奧斯陸是全球電動車密度最高的城市。據外媒《獨立報》、《Dagens Naeringsliv》等媒體報導,挪威朝野四大黨的國會議員已達成立法共識,計畫在2025年前全面禁售或者大量減產汽油車;若計畫順利,挪威將成為全球第一個全面改用電動車的國家。

但由於挪威高度依賴石油產業,此決議是否成行、若成行後會對國家產業帶來何種衝擊,仍在評估當中。

對於這項提案,特斯拉(Tesla)執行長Elon Musk在推特上表示:「這國家太讚了!」若挪威正式通過這項法案,將在國際間產生示範效果,再次推高電動車的接受度。

Just heard that Norway will ban new sales of fuel cars in 2025. What an amazingly awesome country. You guys rock!!

— Elon Musk (@elonmusk)

 

(首頁圖片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※台灣海運大陸貨務運送流程

※兩岸物流進出口一站式服務

中國財政部稱對新能源汽車騙補的現場核查已完成

財政部日前發佈聲明,表示對新能源汽車推廣騙補核查的第一部分工作,即現場核查,已經完成,目前仍處於對核查結果的會審階段,並稱對於核查和處理情況將按資訊公開有關規定及時公開。

業內人士原來普遍預期財政部能在4月或最遲5月公佈此項調查結果,但現在看來調查比預計的更耗時。

1月下旬,財政部、工信部等多個部委聯合發佈新能源汽車推廣應用核查工作的通知,在全國範圍對獲得中央財政補貼的新能源汽車及其生產企業和用戶展開核查。

在財政補貼的刺激下,2015年新能源汽車生產 34.05萬輛,銷售33.11萬輛,同比分別增長3.3倍和3.4倍。今年第一季度,增長有所放緩,但4月份又開始加速。

據中汽協統計,今年4月國內新能源汽車生產31266輛,銷售 31772輛,同比分別增長178.3%和190.6%。前4個月累計產銷量均超9萬輛,相比去年同期增長近130%。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※小三通物流營運型態?

※快速運回,大陸空運推薦?

車用顯示及光學技術研討會 – 2016 台北光電周展場直擊

在光電協會(PIDA)所舉辦的一系列車IOT與車用光電的研討會當中。針對了車用顯示以及光學技術領域。PIDA邀請了幾位業界專家與與會者分享最新的技術動態。   成立於2014年的及至微電機公司,在本次研討會由陳明舜處長為我們分享的主題是雷射掃描在車用電子系統的應用。及至微以MEMS Mirror為核心技術, 整合光學、機械、電子、軟體技術, 以雙軸的MEMS Scanner,搭配RGB 雷射光源,提供雷射投影模組汽車的HUD應用當中。   陳明舜處長分享了未來汽車顯示器的技術走向,包括Tesla等汽車都已經把儀表板的訊息整合到車用面板了,未來有機會用光學投影的方式來呈現出更多行車資訊。其中用雷射投影的方式,其原理是利用RGB雷射光束將畫面掃描出來,對比度高,且沒有對焦問題,可以投影在任何的曲面的物體上面。儘管雷射投影模組的價格昂貴,目前都還要USD 200元/組,其中雷射光源佔了主要的成本結構比重非常高,但隨著各家雷射光源廠商技術有所突破以及大量生產,價格有機會下降。

▲及至微的雷射投影系統   而怡利電子的陳儒賢經理則是分享了車用抬頭顯示器的技術發展趨勢。目前的HUD大致上可以分為下列三種:   Windshield HUD: 部分的BMW車種搭載了 Windshield HUD。透過HUD系統將車速,導航訊息利用投影的方式,將虛像投影至前擋玻璃前方2.5M的距離。其原理主要透過反射方式將光線投影至眼睛中,因此前擋玻璃也需要經過特殊處理。包括讓想要呈現的虛像能更折射到眼睛,該前擋玻璃又能夠看到前方的車況。因此該玻璃需有不同厚度與特殊角度,來消除掉重疊影像。因此該套系統的成本十分高昂,另外駕駛座前方也需要保留很大的空間來安裝這套HUD系統。   HUD Combiner: 由於前述提到的Windshield HUD成本過於高昂,因此各家車廠想盡辦法降低成本。而折衷的方式,就是另外做一個經過特殊處理的前擋玻璃,與投射系統整合成整套模組。但因為模組沒有辦法懸空,因此該玻璃的位置與眼睛無法呈現水平。光路較短,虛擬影像距離縮短,僅1~2M。   LED/TFT reflector: 這是較為常見的系統,且大量應用在後裝市場的系統。利用LED矩陣式投影或者是TFT面板反射影像在汽車的前擋玻璃的膜片上。因此眼睛看到的是實像。而LED矩陣能夠顯示的信息較為有限,最多僅能顯示數字與符號。至於TFT面板反射投影雖然能夠提供較多的影像信息,但是在戶外陽光充足的環境下,TFT面板的亮度需要達到一萬nits以上,影像才不會被陽光干擾。

 
▲怡利電子   合盈光電的許玄岳董事長則是介紹了全球車用攝影機的發展趨勢。在自動駕駛時代來臨下,每台汽車至少需要裝配6-7個攝影機。而這些攝影機的鏡頭如何在戶外的環境下,能夠不被雨水干擾影像,或是沙塵刮傷鏡頭都是需要處理的課題。合盈光電目前除了針對車用鏡頭的材料開發方面做了許多努力之外,也開發出結合倒車影像以及紅外線雷射距離偵測系統。許董事長提到,2018年全美所有車子都需要裝配倒車影像攝影機。若可以將倒車影像攝影機以及距離偵測功能整合在一起,就不需要再多一個倒車雷達了,以目前汽車每年的銷售數量約九千萬台,一台倒車雷達約US$100元,車廠一年可以省下90億美金。   許董事長強調,合盈光電在車用光電的發展方向會將電子,光學,材料,軟體,MEMS等各種技術做整合。而台灣的優勢在於電子產業供應鏈相當完整,方圓一百公里內都可以找到各種供應商。但對於發展汽車電子產業最大挑戰在於台灣的市場太小,沒有龐大的汽車品牌以及汽車工業支持。許多技術都還是要去接觸德國,日本等汽車原廠才有機會導入。加上電子業者對於車規不了解,將是發展汽車電子產業的最大障礙。

▲合盈光電 (本文內容由授權使用)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※台灣海運大陸貨務運送流程

※兩岸物流進出口一站式服務

Tesla電動車需求激增,Panasonic美國電池廠量產提前

特斯拉(Tesla)電動車需求火熱,日本電池製造商Panasonic為滿足大客戶需求,其位在美國那華達州的鋰電池工廠量產進度也配合提早約數個月。   按原先時程表,Panasonic美國工廠要等到特斯拉2016會計年度,也就是2017年初期才會投產。但據日經新聞周二報導,Panasonic工廠今年七月就將啟用,並在十一月開始生產電池芯。特斯拉首輛大眾化電動車Model 3自從發表後,訂單應接不暇,想必Panasonic加快量產時程應該與此有關。   除此之外,Panasonic擴廠腳步也將同時加速,據某Panasonic高層表示,原先16億美元、分八期的投資計畫可能會做修改,目的是避免特斯拉因電池供應不及而被迫中斷生產。   特斯拉目前正在中國尋找合適設廠地點,美國媒體報導指出,上海先馳得點,其國營金橋集團已與特斯拉簽訂不具約束力協定,使上海雀屏中選的機率大增。如果未來成案,特斯拉與金橋將各出資45億美元,其中金橋主要負責取得工廠用地。   (本文內容由授權使用;首圖來源: CC BY 2.0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※小三通物流營運型態?

※快速運回,大陸空運推薦?

Gogoro獲全球首張電動機車用電池UL 2271認證

全球安全科學檢驗實驗室Underwriters Laboratories (UL) 宣布台灣電動機車品牌Gogoro 所研發的智慧電池通過UL 2271測試認證。UL 2271是全球第一本針對輕型電動機車電池推出的安全標準,而Gogoro的車用鋰電池則獲得了全球首張認證書。

Gogoro除拔得頭籌取得UL 2271認證外,也是前全球唯一同時取得UL、IEC、CNS三項標準核可的廠商,將可讓Gogoro在站穩台灣電動機車市場龍頭之餘,進一步跨入歐洲、北美等國際市場。

UL 能源系統與電動交通產業全球工程總監Francisco Martinez 表示,台灣有成為全球電動機車發展重鎮的實力,而UL 2271則聚焦於電動機車之鋰電池安全性的檢測與審查,且格外重視特殊使用條件與環境氣候的測試,以考驗安全性問題。

根據定義,UL 2271 標準係針對輕型電動車電池產品,評估其在一般道路與越野特定道路環境下的使用情況。該標準不僅完整考量實際使用可能發生的危險,並會進行特殊的測試要求,例如震動、衝擊、擠壓、翻轉和水浸測試等,再加上軟硬體的功能性安全評估。

Martinez表示:「Gogoro 智慧電池取得UL 認證,證明Gogoro 製造的產品已達到世界級的安全水準,其將減少消費者對於電動機車電池安全的疑慮,更有助於Gogoro 邁向國際。」

Gogoro 執行長暨共同創辦人陸學森對此回應:「目前Gogoro 有超過8,000 位車主,每日平均創造近2,500 人次的電池交換,智慧電池在今日得到世界安全權威UL 認證,除了代表業界給予我們高標準的肯定外,也象徵Gogoro 對消費者安全承諾的實踐。」

除了頒發認證給Gogoro之外,UL期待未來在能源與電動機車服務方面,包括電動車、電動機車產品與電池、能源系統標準的開發,以及歐洲電動車、電動機車及儲能系統之標準研發方面與Gogoro 有更多深度探討與交流;Gogoro 於未來亦將尋求參與UL STP 標準技術小組的機會,提供產業觀點,以更廣闊的國際視野,共同守護消費者安全。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※台灣海運大陸貨務運送流程

※兩岸物流進出口一站式服務

中國強推動力電池技術,三元電池材料需求增

大陸國家動力電池創新中心昨日(6月30日)舉行成立大會,目的在於將研究資源進行整合,力求在動力電池核心技術取得突破;該創新中心將以國聯公司作為重要合作機構,聚合一汽、東風、北汽等諸多大型汽車集團。

陸媒報導指出,大陸動力電池主要為磷酸鐵鋰電池或三元電池,而三元電池比磷酸鐵鋰電池的能量密度更高、標準電壓更強、電芯占空間更少。同時作為電動車的心臟,動力電池的續航能力也是非常重要的一點,三元電池的續航里程和儲能強於傳統鋰電池,鋰電池每月衰減3%的電量,而三元電池反復使用後每月衰減僅1%-2%,近期已有多家大陸主流汽車廠商表示已生產和使用三元電池。

另從大陸工信部新能源汽車產銷數據來看,大陸電動汽車產業已進入快速增長階段,而作為電動車必備的動力電池需求量也已水漲船高;目前,三元材料較多應用在乘用車領域,作為三元電池的主要需求端,多款車型在政策支持力道不斷提升的同時,對三元材料的需求也在快速增長。

在此之前,大陸科技部發佈的《國家重點研發計畫新能源汽車重點專項實施方案》明確表示,轎車動力電池到2020年單體電池指標達到350Wh,系統達到250Wh以上。中泰證券研究報告預計,到2020年,車用三元材料將達20萬噸,年均增速將達到54%。大陸工信部預測,2018年三元電池的出貨量可望首度超過鋰電池,成為續航電池的主流材料。業內人士預計,三元電池產業可望迎接加速增長,進一步佔領動力電池的市場份額。

(本文由授權提供)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※小三通物流營運型態?

※快速運回,大陸空運推薦?

電動車需求看好,2016年美國銷量可達20萬輛

電動車市場越趨火熱,在許多國家甚至能經常撞見電動車在街頭奔走。根據美國市場研究機構Navigant Research 近日發布的研究指出,預計從2016 年起,美國電動車市場每年將成長62%,而今年銷量可望達到近20 萬台。

Navigant Research 指出,定價低於4 萬美元的長程純電動車(BEVs),舉例來說,定價3.7 萬美元、預計於今年秋季上市的雪佛蘭(Chevrolet)Bolt 車款,將是造成電動車市場成長的關鍵。此外,特斯拉(Tesla)的純電動車Model X,以及雪佛蘭Volt 、豐田(Toyota)的Prius Prime 與即將推出的三菱(Mitsubishi)Outlander 等插電式混合動力汽車(PHEV),都將引爆美國市場對電動車需求的熱潮。

隨著電動車續航力提高、定價逐漸下滑,加上各家車廠紛紛加入這塊兵家之地,使消費者購買電動車的選擇相對變多,對於消費者來說,現階段要購買一台好用的電動車變得容易許多。2015 年時,就已有9 種不同的電動車種類,從2 人座到標準運動休旅車(SUV)大小的種類都有,而當年度所售出的插電式電動車中,約6 成為如日產(Nissan)Leaf 與特斯拉Model S 等中型或大型車款。

Navigant Research 與美國能源部認為,插電式電動車市場真正開啟是在2011 年,當時僅有4 種車款可供選擇,而現在,從純電動小車Fiat 500e 到富豪(Volvo)XC90 插電式混合動力車,一共有29 種車款可供選擇。

2011 年至今,電動車銷量已從當年的2 萬台,增加到12 萬台,等於成長了6 倍,不過,若以全美的車輛銷售量來看,這個數字依然只占了美國車輛銷量不到1%,就且待Navigant Research 對於電動車2016 年的銷量預測是否真能達標了。

(本文授權自《》──〈〉。照片來源:shared by CC 2.0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

上海市半年內推出八項新能源汽車政策

2016上半年,中國與新能源汽車相關的中央及地方各類政策已經推出了80餘項,包括新能源汽車及充電基礎設施推廣規劃、補貼標準、充電費、充電服務費標準、專項資金補貼、及指標交通等配套優惠政策。雖然政策密集,但2016上半年新能源汽車產業的發展依然掣肘於政策的完善性。   2016年上半年,上海市共推出了8項新能源汽車相關政策,包括購買和使用新能源汽車暫行辦法、操作流程、補貼標準,充電基礎設施建設規劃、補貼標準,新能源汽車專項資金,支援新能源貨車推廣計畫,促進新能源汽車分時租賃業務發展指南,以及嘉定區補貼方案。這些政策在內容上覆蓋了從生產、銷售、購買、使用整個新能源汽車生產鏈,也對技術研發給予專項資金的鼓勵支援,同時覆蓋了新能源汽車的分時租賃業務。   相對比2015年上海市新能源汽車的補貼標準,2016年上海的補貼標準在乘用車、客車及專用車領域都有所消退,同時,上海市還首次提出了補貼「按量退坡(減少)」的概念,即超過一定量後補貼會繼續減少,這體現了上海由補貼向市場引導新能源汽車發展的策略。除了補貼標準的降低外,上海市對補貼的審批也更加嚴格,對廠商的銷售資格及消費者的購買資格都做出了嚴格要求。   除此之外,上海市的推廣政策還照顧到了分時租賃業務,通過支持運營車輛額度需求及相關補貼政策鼓勵新能源汽車分時租賃業務發展。  
文章來源:第一電動網(中國大陸)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

go中的關鍵字-defer

1. defer的使用

  defer 延遲調用。我們先來看一下,有defer關鍵字的代碼執行順序:

1 func main() {
2     defer func() {
3         fmt.Println("1號輸出")
4     }()
5     defer func() {
6         fmt.Println("2號輸出")
7     }()
8 }

  輸出結果:

1 2號出來
2 1號出來

  結論:多個defer的執行順序是倒序執行(同入棧先進后出)。

  由例子可以看出來,defer有延遲生效的作用,先使用defer的語句延遲到最後執行。

1.1 defer與返回值之間的順序

 1 func defertest() int
 2 
 3 func main() {
 4     fmt.Println("main:", defertest())
 5 }
 6 
 7 func defertest() int {
 8     var i int
 9     defer func() {
10         i++
11         fmt.Println("defer2的值:", i)
12     }()
13     defer func() {
14         i++
15         fmt.Println("defer1的值:", i)
16     }()
17     return i
18 }

  輸出結果:

1 defer1的值: 1
2 defer2的值: 2
3 main: 0

  結論:return最先執行->return負責將結果寫入返回值中->接着defer開始執行一些收尾工作->最後函數攜帶當前返回值退出

   return的時候已經先將返回值給定義下來了,就是0,由於i是在函數內部聲明所以即使在defer中進行了++操作,也不會影響return的時候做的決定。

 1 func test() (i int)
 2 
 3 func main() {
 4     fmt.Println("main:", test())
 5 }
 6 
 7 func test() (i int) {
 8     defer func() {
 9         i++
10         fmt.Println("defer2的值:", i)
11     }()
12     defer func() {
13         i++
14         fmt.Println("defer1的值:", i)
15     }()
16     return i
17 }

  詳解:由於返回值提前聲明了,所以在return的時候決定的返回值還是0,但是後面兩個defer執行後進行了兩次++,將i的值變為2,待defer執行完后,函數將i值進行了返回。

2. defer定義和執行

 1 func test(i *int) int {
 2     return *i
 3 }
 4 
 5 func main(){
 6     var i = 1
 7 
 8     // defer定義的時候test(&i)的值就已經定了,是1,後面就不會變了
 9     defer fmt.Println("i1 ="  , test(&i))
10     i++
11 
12     // defer定義的時候test(&i)的值就已經定了,是2,後面就不會變了
13     defer fmt.Println("i2 ="  , test(&i))
14 
15     // defer定義的時候,i就已經確定了是一個指針類型,地址上的值變了,這裏跟着變
16     defer func(i *int) {
17         fmt.Println("i3 ="  , *i)
18     }(&i)
19 
20     // defer定義的時候i的值就已經定了,是2,後面就不會變了
21     defer func(i int) {
22         //defer 在定義的時候就定了
23         fmt.Println("i4 ="  , i)
24     }(i)
25 
26     defer func() {
27         // 地址,所以後續跟着變
28         var c = &i
29         fmt.Println("i5 ="  , *c)
30     }()
31 
32     // 執行了 i=11 后才調用,此時i值已是11
33     defer func() {
34         fmt.Println("i6 ="  , i)
35     }()
36 
37     i = 11
38 }

  結論:會先將defer后函數的參數部分的值(或者地址)給先下來【你可以理解為()裡頭的會先確定】,後面函數執行完,才會執行defer后函數的{}中的邏輯。

例題分析

 1 //例子1
 2 func f() (result int) {
 3     defer func() {
 4         result++
 5     }()
 6     return 0
 7 }
 8 //例子2
 9 func f() (r int) {
10      t := 5
11      defer func() {
12        t = t + 5
13      }()
14      return t
15 }
16 //例子3
17 func f() (r int) {
18     defer func(r int) {
19           r = r + 5
20     }(r)
21     return 1
22 }

  例1的正確答案不是0,例2的正確答案不是10,例3的正確答案不是6……

  這裏先說一下返回值。defer是在return之前執行的。這條規則毋庸置疑,但最重要的一點是要明白,return xxx這一條語句並不是一條原子指令!

  函數返回的過程:先給返回值賦值,然後調用defer表達式,最後才是返回到調用函數中。defer表達式可能會在設置函數返回值之後,且在返回到調用函數之前去修改返回值,使最終的函數返回值與你想象的不一致。

  return xxx 可被改寫成:

1 返回值 = xxx
2 調用defer函數
3 空的return

  所以例子也可以改寫成:

 1 //例1
 2 func f() (result int) {
 3      result = 0  //return語句不是一條原子調用,return xxx其實是賦值+ret指令
 4      func() { //defer被插入到return之前執行,也就是賦返回值和ret指令之間
 5          result++
 6      }()
 7      return
 8 }
 9 //例2
10 func f() (r int) {
11      t := 5
12      r = t //賦值指令
13      func() {        //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過
14          t = t + 5
15      }
16      return        //空的return指令
17 }
18 例3
19 func f() (r int) {
20      r = 1  //給返回值賦值
21      func(r int) {        //這裏改的r是傳值傳進去的r,不會改變要返回的那個r值
22           r = r + 5
23      }(r)
24      return        //空的return
25 }

  所以例1的結果是1,例2的結果是5,例3的結果是1.

3. defer內部原理

  從例子開始看:

1 packmage main
2 
3 import()
4 
5 func main() {
6   defer println("這是一個測試")
7 }

  反編譯一下看看:

 1   src $ go build -o test test.go
 2   src $ go tool objdump -s "main\.main" test
 1 TEXT main.main(SB) /Users/tushanshan/go/src/test3.go
 2   test3.go:5        0x104ea70        65488b0c2530000000      MOVQ GS:0x30, CX
 3   test3.go:5        0x104ea79        483b6110                CMPQ 0x10(CX), SP
 4   test3.go:5        0x104ea7d        765f                    JBE 0x104eade
 5   test3.go:5        0x104ea7f        4883ec28                SUBQ $0x28, SP
 6   test3.go:5        0x104ea83        48896c2420              MOVQ BP, 0x20(SP)
 7   test3.go:5        0x104ea88        488d6c2420              LEAQ 0x20(SP), BP
 8   test3.go:6        0x104ea8d        c7042410000000          MOVL $0x10, 0(SP)
 9   test3.go:6        0x104ea94        488d05e5290200          LEAQ go.func.*+57(SB), AX
10   test3.go:6        0x104ea9b        4889442408              MOVQ AX, 0x8(SP)
11   test3.go:6        0x104eaa0        488d05e6e50100          LEAQ go.string.*+173(SB), AX
12   test3.go:6        0x104eaa7        4889442410              MOVQ AX, 0x10(SP)
13   test3.go:6        0x104eaac        48c744241804000000      MOVQ $0x4, 0x18(SP)
14   test3.go:6        0x104eab5        e8b631fdff              CALL runtime.deferproc(SB)
15   test3.go:6        0x104eaba        85c0                    TESTL AX, AX
16   test3.go:6        0x104eabc        7510                    JNE 0x104eace
17   test3.go:7        0x104eabe        90                      NOPL
18   test3.go:7        0x104eabf        e83c3afdff              CALL runtime.deferreturn(SB)
19   test3.go:7        0x104eac4        488b6c2420              MOVQ 0x20(SP), BP
20   test3.go:7        0x104eac9        4883c428                ADDQ $0x28, SP
21   test3.go:7        0x104eacd        c3                      RET
22   test3.go:6        0x104eace        90                      NOPL
23   test3.go:6        0x104eacf        e82c3afdff              CALL runtime.deferreturn(SB)
24   test3.go:6        0x104ead4        488b6c2420              MOVQ 0x20(SP), BP
25   test3.go:6        0x104ead9        4883c428                ADDQ $0x28, SP
26   test3.go:6        0x104eadd        c3                      RET
27   test3.go:5        0x104eade        e8cd84ffff              CALL runtime.morestack_noctxt(SB)
28   test3.go:5        0x104eae3        eb8b                    JMP main.main(SB)
29   :-1               0x104eae5        cc                      INT $0x3
30   :-1               0x104eae6        cc                      INT $0x3
31   :-1               0x104eae7        cc                      INT $0x3

   編譯器將defer處理成兩個函數調用 deferproc 定義一個延遲調用對象,然後在函數結束前通過 deferreturn 完成最終調用。在defer出現的地方,插入了指令call runtime.deferproc,然後在函數返回之前的地方,插入指令call runtime.deferreturn。

內部結構

 1 //defer
 2 type _defer struct {
 3    siz     int32   // 參數的大小
 4    started bool    // 是否執行過了
 5    sp      uintptr // sp at time of defer
 6    pc      uintptr
 7    fn      *funcval 
 8    _panic  *_panic // defer中的panic
 9    link    *_defer // defer鏈表,函數執行流程中的defer,會通過 link這個 屬性進行串聯
10 }
11 //panic
12 type _panic struct {
13    argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
14    arg       interface{}    // argument to panic
15    link      *_panic        // link to earlier panic
16    recovered bool           // whether this panic is over
17    aborted   bool           // the panic was aborted
18 }
19 //g
20 type g struct {
21    _panic         *_panic // panic組成的鏈表
22    _defer         *_defer // defer組成的先進后出的鏈表,同棧
23 }

  因為 defer panic 都是綁定在運行的g上的,這裏也說一下g中與 defer panic相關的屬性

  再把defer, panic, recover放一起看一下:

1 func main() {
2     defer func() {
3         recover()
4     }()
5     panic("error")
6 }

  反編譯結果:

1 go build -gcflags=all="-N -l" main.go
2 go tool objdump -s "main.main" main
1 go tool objdump -s "main\.main" main | grep CALL
2   main.go:4             0x4548d0                e81b00fdff              CALL runtime.deferproc(SB)              
3   main.go:7             0x4548f2                e8b90cfdff              CALL runtime.gopanic(SB)                
4   main.go:4             0x4548fa                e88108fdff              CALL runtime.deferreturn(SB)            
5   main.go:3             0x454909                e85282ffff              CALL runtime.morestack_noctxt(SB)       
6   main.go:5             0x4549a6                e8d511fdff              CALL runtime.gorecover(SB)              
7   main.go:4             0x4549b5                e8a681ffff              CALL runtime.morestack_noctxt(SB)

  defer 關鍵字首先會調用 runtime.deferproc 定義一個延遲調用對象,然後再函數結束前,調用 runtime.deferreturn 來完成 defer 定義的函數的調用

  panic 函數就會調用 runtime.gopanic 來實現相關的邏輯

  recover 則調用 runtime.gorecover 來實現 recover 的功能

deferproc

  根據 defer 關鍵字後面定義的函數 fn 以及 參數的size,來創建一個延遲執行的 函數,並將這個延遲函數,掛在到當前g的 _defer 的鏈表上,下面是deferproc的實現:

 1 func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
 2    sp := getcallersp()
 3    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
 4    callerpc := getcallerpc()
 5    // 獲取一個_defer對象, 並放入g._defer鏈表的頭部
 6    d := newdefer(siz)
 7      // 設置defer的fn pc sp等,後面調用
 8    d.fn = fn
 9    d.pc = callerpc
10    d.sp = sp
11    switch siz {
12    case 0:
13       // Do nothing.
14    case sys.PtrSize:
15       // _defer 後面的內存 存儲 argp的地址信息
16       *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
17    default:
18       // 如果不是指針類型的參數,把參數拷貝到 _defer 的後面的內存空間
19       memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
20    }
21    return0()
22 }

  通過newproc 獲取一個 _defer 的對象,並加入到當前g的 _defer 鏈表的頭部,然後再把參數或參數的指針拷貝到 獲取到的 _defer對象的後面的內存空間。

  再看看newdefer 的實現:

 1 func newdefer(siz int32) *_defer {
 2    var d *_defer
 3    // 根據 size 通過deferclass判斷應該分配的 sizeclass,就類似於 內存分配預先確定好幾個sizeclass,然後根據size確定sizeclass,找對應的緩存的內存塊
 4    sc := deferclass(uintptr(siz))
 5    gp := getg()
 6    // 如果sizeclass在既定的sizeclass範圍內,去g綁定的p上找
 7    if sc < uintptr(len(p{}.deferpool)) {
 8       pp := gp.m.p.ptr()
 9       if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
10          // 當前sizeclass的緩存數量==0,且不為nil,從sched上獲取一批緩存
11          systemstack(func() {
12             lock(&sched.deferlock)
13             for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
14                d := sched.deferpool[sc]
15                sched.deferpool[sc] = d.link
16                d.link = nil
17                pp.deferpool[sc] = append(pp.deferpool[sc], d)
18             }
19             unlock(&sched.deferlock)
20          })
21       }
22       // 如果從sched獲取之後,sizeclass對應的緩存不為空,分配
23       if n := len(pp.deferpool[sc]); n > 0 {
24          d = pp.deferpool[sc][n-1]
25          pp.deferpool[sc][n-1] = nil
26          pp.deferpool[sc] = pp.deferpool[sc][:n-1]
27       }
28    }
29    // p和sched都沒有找到 或者 沒有對應的sizeclass,直接分配
30    if d == nil {
31       // Allocate new defer+args.
32       systemstack(func() {
33          total := roundupsize(totaldefersize(uintptr(siz)))
34          d = (*_defer)(mallocgc(total, deferType, true))
35       })
36    }
37    d.siz = siz
38    // 插入到g._defer的鏈表頭
39    d.link = gp._defer
40    gp._defer = d
41    return d
42 }

  newdefer的作用是獲取一個_defer對象, 並推入 g._defer鏈表的頭部。根據size獲取sizeclass,對sizeclass進行分類緩存,這是內存分配時的思想,先去p上分配,然後批量從全局 sched上獲取到本地緩存,這種二級緩存的思想真的在go源碼的各個部分都有。

deferreturn

 1 func deferreturn(arg0 uintptr) {
 2    gp := getg()
 3    // 獲取g defer鏈表的第一個defer,也是最後一個聲明的defer
 4    d := gp._defer
 5    // 沒有defer,就不需要干什麼事了
 6    if d == nil {
 7       return
 8    }
 9    sp := getcallersp()
10    // 如果defer的sp與callersp不匹配,說明defer不對應,有可能是調用了其他棧幀的延遲函數
11    if d.sp != sp {
12       return
13    }
14    // 根據d.siz,把原先存儲的參數信息獲取並存儲到arg0裏面
15    switch d.siz {
16    case 0:
17       // Do nothing.
18    case sys.PtrSize:
19       *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
20    default:
21       memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
22    }
23    fn := d.fn
24    d.fn = nil
25    // defer用過了就釋放了,
26    gp._defer = d.link
27    freedefer(d)
28    // 跳轉到執行defer
29    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
30 }

freedefer

  釋放defer用到的函數,應該跟調度器、內存分配的思想是一樣的。

 1 func freedefer(d *_defer) {
 2    // 判斷defer的sizeclass
 3    sc := deferclass(uintptr(d.siz))
 4    // 超出既定的sizeclass範圍的話,就是直接分配的內存,那就不管了
 5    if sc >= uintptr(len(p{}.deferpool)) {
 6       return
 7    }
 8    pp := getg().m.p.ptr()
 9    // p本地sizeclass對應的緩衝區滿了,批量轉移一半到全局sched
10    if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
11       // 使用g0來轉移
12       systemstack(func() {
13          var first, last *_defer
14          for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
15             n := len(pp.deferpool[sc])
16             d := pp.deferpool[sc][n-1]
17             pp.deferpool[sc][n-1] = nil
18             pp.deferpool[sc] = pp.deferpool[sc][:n-1]
19             // 先將需要轉移的那批defer對象串成一個鏈表
20             if first == nil {
21                first = d
22             } else {
23                last.link = d
24             }
25             last = d
26          }
27          lock(&sched.deferlock)
28          // 把這個鏈表放到sched.deferpool對應sizeclass的鏈表頭
29          last.link = sched.deferpool[sc]
30          sched.deferpool[sc] = first
31          unlock(&sched.deferlock)
32       })
33    }
34    // 清空當前要釋放的defer的屬性
35    d.siz = 0
36    d.started = false
37    d.sp = 0
38    d.pc = 0
39    d.link = nil
40 
41    pp.deferpool[sc] = append(pp.deferpool[sc], d)
42 }

gopanic

 1 func gopanic(e interface{}) {
 2    gp := getg()
 3 
 4    var p _panic
 5    p.arg = e
 6    p.link = gp._panic
 7    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
 8 
 9    atomic.Xadd(&runningPanicDefers, 1)
10    // 依次執行 g._defer鏈表的defer對象
11    for {
12       d := gp._defer
13       if d == nil {
14          break
15       }
16 
17       // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
18       // take defer off list. The earlier panic or Goexit will not continue running.
19       // 正常情況下,defer執行完成之後都會被移除,既然這個defer沒有移除,原因只有兩種: 1. 這個defer裏面引發了panic 2. 這個defer裏面引發了 runtime.Goexit,但是這個defer已經執行過了,需要移除,如果引發這個defer沒有被移除是第一個原因,那麼這個panic也需要移除,因為這個panic也執行過了,這裏給panic增加標誌位,以待後續移除
20       if d.started {
21          if d._panic != nil {
22             d._panic.aborted = true
23          }
24          d._panic = nil
25          d.fn = nil
26          gp._defer = d.link
27          freedefer(d)
28          continue
29       }
30       d.started = true
31 
32       // Record the panic that is running the defer.
33       // If there is a new panic during the deferred call, that panic
34       // will find d in the list and will mark d._panic (this panic) aborted.
35       // 把當前的panic 綁定到這個defer上面,defer裏面有可能panic,這種情況下就會進入到 上面d.started 的邏輯裏面,然後把當前的panic終止掉,因為已經執行過了 
36       d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
37       // 執行defer.fn
38       p.argp = unsafe.Pointer(getargp(0))
39       reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
40       p.argp = nil
41 
42       // reflectcall did not panic. Remove d.
43       if gp._defer != d {
44          throw("bad defer entry in panic")
45       }
46       // 解決defer與panic的綁定關係,因為 defer函數已經執行完了,如果有panic或Goexit就不會執行到這裏了
47       d._panic = nil
48       d.fn = nil
49       gp._defer = d.link
50 
51       // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
52       //GC()
53 
54       pc := d.pc
55       sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
56       freedefer(d)
57       // panic被recover了,就不需要繼續panic了,繼續執行剩餘的代碼
58       if p.recovered {
59          atomic.Xadd(&runningPanicDefers, -1)
60 
61          gp._panic = p.link
62          // Aborted panics are marked but remain on the g.panic list.
63          // Remove them from the list.
64          // 從panic鏈表中移除aborted的panic,下面解釋
65          for gp._panic != nil && gp._panic.aborted {
66             gp._panic = gp._panic.link
67          }
68          if gp._panic == nil { // must be done with signal
69             gp.sig = 0
70          }
71          // Pass information about recovering frame to recovery.
72          gp.sigcode0 = uintptr(sp)
73          gp.sigcode1 = pc
74          // 調用recovery, 恢復當前g的調度執行
75          mcall(recovery)
76          throw("recovery failed") // mcall should not return
77       }
78    }
79      // 打印panic信息
80    preprintpanics(gp._panic)
81      // panic
82    fatalpanic(gp._panic) // should not return
83    *(*int)(nil) = 0      // not reached
84 }

  看下裏面gp._panic.aborted 的作用:

 1 func main() {
 2    defer func() { // defer1
 3       recover()
 4    }()
 5    panic1()
 6 }
 7 
 8 func panic1() {
 9    defer func() {  // defer2
10       panic("error1") // panic2
11    }()
12    panic("error")  // panic1
13 }

  執行順序詳解:

  • 當執行到 panic("error") 時

  g._defer鏈表: g._defer->defer2->defer1

  g._panic鏈表:g._panic->panic1 

  • 當執行到 panic("error1") 時 

  g._defer鏈表: g._defer->defer2->defer1

  g._panic鏈表:g._panic->panic2->panic1

  • 繼續執行到 defer1 函數內部,進行recover()
    此時會去恢復 panic2 引起的 panic, panic2.recovered = true,應該順着g._panic鏈表繼續處理下一個panic了,但是我們可以發現 panic1 已經執行過了,這也就是下面的代碼的邏輯了,去掉已經執行過的panic
1 for gp._panic != nil && gp._panic.aborted {
2    gp._panic = gp._panic.link
3 }

panic的邏輯:

  程序在遇到panic的時候,就不再繼續執行下去了,先把當前panic 掛載到 g._panic 鏈表上,開始遍歷當前g的g._defer鏈表,然後執行_defer對象定義的函數等,如果 defer函數在調用過程中又發生了 panic,則又執行到了 gopanic函數,最後,循環打印所有panic的信息,並退出當前g。然而,如果調用defer的過程中,遇到了recover,則繼續進行調度(mcall(recovery))。

recovery

 1 func recovery(gp *g) {
 2    // Info about defer passed in G struct.
 3    sp := gp.sigcode0
 4    pc := gp.sigcode1
 5    // Make the deferproc for this d return again,
 6    // this time returning 1.  The calling function will
 7    // jump to the standard return epilogue.
 8    // 記錄defer返回的sp pc
 9    gp.sched.sp = sp
10    gp.sched.pc = pc
11    gp.sched.lr = 0
12    gp.sched.ret = 1
13    // 重新恢復執行調度
14    gogo(&gp.sched)
15 }

gorecover

  gorecovery 僅僅只是設置了 g._panic.recovered 的標誌位

 1 func gorecover(argp uintptr) interface{} {
 2    gp := getg()
 3    p := gp._panic
 4    // 需要根據 argp的地址,判斷是否在defer函數中被調用
 5    if p != nil && !p.recovered && argp == uintptr(p.argp) {
 6       // 設置標誌位,上面gopanic中會對這個標誌位做判斷
 7       p.recovered = true
 8       return p.arg
 9    }
10    return nil
11 }

goexit

  當手動調用 runtime.Goexit() 退出的時候,defer函數也會執行:

 1 func Goexit() {
 2     // Run all deferred functions for the current goroutine.
 3     // This code is similar to gopanic, see that implementation
 4     // for detailed comments.
 5     gp := getg()
 6   // 遍歷defer鏈表
 7     for {
 8         d := gp._defer
 9         if d == nil {
10             break
11         }
12     // 如果 defer已經執行過了,與defer綁定的panic 終止掉
13         if d.started {
14             if d._panic != nil {
15                 d._panic.aborted = true
16                 d._panic = nil
17             }
18             d.fn = nil
19       // 從defer鏈表中移除
20             gp._defer = d.link
21       // 釋放defer
22             freedefer(d)
23             continue
24         }
25     // 調用defer內部函數
26         d.started = true
27         reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
28         if gp._defer != d {
29             throw("bad defer entry in Goexit")
30         }
31         d._panic = nil
32         d.fn = nil
33         gp._defer = d.link
34         freedefer(d)
35         // Note: we ignore recovers here because Goexit isn't a panic
36     }
37   // 調用goexit0,清除當前g的屬性,重新進入調度
38     goexit1()
39 }

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

【其他文章推薦】

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

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

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