特斯拉休旅車 Model X Signarue 9 月 29 日開始交車

美國電動車大廠特斯拉(Tesla)於 9 月 2 日宣布,第一部豪華電動跨界車款 Model X Signarue 系列訂 9 月 29 日開始交車。Model X 早在 2012 年初就已用概念車的形式發表,但交車時間一延再延。特斯拉發言人說,這款車的售價約在 13.2 萬到 14.4 萬美元之間。   特斯拉執行長穆斯克在推特上表示,第一批生產的車輛將在 29 日當天於加州 Fremont 廠交車。而較平價的 Model 3 則將於 2 年後開始生產。   限定版的 Signature 系列車,車身將有 Model X 所沒有的獨特紅,還有自動停車和升級的音效設備等。

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

【其他文章推薦】

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

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

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

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

007.Kubernetes二進制部署Flannel

一 部署flannel

1.1 安裝flannel


kubernetes 要求集群內各節點(包括 master 節點)能通過 Pod 網段互聯互通。flannel 使用 vxlan 技術為各節點創建一個可以互通的 Pod 網絡,使用的端口為 UDP 8472。




flanneld 第一次啟動時,從 etcd 獲取配置的 Pod 網段信息,為本節點分配一個未使用的地址段,然後創建 flannedl.1 網絡接口(也可能是其它名稱,如 flannel1 等)。




flannel 將分配給自己的 Pod 網段信息寫入 /run/flannel/docker 文件,docker 後續使用這個文件中的環境變量設置 docker0 網橋,從而從這個地址段為本節點的所有 Pod 容器分配 IP。

更多flannel參考:《008.Docker Flannel+Etcd分佈式網絡部署》。

  1 [root@k8smaster01 ~]# cd /opt/k8s/work/
  2 [root@k8smaster01 work]# mkdir flannel
  3 [root@k8smaster01 work]# wget https://github.com/coreos/flannel/releases/download/v0.11.0/flannel-v0.11.0-linux-amd64.tar.gz
  4 [root@k8smaster01 work]# tar -xzvf flannel-v0.11.0-linux-amd64.tar.gz -C flannel


1.2 分發flannel

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp flannel/{flanneld,mk-docker-opts.sh} root@${master_ip}:/opt/k8s/bin/
  7     ssh root@${master_ip} "chmod +x /opt/k8s/bin/*"
  8   done


1.3 創建flannel證書和密鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cat > flanneld-csr.json <<EOF
  3 {
  4     "CN": "flanneld",
  5     "hosts": [],
  6     "key": {
  7         "algo": "rsa",
  8         "size": 2048
  9     },
 10     "names": [
 11         {
 12             "C": "CN",
 13             "ST": "Shanghai",
 14             "L": "Shanghai",
 15             "O": "k8s",
 16             "OU": "System"
 17         }
 18     ]
 19 }
 20 EOF
 21 #創建flanneld的CA證書請求文件



解釋:

該證書只會被 kubectl 當做 client 證書使用,所以 hosts 字段為空。

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cfssl gencert -ca=/opt/k8s/work/ca.pem \
  3 -ca-key=/opt/k8s/work/ca-key.pem -config=/opt/k8s/work/ca-config.json \
  4 -profile=kubernetes flanneld-csr.json | cfssljson -bare flanneld	#生成CA密鑰(ca-key.pem)和證書(ca.pem)


1.4 分發證書和私鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     ssh root@${master_ip} "mkdir -p /etc/flanneld/cert"
  7     scp flanneld*.pem root@${master_ip}:/etc/flanneld/cert
  8   done


1.5 寫入集群 Pod 網段信息

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# etcdctl \
  4   --endpoints=${ETCD_ENDPOINTS} \
  5   --ca-file=/opt/k8s/work/ca.pem \
  6   --cert-file=/opt/k8s/work/flanneld.pem \
  7   --key-file=/opt/k8s/work/flanneld-key.pem \
  8   mk ${FLANNEL_ETCD_PREFIX}/config '{"Network":"'${CLUSTER_CIDR}'", "SubnetLen": 21, "Backend": {"Type": "vxlan"}}'



注意:注意:本步驟只需執行一次。

提示:flanneld 當前版本 (v0.11.0) 不支持 etcd v3,故使用 etcd v2 API 寫入配置 key 和網段數據;

寫入的 Pod 網段 ${CLUSTER_CIDR} 地址段(如 /16)必須小於 SubnetLen,必須與 kube-controller-manager 的 –cluster-cidr 參數值一致。

