十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
GO是編譯性語言,所以函數(shù)的順序是無關(guān)緊要的,為了方便閱讀,建議入口函數(shù) main 寫在最前面,其余函數(shù)按照功能需要進(jìn)行排列
目前成都創(chuàng)新互聯(lián)已為千余家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬空間、網(wǎng)站托管運營、企業(yè)網(wǎng)站設(shè)計、清徐網(wǎng)站維護(hù)等服務(wù),公司將堅持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
GO的函數(shù) 不支持嵌套,重載和默認(rèn)參數(shù)
GO的函數(shù) 支持 無需聲明變量,可變長度,多返回值,匿名,閉包等
GO的函數(shù)用 func 來聲明,且左大括號 { 不能另起一行
一個簡單的示例:
輸出為:
參數(shù):可以傳0個或多個值來供自己用
返回:通過用 return 來進(jìn)行返回
輸出為:
上面就是一個典型的多參數(shù)傳遞與多返回值
對例子的說明:
按值傳遞:是對某個變量進(jìn)行復(fù)制,不能更改原變量的值
引用傳遞:相當(dāng)于按指針傳遞,可以同時改變原來的值,并且消耗的內(nèi)存會更少,只有4或8個字節(jié)的消耗
在上例中,返回值 (d int, e int, f int) { 是進(jìn)行了命名,如果不想命名可以寫成 (int,int,int){ ,返回的結(jié)果都是一樣的,但要注意:
當(dāng)返回了多個值,我們某些變量不想要,或?qū)嶋H用不到,我們可以使用 _ 來補位,例如上例的返回我們可以寫成 d,_,f := test(a,b,c) ,我們不想要中間的返回值,可以以這種形式來舍棄掉
在參數(shù)后面以 變量 ... type 這種形式的,我們就要以判斷出這是一個可變長度的參數(shù)
輸出為:
在上例中, strs ...string 中, strs 的實際值是b,c,d,e,這就是一個最簡單的傳遞可變長度的參數(shù)的例子,更多一些演變的形式,都非常類似
在GO中 defer 關(guān)鍵字非常重要,相當(dāng)于面相對像中的析構(gòu)函數(shù),也就是在某個函數(shù)執(zhí)行完成后,GO會自動這個;
如果在多層循環(huán)中函數(shù)里,都定義了 defer ,那么它的執(zhí)行順序是先進(jìn)后出;
當(dāng)某個函數(shù)出現(xiàn)嚴(yán)重錯誤時, defer 也會被調(diào)用
輸出為
這是一個最簡單的測試了,當(dāng)然還有更復(fù)雜的調(diào)用,比如調(diào)試程序時,判斷是哪個函數(shù)出了問題,完全可以根據(jù) defer 打印出來的內(nèi)容來進(jìn)行判斷,非??焖?,這種留給你們?nèi)崿F(xiàn)
一個函數(shù)在函數(shù)體內(nèi)自己調(diào)用自己我們稱之為遞歸函數(shù),在做遞歸調(diào)用時,經(jīng)常會將內(nèi)存給占滿,這是非常要注意的,常用的比如,快速排序就是用的遞歸調(diào)用
本篇重點介紹了GO函數(shù)(func)的聲明與使用,下一篇將介紹GO的結(jié)構(gòu) struct
前言
最近工作中遇到的一個場景,php項目中需要使用一個第三方的功能,而恰好有一個用Golang寫好的類庫。那么問題就來了,要如何實現(xiàn)不同語言之間的通信呢?下面就來一起看看吧。
常規(guī)的方案
1、 用Golang寫一個http/TCP服務(wù),php通過http/TCP與Golang通信
2、將Golang經(jīng)過較多封裝,做為php擴展。
3、PHP通過系統(tǒng)命令,調(diào)取Golang的可執(zhí)行文件
存在的問題
1、http請求,網(wǎng)絡(luò)I/O將會消耗大量時間
2、需要封裝大量代碼
3、PHP每調(diào)取一次Golang程序,就需要一次初始化,時間消耗很多
優(yōu)化目標(biāo)
1、Golang程序只初始化一次(因為初始化很耗時)
2、所有請求不需要走網(wǎng)絡(luò)
3、盡量不大量修改代碼
解決方案
1、簡單的Golang封裝,將第三方類庫編譯生成為一個可執(zhí)行文件
2、PHP與Golang通過雙向管道通信
使用雙向管道通信優(yōu)勢
1:只需要對原有Golang類庫進(jìn)行很少的封裝
2:性能最佳 (IPC通信是進(jìn)程間通信的最佳途徑)
3:不需要走網(wǎng)絡(luò)請求,節(jié)約大量時間
4:程序只需初始化一次,并一直保持在內(nèi)存中
具體實現(xiàn)步驟
1:類庫中的原始調(diào)取demo
package main
import (
"fmt"
"github.com/yanyiwu/gojieba"
"strings"
)
func main() {
x := gojieba.NewJieba()
defer x.Free()
s := "小明碩士畢業(yè)于中國科學(xué)院計算所,后在日本京都大學(xué)深造"
words := x.CutForSearch(s, true)
fmt.Println(strings.Join(words, "/"))
}
保存文件為main.go,就可以運行
2:調(diào)整后代碼為:
package main
import (
"bufio"
"fmt"
"github.com/yanyiwu/gojieba"
"io"
"os"
"strings"
)
func main() {
x := gojieba.NewJieba(
"/data/tmp/jiebaDict/jieba.dict.utf8",
"/data/tmp/jiebaDict/hmm_model.utf8",
"/data/tmp/jiebaDict/user.dict.utf8"
)
defer x.Free()
inputReader := bufio.NewReader(os.Stdin)
for {
s, err := inputReader.ReadString('\n')
if err != nil err == io.EOF {
break
}
s = strings.TrimSpace(s)
if s != "" {
words := x.CutForSearch(s, true)
fmt.Println(strings.Join(words, " "))
} else {
fmt.Println("get empty \n")
}
}
}
只需要簡單的幾行調(diào)整,即可實現(xiàn):從標(biāo)準(zhǔn)輸入接收字符串,經(jīng)過分詞再輸出
測試:
# go build test
# ./test
# //等待用戶輸入,輸入”這是一個測試“
# 這是 一個 測試 //程序
3:使用cat與Golang通信做簡單測試
//準(zhǔn)備一個title.txt,每行是一句文本
# cat title.txt | ./test
正常輸出,表示cat已經(jīng)可以和Golang正常交互了
4:PHP與Golang通信
以上所示的cat與Golang通信,使用的是單向管道。即:只能從cat向Golang傳入數(shù)據(jù),Golang輸出的數(shù)據(jù)并沒有傳回給cat,而是直接輸出到屏幕。但文中的需求是:php與Golang通信。即php要傳數(shù)據(jù)給Golang,同時Golang也必須把執(zhí)行結(jié)果返回給php。因此,需要引入雙向管道。
在PHP中管道的使用:popen("/path/test") ,具體就不展開說了,因為此方法解決不了文中的問題。
雙向管道:
$descriptorspec = array(
0 = array("pipe", "r"),
1 = array("pipe", "w")
);
$handle = proc_open(
'/webroot/go/src/test/test',
$descriptorspec,
$pipes
);
fwrite($pipes['0'], "這是一個測試文本\n");
echo fgets($pipes[1]);
解釋:使用proc_open打開一個進(jìn)程,調(diào)用Golang程序。同時返回一個雙向管道pipes數(shù)組,php向$pipe['0']中寫數(shù)據(jù),從$pipe['1']中讀數(shù)據(jù)。
好吧,也許你已經(jīng)發(fā)現(xiàn),我是標(biāo)題檔,這里重點要講的并不只是PHP與Golang如何通信。而是在介紹一種方法: 通過雙向管道讓任意語言通信。(所有語言都會實現(xiàn)管道相關(guān)內(nèi)容)
測試:
通過對比測試,計算出各個流程占用的時間。下面提到的title.txt文件,包含100萬行文本,每行文本是從b2b平臺取的商品標(biāo)題
1: 整體流程耗時
time cat title.txt | ./test /dev/null
耗時:14.819秒,消耗時間包含:
進(jìn)程cat讀出文本
通過管道將數(shù)據(jù)傳入Golang
Golang處理數(shù)據(jù),將結(jié)果返回到屏幕
2:計算分詞函數(shù)耗時。方案:去除分詞函數(shù)的調(diào)取,即:注釋掉Golang源代碼中的調(diào)取分詞那行的代碼
time cat title.txt | ./test /dev/null
耗時:1.817秒時間,消耗時間包含:
進(jìn)程cat讀出文本
通過管道將數(shù)據(jù)傳入Golang
Golang處理數(shù)據(jù),將結(jié)果返回到屏幕
分詞耗時 = (第一步耗時) - (以上命令所耗時)
分詞耗時 : 14.819 - 1.817 = 13.002秒
3:測試cat進(jìn)程與Golang進(jìn)程之間通信所占時間
time cat title.txt /dev/null
耗時:0.015秒,消耗時間包含:
進(jìn)程cat讀出文本
通過管道將數(shù)據(jù)傳入Golang
go處理數(shù)據(jù),將結(jié)果返回到屏幕
管道通信耗時:(第二步耗時) - (第三步耗時)
管道通信耗時: 1.817 - 0.015 = 1.802秒
4:PHP與Golang通信的時間消耗
編寫簡單的php文件:
?php
$descriptorspec = array(
0 = array("pipe", "r"),
1 = array("pipe", "w")
);
$handle = proc_open(
'/webroot/go/src/test/test',
$descriptorspec,
$pipes
);
$fp = fopen("title.txt", "rb");
while (!feof($fp)) {
fwrite($pipes['0'], trim(fgets($fp))."\n");
echo fgets($pipes[1]);
}
fclose($pipes['0']);
fclose($pipes['1']);
proc_close($handle);
流程與上面基本一致,讀出title.txt內(nèi)容,通過雙向管道傳入Golang進(jìn)程分詞后,再返回給php (比上面的測試多一步:數(shù)據(jù)再通過管道返回)
time php popen.php /dev/null
耗時:24.037秒,消耗時間包含:
進(jìn)程PHP讀出文本
通過管道將數(shù)據(jù)傳入Golang
Golang處理數(shù)據(jù)
Golang將返回結(jié)果再寫入管道,PHP通過管道接收數(shù)據(jù)
將結(jié)果返回到屏幕
結(jié)論:
1 :整個分詞過程中的耗時分布
使用cat控制邏輯耗時: 14.819 秒
使用PHP控制邏輯耗時: 24.037 秒(比cat多一次管道通信)
單向管道通信耗時: 1.8 秒
Golang中的分詞函數(shù)耗時: 13.002 秒
2:分詞函數(shù)的性能: 單進(jìn)程,100萬商品標(biāo)題分詞,耗時13秒
以上時間只包括分詞時間,不包括詞典載入時間。但在本方案中,詞典只載入一次,所以載入詞典時間可以忽略(1秒左右)
3:PHP比cat慢 (這結(jié)論有點多余了,呵呵)
語言層面慢: (24.037 - 1.8 - 14.819) / 14.819 = 50%
單進(jìn)程對比測試的話,應(yīng)該不會有哪個語言比cat更快。
相關(guān)問題:
1:以上Golang源碼中寫的是一個循環(huán),也就是會一直從管道中讀數(shù)據(jù)。那么存在一個問題:是不是php進(jìn)程結(jié)束后,Golang的進(jìn)程還會一直存在?
管道機制自身可解決此問題。管道提供兩個接口:讀、寫。當(dāng)寫進(jìn)程結(jié)束或者意外掛掉時,讀進(jìn)程也會報錯,以上Golang源代碼中的err邏輯就會執(zhí)行,Golang進(jìn)程結(jié)束。
但如果PHP進(jìn)程沒有結(jié)束,只是暫時沒有數(shù)據(jù)傳入,此時Golang進(jìn)程會一直等待。直到php結(jié)束后,Golang進(jìn)程才會自動結(jié)束。
2:能否多個php進(jìn)程并行讀寫同一個管道,Golang進(jìn)程同時為其服務(wù)?
不可以。管道是單向的,如果多個進(jìn)程同時向管道中寫,那Golang的返回值就會錯亂。
可以多開幾個Golang進(jìn)程實現(xiàn),每個php進(jìn)程對應(yīng)一個Golang進(jìn)程。
最后,上面都是瞎扯的。如果你了解管道、雙向管道,上面的解釋對你基本沒啥用。但如果你不了解管道,調(diào)試上面的代碼沒問題,但稍有修改就有可能掉坑里。
剛?cè)腴TGo語言小白需要注意以下五點:
1、注意書寫代碼的一些規(guī)范吧,特別是注意大小寫、英文標(biāo)點符號區(qū)別等,在特別的位置寫上注釋。
2、主要是理解偽代碼所描述的算法,偽代碼要注意是不能直接運行的。
3、注意編譯器版本與書籍上所介紹版本是否一致,也注意特殊符號,印刷版本可能與實際不一致。
4、書上的版本和當(dāng)前所用的版本是否一致,有些情況下書上版本在現(xiàn)在來用已經(jīng)過時了。
5、邏輯走通;給自己信心,其實起步階段不難的。
在開始之前,希望你計算一下 Part1 共占用的大小是多少呢?
輸出結(jié)果:
這么一算, Part1 這一個結(jié)構(gòu)體的占用內(nèi)存大小為 1+4+1+8+1 = 15 個字節(jié)。相信有的小伙伴是這么算的,看上去也沒什么毛病
真實情況是怎么樣的呢?我們實際調(diào)用看看,如下:
輸出結(jié)果:
最終輸出為占用 32 個字節(jié)。這與前面所預(yù)期的結(jié)果完全不一樣。這充分地說明了先前的計算方式是錯誤的。為什么呢?
在這里要提到 “內(nèi)存對齊” 這一概念,才能夠用正確的姿勢去計算,接下來我們詳細(xì)的講講它是什么
有的小伙伴可能會認(rèn)為內(nèi)存讀取,就是一個簡單的字節(jié)數(shù)組擺放
上圖表示一個坑一個蘿卜的內(nèi)存讀取方式。但實際上 CPU 并不會以一個一個字節(jié)去讀取和寫入內(nèi)存。相反 CPU 讀取內(nèi)存是 一塊一塊讀取 的,塊的大小可以為 2、4、6、8、16 字節(jié)等大小。塊大小我們稱其為 內(nèi)存訪問粒度 。如下圖:
在樣例中,假設(shè)訪問粒度為 4。 CPU 是以每 4 個字節(jié)大小的訪問粒度去讀取和寫入內(nèi)存的。這才是正確的姿勢
另外作為一個工程師,你也很有必要學(xué)習(xí)這塊知識點哦 :)
在上圖中,假設(shè)從 Index 1 開始讀取,將會出現(xiàn)很崩潰的問題。因為它的內(nèi)存訪問邊界是不對齊的。因此 CPU 會做一些額外的處理工作。如下:
從上述流程可得出,不做 “內(nèi)存對齊” 是一件有點 "麻煩" 的事。因為它會增加許多耗費時間的動作
而假設(shè)做了內(nèi)存對齊,從 Index 0 開始讀取 4 個字節(jié),只需要讀取一次,也不需要額外的運算。這顯然高效很多,是標(biāo)準(zhǔn)的 空間換時間 做法
在不同平臺上的編譯器都有自己默認(rèn)的 “對齊系數(shù)”,可通過預(yù)編譯命令 #pragma pack(n) 進(jìn)行變更,n 就是代指 “對齊系數(shù)”。一般來講,我們常用的平臺的系數(shù)如下:
另外要注意,不同硬件平臺占用的大小和對齊值都可能是不一樣的。因此本文的值不是唯一的,調(diào)試的時候需按本機的實際情況考慮
輸出結(jié)果:
在 Go 中可以調(diào)用 unsafe.Alignof 來返回相應(yīng)類型的對齊系數(shù)。通過觀察輸出結(jié)果,可得知基本都是 2^n ,最大也不會超過 8。這是因為我手提(64 位)編譯器默認(rèn)對齊系數(shù)是 8,因此最大值不會超過這個數(shù)
在上小節(jié)中,提到了結(jié)構(gòu)體中的成員變量要做字節(jié)對齊。那么想當(dāng)然身為最終結(jié)果的結(jié)構(gòu)體,也是需要做字節(jié)對齊的
接下來我們一起分析一下,“它” 到底經(jīng)歷了些什么,影響了 “預(yù)期” 結(jié)果
在每個成員變量進(jìn)行對齊后,根據(jù)規(guī)則 2,整個結(jié)構(gòu)體本身也要進(jìn)行字節(jié)對齊,因為可發(fā)現(xiàn)它可能并不是 2^n ,不是偶數(shù)倍。顯然不符合對齊的規(guī)則
根據(jù)規(guī)則 2,可得出對齊值為 8?,F(xiàn)在的偏移量為 25,不是 8 的整倍數(shù)。因此確定偏移量為 32。對結(jié)構(gòu)體進(jìn)行對齊
Part1 內(nèi)存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx
通過本節(jié)的分析,可得知先前的 “推算” 為什么錯誤?
是因為實際內(nèi)存管理并非 “一個蘿卜一個坑” 的思想。而是一塊一塊。通過空間換時間(效率)的思想來完成這塊讀取、寫入。另外也需要兼顧不同平臺的內(nèi)存操作情況
在上一小節(jié),可得知根據(jù)成員變量的類型不同,其結(jié)構(gòu)體的內(nèi)存會產(chǎn)生對齊等動作。那假設(shè)字段順序不同,會不會有什么變化呢?我們一起來試試吧 :-)
輸出結(jié)果:
通過結(jié)果可以驚喜的發(fā)現(xiàn),只是 “簡單” 對成員變量的字段順序進(jìn)行改變,就改變了結(jié)構(gòu)體占用大小
接下來我們一起剖析一下 Part2 ,看看它的內(nèi)部到底和上一位之間有什么區(qū)別,才導(dǎo)致了這樣的結(jié)果?
符合規(guī)則 2,不需要額外對齊
Part2 內(nèi)存布局:ecax|bbbb|dddd|dddd
通過對比 Part1 和 Part2 的內(nèi)存布局,你會發(fā)現(xiàn)兩者有很大的不同。如下:
仔細(xì)一看, Part1 存在許多 Padding。顯然它占據(jù)了不少空間,那么 Padding 是怎么出現(xiàn)的呢?
通過本文的介紹,可得知是由于不同類型導(dǎo)致需要進(jìn)行字節(jié)對齊,以此保證內(nèi)存的訪問邊界
那么也不難理解,為什么 調(diào)整結(jié)構(gòu)體內(nèi)成員變量的字段順序 就能達(dá)到縮小結(jié)構(gòu)體占用大小的疑問了,是因為巧妙地減少了 Padding 的存在。讓它們更 “緊湊” 了。這一點對于加深 Go 的內(nèi)存布局印象和大對象的優(yōu)化非常有幫
Go 由于不支持泛型而臭名昭著,但最近,泛型已接近成為現(xiàn)實。Go 團隊實施了一個看起來比較穩(wěn)定的設(shè)計草案,并且正以源到源翻譯器原型的形式獲得關(guān)注。本文講述的是泛型的最新設(shè)計,以及如何自己嘗試泛型。
例子
FIFO Stack
假設(shè)你要創(chuàng)建一個先進(jìn)先出堆棧。沒有泛型,你可能會這樣實現(xiàn):
type?Stack?[]interface{}func?(s?Stack)?Peek()?interface{}?{
return?s[len(s)-1]
}
func?(s?*Stack)?Pop()?{
*s?=?(*s)[:
len(*s)-1]
}
func?(s?*Stack)?Push(value?interface{})?{
*s?=?
append(*s,?value)
}
但是,這里存在一個問題:每當(dāng)你 Peek 項時,都必須使用類型斷言將其從 interface{} 轉(zhuǎn)換為你需要的類型。如果你的堆棧是 *MyObject 的堆棧,則意味著很多 s.Peek().(*MyObject)這樣的代碼。這不僅讓人眼花繚亂,而且還可能引發(fā)錯誤。比如忘記 * 怎么辦?或者如果您輸入錯誤的類型怎么辦?s.Push(MyObject{})` 可以順利編譯,而且你可能不會發(fā)現(xiàn)到自己的錯誤,直到它影響到你的整個服務(wù)為止。
通常,使用 interface{} 是相對危險的。使用更多受限制的類型總是更安全,因為可以在編譯時而不是運行時發(fā)現(xiàn)問題。
泛型通過允許類型具有類型參數(shù)來解決此問題:
type?Stack(type?T)?[]Tfunc?(s?Stack(T))?Peek()?T?{
return?s[len(s)-1]
}
func?(s?*Stack(T))?Pop()?{
*s?=?(*s)[:
len(*s)-1]
}
func?(s?*Stack(T))?Push(value?T)?{
*s?=?
append(*s,?value)
}
這會向 Stack 添加一個類型參數(shù),從而完全不需要 interface{}?,F(xiàn)在,當(dāng)你使用 Peek() 時,返回的值已經(jīng)是原始類型,并且沒有機會返回錯誤的值類型。這種方式更安全,更容易使用。(譯注:就是看起來更丑陋,^-^)
此外,泛型代碼通常更易于編譯器優(yōu)化,從而獲得更好的性能(以二進(jìn)制大小為代價)。如果我們對上面的非泛型代碼和泛型代碼進(jìn)行基準(zhǔn)測試,我們可以看到區(qū)別:
type?MyObject?struct?{
X?
int
}
var?sink?MyObjectfunc?BenchmarkGo1(b?*testing.B)?{
for?i?:=?0;?i??b.N;?i++?{
var?s?Stack
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink?=?s.Peek().(MyObject)
}
}
func?BenchmarkGo2(b?*testing.B)?{
for?i?:=?0;?i??b.N;?i++?{
var?s?Stack(MyObject)
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink?=?s.Peek()
}
}
結(jié)果:
BenchmarkGo1BenchmarkGo1-16?????12837528?????????87.0?ns/op???????48?B/op????????2?allocs/opBenchmarkGo2BenchmarkGo2-16?????28406479?????????41.9?ns/op???????24?B/op????????2?allocs/op
在這種情況下,我們分配更少的內(nèi)存,同時泛型的速度是非泛型的兩倍。
合約(Contracts)
上面的堆棧示例適用于任何類型。但是,在許多情況下,你需要編寫僅適用于具有某些特征的類型的代碼。例如,你可能希望堆棧要求類型實現(xiàn) String() 函數(shù)
有兩種方法,根據(jù)例子說明: String - ints="12345";int i;第一種方法:i=Integer.parseInt(s);第二種方法:i=Integer.valueOf(s).intValue();第一種方法:i=Integer.parseInt(s);//直接使用靜態(tài)方法,不會產(chǎn)生多余的對象,但會拋出異常第二種.