1.6 創建flanneld的systemd

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# cat > flanneld.service << EOF
  4 [Unit]
  5 Description=Flanneld overlay address etcd agent
  6 After=network.target
  7 After=network-online.target
  8 Wants=network-online.target
  9 After=etcd.service
 10 Before=docker.service
 11 
 12 [Service]
 13 Type=notify
 14 ExecStart=/opt/k8s/bin/flanneld \\
 15   -etcd-cafile=/etc/kubernetes/cert/ca.pem \\
 16   -etcd-certfile=/etc/flanneld/cert/flanneld.pem \\
 17   -etcd-keyfile=/etc/flanneld/cert/flanneld-key.pem \\
 18   -etcd-endpoints=${ETCD_ENDPOINTS} \\
 19   -etcd-prefix=${FLANNEL_ETCD_PREFIX} \\
 20   -iface=${IFACE} \\
 21   -ip-masq
 22 ExecStartPost=/opt/k8s/bin/mk-docker-opts.sh -k DOCKER_NETWORK_OPTIONS -d /run/flannel/docker
 23 Restart=always
 24 RestartSec=5
 25 StartLimitInterval=0
 26 
 27 [Install]
 28 WantedBy=multi-user.target
 29 RequiredBy=docker.service
 30 EOF



解釋:

mk-docker-opts.sh:該腳本將分配給 flanneld 的 Pod 子網段信息寫入 /run/flannel/docker 文件,後續 docker 啟動時使用這個文件中的環境變量配置 docker0 網橋;

flanneld:使用系統缺省路由所在的接口與其它節點通信,對於有多個網絡接口(如內網和公網)的節點,可以用 -iface 參數指定通信接口;

flanneld:運行時需要 root 權限;

-ip-masq: flanneld 為訪問 Pod 網絡外的流量設置 SNAT 規則,同時將傳遞給 Docker 的變量 –ip-masq(/run/flannel/docker 文件中)設置為 false,這樣 Docker 將不再創建 SNAT 規則; Docker 的 –ip-masq 為 true 時,創建的 SNAT 規則比較“暴力”:將所有本節點 Pod 發起的、訪問非 docker0 接口的請求做 SNAT,這樣訪問其他節點 Pod 的請求來源 IP 會被設置為 flannel.1 接口的 IP,導致目的 Pod 看不到真實的來源 Pod IP。 flanneld 創建的 SNAT 規則比較溫和,只對訪問非 Pod 網段的請求做 SNAT。

1.7 分發flannel systemd

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp flanneld.service root@${master_ip}:/etc/systemd/system/
  7   done


二 啟動並驗證

2.1 啟動flannel

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh root@${master_ip} "systemctl daemon-reload && systemctl enable flanneld && systemctl restart flanneld"
  6   done


2.2 檢查flannel啟動

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh root@${master_ip} "systemctl status flanneld|grep Active"
  6   done



2.3 檢查pod網段信息

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# etcdctl \
  3   --endpoints=${ETCD_ENDPOINTS} \
  4   --ca-file=/etc/kubernetes/cert/ca.pem \
  5   --cert-file=/etc/flanneld/cert/flanneld.pem \
  6   --key-file=/etc/flanneld/cert/flanneld-key.pem \
  7   get ${FLANNEL_ETCD_PREFIX}/config			#查看集群 Pod 網段(/16)



  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# etcdctl \
  3   --endpoints=${ETCD_ENDPOINTS} \
  4   --ca-file=/etc/kubernetes/cert/ca.pem \
  5   --cert-file=/etc/flanneld/cert/flanneld.pem \
  6   --key-file=/etc/flanneld/cert/flanneld-key.pem \
  7   ls ${FLANNEL_ETCD_PREFIX}/subnets			#查看已分配的 Pod 子網段列表(/24)
  8 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  9 [root@k8smaster01 ~]# etcdctl \
 10   --endpoints=${ETCD_ENDPOINTS} \
 11   --ca-file=/etc/kubernetes/cert/ca.pem \
 12   --cert-file=/etc/flanneld/cert/flanneld.pem \
 13   --key-file=/etc/flanneld/cert/flanneld-key.pem \
 14   get ${FLANNEL_ETCD_PREFIX}/subnets/172.30.32.0-21	#查看某一 Pod 網段對應的節點 IP 和 flannel 接口地址




解釋:

172.30.32.0/21 被分配給節點 k8smaster01 (172.24.8.71);

VtepMAC 為 k8smaster01 節點的 flannel.1 網卡 MAC 地址。

2.4 檢查flannel網絡信息

  1 [root@k8smaster01 ~]# ip addr show



解釋:flannel.1 網卡的地址為分配的 Pod 子網段的第一個 IP(.0),且是 /32 的地址。

  1 [root@k8smaster01 ~]# ip route show |grep flannel.1
  2 172.30.128.0/21 via 172.30.128.0 dev flannel.1 onlink
  3 172.30.208.0/21 via 172.30.208.0 dev flannel.1 onlink



解釋:

到其它節點 Pod 網段請求都被轉發到 flannel.1 網卡;

flanneld 根據 etcd 中子網段的信息,如 ${FLANNEL_ETCD_PREFIX}/subnets/172.30.32.0-21 ,來決定進請求發送給哪個節點的互聯 IP。

2.5 驗證各節點flannel


在各節點上部署 flannel 后,檢查是否創建了 flannel 接口(名稱可能為 flannel0、flannel.0、flannel.1 等):

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh ${master_ip} "/usr/sbin/ip addr show flannel.1|grep -w inet"
  6   done



輸出:

  1 >>> 172.24.8.71
  2     inet 172.30.32.0/32 scope global flannel.1
  3 >>> 172.24.8.72
  4     inet 172.30.128.0/32 scope global flannel.1
  5 >>> 172.24.8.73
  6     inet 172.30.208.0/32 scope global flannel.1



在各節點上 ping 所有 flannel 接口 IP,確保能通:

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh ${master_ip} "ping -c 1 172.30.32.0"
  6     ssh ${master_ip} "ping -c 1 172.30.128.0"
  7     ssh ${master_ip} "ping -c 1 172.30.208.0"
  8   done


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

【其他文章推薦】

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

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

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

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

transformer模型簡介

Transformer模型由《Attention is All You Need》提出,有一個完整的Encoder-Decoder框架,其主要由attention(注意力)機制構成。論文地址:。

其整體結構如圖所示:

 

模型分為編碼器(Encoder)和解碼器(Decoder)兩部分,包含內部結構的總體結構如下圖所示:

 

                                                      圖二

在論文中編碼器部分由6個相同編碼器疊在一起,解碼器部分也是由6個相同解碼器疊在一起,編碼器之間不共享參數。(這裏不一定要是6個)

在將詞向量表示送入編碼器、解碼器之前,先做positional encoding,下面依次對positional encoding、encoding、decoding進行介紹:

1、positional encoding

 

 

如圖所示,由於attention機制不包含位置信息,因此句子首先進行embedding得到詞向量表示,同時為了增加位置信息,根據句子中詞的位置信息給詞嵌入添加位置編碼向量,論文中添加位置編碼的方法是:構造一個跟輸入embedding維度一樣的矩陣,然後跟輸入embedding相加得到multi-head attention 的輸入。

作者希望引入絕對位置的編碼公式,讓模型能夠學習到相對位置信息,作者使用的positional encoding生成固定位置表示如下:

已知三角函數公式如下:

 

 

 作者希望通過絕對位置的編碼公式,讓模型可以學習到相對位置信息。雖然如此獲得的 position embeddings,兩者之間的點積能夠反應相對距離,但它缺乏方向性,並且這種特性(相對距離)會被原始 Transformer 的注意力機制破壞。

基於公式 (1),位置t的位置嵌入可以表示為:

 

 

 

 

 

 

2、encoding

如圖二左邊結構所示,編碼器主要由前饋神經網絡層與多頭自注意力層構成,值得注意的是,在每個編碼器中的每個子層(自注意力、前饋網絡)的周圍都有一個殘差連接,並且都跟隨着一個“層-歸一化”步驟。這裏先介紹attention機制,還是舉個栗子:

假設我們想要翻譯這個句子:

“The animal didn’t cross the street because it was too tired”

那麼it在這句話中是是指animal還是street,人類好理解這句話,但是對機器來說就很困難了。當模型處理這個單詞“it”的時候,自注意力機制會允許“it”與“animal”建立聯繫。隨着模型處理輸入序列的每個單詞,自注意力會關注整個輸入序列的所有單詞,幫助模型對本單詞更好地進行編碼。如下圖。

 

當我們在編碼器#5(棧中最上層編碼器)中編碼“it”這個單詞的時,注意力機制的部分會去關注“The Animal”,將它的表示的一部分編入“it”的編碼中。

接下來介紹attention實現的思想。

計算自注意力的第一步就是從每個編碼器的輸入向量(每個單詞的詞向量)中生成三個向量。也就是說對於每個單詞,我們創造一個查詢向量、一個鍵向量和一個值向量。這三個向量是通過詞嵌入與三個權重矩陣后相乘創建的。在論文中這三個向量的維度比詞嵌入向量要低,實際中維度更低不是必須的,只是架構上的選擇,可以使多頭注意力的大部分計算保持不變。

計算自注意力的第二步是計算得分。假設我們需要對第一個詞’Thinking’計算自注意力向量那麼需要拿輸入句子中的每個單詞對“Thinking”打分。這些分數決定了在編碼單詞“Thinking”的過程中有多重視句子的其它部分。

這些分數是通過打分單詞(所有輸入句子的單詞)的鍵向量與“Thinking”的查詢向量相點積來計算的。所以如果我們是處理位置最靠前的詞的自注意力的話,第一個分數是q1和k1的點積,第二個分數是q1和k2的點積。

 

第三步和第四步是將分數除以8(8是論文中使用的鍵向量的維數64的平方根,這會讓梯度更穩定。這裏也可以使用其它值,8隻是默認值),然後通過softmax傳遞結果。softmax的作用是使所有單詞的分數歸一化,得到的分數都是正值且和為1。

 

這個softmax分數決定了每個單詞對編碼當下位置(“Thinking”)的貢獻。顯然,已經在這個位置上的單詞將獲得最高的softmax分數,但有時關注另一個與當前單詞相關的單詞也會有幫助。

第五步是將每個值向量乘以softmax分數(這是為了準備之後將它們求和)。這裏的直覺是希望關注語義上相關的單詞,並弱化不相關的單詞。

第六步是對加權值向量求和,然後即得到自注意力層在該位置的輸出。

 

這樣自注意力的計算就完成了。得到的向量就可以傳給前饋神經網絡。

在現實中自注意力機制是通過矩陣來實現的,與上面思路一樣:

第一步是計算查詢矩陣、鍵矩陣和值矩陣,如下圖所示:

 

將前面的計算步驟可以合併成:

 

介紹完自注意力機制后,介紹在論文中使用的多頭自注意力機制“multi-headed” attention。

 

每個頭都是獨立的查詢/鍵/值權重矩陣,從而產生不同的查詢/鍵/值矩陣。在論文中採用的是8頭,那麼經過8次不同權重矩陣運算,我們會得到8個不同的Z矩陣。

 

然後我們將這8個矩陣壓縮成一個矩陣,實現原理是將這8個矩陣拼接在一起,然後再用一個權重矩陣與之相乘,得到一個融合所有注意力頭信息的矩陣Z,再將其求和與歸一化後傳給前饋層。

 

Decoding(解碼器):

解碼器內部組件與編碼器大同小異,需要注意的是,解碼器的第一個注意力層被稱作MaskedMulti-Head Attention,通過加入了MASK操作,使得我們只被允許處理輸出序列中更靠前的那些位置,即我們只能attend到前面已經處理過的語句。第二個注意力層被稱作encoder-decoder attention layer,由圖二可知,它的query來自前一級的decoder層的輸出,key、value來自encoder的輸出,encoder的輸出可以幫助解碼器關注輸入序列哪些位置合適。接下來送入前饋層,然後重複這些步驟,直到到達一個特殊的終止符號,它表示transformer的解碼器已經完成了它的輸出。每個步驟的輸出在下一個時間步被提供給底端解碼器,並且就像編碼器之前做的那樣,這些解碼器會輸出它們的解碼結果 。另外,就像我們對編碼器的輸入所做的那樣,我們會嵌入並添加位置編碼給那些解碼器,來表示每個單詞的位置。

 

在解碼完成後會輸出一個實數向量,經過一個簡單的全連接神經網絡(線性變換層)映射到一個被稱作對數幾率(logits)的向量里,假設從訓練集中學習一萬個單詞,那麼對數幾率向量為一萬個單元格長度的向量——每個單元格對應某一個單詞的分數。接下來的Softmax 層便會把那些分數變成概率(都為正數、上限1.0)。概率最高的單元格被選中,並且它對應的單詞被作為這個時間步的輸出。

 

 

 參考:

   

           

         

 

 

(完)

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

【其他文章推薦】

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

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

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

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

中國發佈《電動汽車充電基礎設施發展指南(2015-2020年)》

  各省、自治區、直轄市、新疆生產建設兵團發展改革委(能源局)、工業和資訊化主管部門、住房城鄉建設廳(委、局),國家電網公司、南方電網公司:   為落實《國務院辦公廳關於加快新能源汽車推廣應用的指導意見》(國辦發〔2014〕35號),科學引導電動汽車充電基礎設施建設,促進電動汽車產業健康快速發展,我們組織編制了《電動汽車充電基礎設施發展指南(2015-2020年)》,現予印發,請認真貫徹執行。  

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

【其他文章推薦】

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

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

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

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

實現中國新能源汽車大發展,還需解決4大難題

一系列政策推出後,新能源汽車進入了快速發展階段,據數據統計,前10個月,我國新能源汽車產銷分別增長了2.7倍和2.9倍,而且目前私人購買新能源汽車增長快速。然而,制約新能源汽車發展的因素逐漸顯現。  
1.充電基礎設施建設滯後   Interbrand公司的孟祥峰博士表示,到2014年底全國的示範城市推廣的新能源汽車數量是9.1萬輛,但是充電樁只有3.1萬個,充電樁和新能源車的比例明顯不足。   11月18日,國家發展改革委等四部門聯合發佈《電動汽車充電基礎設施發展指南(2015~2020年)》表示,到2020年,新增集中式充換電站超過1.2萬座,分散式充電樁超過480萬個,以滿足全國500萬輛電動汽車充電需求。  
2.動力電池產業佈局系統性不足   從去年下半年開始,隨著整車銷量的快速增長,動力電池產品供不應求,限制了整車的生產和推廣。廣汽集團總經理曾慶洪表示,國家要求2025年電池的能量密度要達到每公斤350瓦時,現在是150~250瓦時左右。   清華大學教授歐陽明高透露,我國今年1-9月份乘用車電池的用量超過了61億瓦時,預計全年至少會超過110億瓦時,預計2020年有可能達到1000億瓦時。同時,歐陽明高表示,我國今年電池的投資大概接近1000億元,已有和在建的產能投資也超過了800億元,預計明年下半年電池供需總量基本平衡。  
3.整車技術有待提升   廣州日報記者在第二屆廣州國際電動汽車展覽會上看到,目前我國自主推出的電動車在車型方面仍有待提升,不少電動車的外形與傳統燃油車各種“靚”形成較大反差。一對夫婦看了某品牌的電動汽車後表示,開這種車確實是綠色出行,但很卡通的外形用來見商務客戶就不太合適了。  
4.過分依賴補貼   清華大學教授歐陽明高表示,目前國內新能源汽車存在的問題之一就是補貼的依賴還比較嚴重,補貼對整個市場產生的作用應該說過大,需要進行合理化。他認為,尤其是6~8米的客車,目前來說補貼的合理性有待完善。   中國國際貿易促進委員會汽車行業分會會長王俠也表示,我國的汽車市場規模龐大,全產業鏈發展的自然基礎,但是能否實現全產業鏈的協調發展,其中政策的制定是系統的工程,沒有補貼不行,補貼時間過長也不行。王俠表示,企業的當務之急是,實現核心技術的突破,降低成本,提高產品的安全性和可靠性,改變單純依靠補貼的盈利模式。   資料來源:廣州日報

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

【其他文章推薦】

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

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

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

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

簡單看看@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ㄒ)

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

【其他文章推薦】

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

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

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

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

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學習筆記或整理或轉載,如有侵權請聯繫,必改之。

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

【其他文章推薦】

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

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

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

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

第二次汽車下鄉政策或啟動 新能源汽車將成為受益者?

據報導,中國將啟動新一輪汽車下鄉政策,1.6升以下乘用車、微型客貨、皮卡等輕型貨車將納入產品範圍。與此同時,中國還計畫加大停車場和新能源汽車充電基礎設施建設力度。

分析人士認為,二三線城市及四五線的農村的汽車保有量較低,剛性需求較大,預計將成為未來汽車市場增長的主力。

中國品牌汽車與新能源汽車有望雙提升

據中汽協發佈的10月份汽車產銷資料來看,1-10月,中國乘用車累計銷售1648.47萬輛,中國品牌乘用車共銷售675.71萬輛,同比增長12.58%,乘用車銷售總量的40.99%,佔有率比上年同期提升3.16個百分點。

根據乘聯會廠家資料,2015年4季度的新能源乘用車出現爆發增長態勢。11月的新能源乘用車銷量24664台,暴增2.4倍,其中插電混合動力達到7599台,純電動車達到17065台。

一旦新一輪汽車下鄉政策實施,加之新能源汽車充電基礎設施建設力度不斷提升,這不僅將為二三線甚至四五線的消費者帶來更多新車選擇,更將帶動中國汽車品牌以及新能源汽車產銷實現新突破。

可以預見,即將到來的“第二次汽車下鄉”,有望對“十三五”開局之年的汽車產業和發展注入新的動力這一方面會進一步刺激中國品牌車企加速對四五線市場的深耕;另一方面,新能源汽車廠家的佈局與規劃能否滿足未來的市場需求,一切都尚待觀察。

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

【其他文章推薦】

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

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

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

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

福特砸45億美元擴增電動車產線

美商福特汽車(Ford)宣布將投資45億美元擴增電動車生產線,目標在五年內將旗下電動車產品的比例從目前的13%提高到40%左右,同時陸續推出共13款新的電動車與油電混和車。

隨著汽車排放標準愈趨嚴格,福特汽車認為電動車領域的吸引力將比過去更高,因此宣布擴增電動車產線。執行長Mark Fields 表示,在即將推出的13款新車中,也包括有快速充電功能的Focus Electric車款。

近來油價低廉,美國電動車、油電混和車的銷量因而受到壓抑,但福特認為車商仍必須讓消費者了解電動車款的優點,且認為電動車的投資未來將能與一般汽油車擁有差不多的獲益。

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

【其他文章推薦】

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

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

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

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

為何特斯拉 Model S 有高滿意度,「擁有好的客服不怕產品出包」

俗話說早起的鳥兒有蟲吃,不過早買的消費者買到的第一批產品常常問題最多,因為日後的產品,隨著製造技術與經驗進步,以及收到第一批產品的問題回報,品質會逐漸改進。電動車也不例外,美國電動車及油電混合車推動組織 Plug In America 分析報告顯示,特斯拉(Tesla)Model S 電動車,其 2012 ~ 2013 年款的早期車主,可能有高達三分之二行駛 6 萬英里(9.66 萬公里)就需要更換傳動系統。

Plug In America 的分析報告資料來自 370 名車主,其中 43 名擁有 2014 ~ 2015 年份 Model S,由於是新買的車,行駛里程尚未超過 2 萬英里,而根據 327 名早期購買的車主資料進行數學運算分析,預測有 66% 車主行駛 6 萬英里就需要更換傳動系統。不過需要更換傳動系統不代表傳動系統已經故障損壞,因為特斯拉的政策是,一旦傳動系統開始發出不正常雜音就提早換新,而不是等到傳動系統真的故障損壞才換修,另外由於特斯拉保固 8 年,換新對車主也不會造成額外負擔。   這份資料也說明了為何 Model S 遭到一些負面評價,如美國消費性商品評鑑雜誌《消費者報告》(Consumer Reports)把 Model S 從推薦名單中除名,其原因就是認定其「問題發生率高於平均」。日後特斯拉多次表示其技術與經驗進步讓產品品質大幅提升,2015 年 11 月時更表示將故障率降至原有的一半,執行長伊隆‧馬斯克(Elon Musk)也對近幾個月出貨的傳動系統表示信心滿滿。   不過,到底特斯拉的製造品質是否提升,恐怕要等 2014 ~ 2015 年的車輛行駛更久才能見真章。   無論如何,可以確定的一點是,Model S  仍銷售破 10 萬輛,而《消費者報告》指出,儘管問題發生率高於平均,特斯拉車主的滿意度卻居高不下,有高達 97% 車主說會再買特斯拉,原因在於高效率的客服,以最快速度幫助消費者解決問題,不論是馬達、差速器、剎車、資訊系統出問題,都以最不讓消費者困擾的方式盡速更換,「誠意」讓人滿意。   新創產品總是難免有許多意料之外的問題,特斯拉的例子或許可以成為很好的範例,不論是硬體產品還是服務,爛的客服能把好產品弄臭,好的客服讓產品出包也無妨,這天差地別,業者可得戒慎恐懼。

(首圖來源: CC BY 2.0)    (本文授權轉載自《》─〈〉)

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

【其他文章推薦】

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

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

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

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