零基礎經過開發Web服務學習Go語言javascript
本文適合有必定編程基礎,可是沒有Go語言基礎的同窗。css
也就是俗稱的「騙你」學Go語言系列。html
這是一個適合閱讀的系列,我但願您可以在車上、廁所、餐廳都閱讀它,涉及代碼的部分也是精簡而實用的。前端
Go語言能幹什麼?爲何要學習Go語言?java
本系列文章,將會以編程開發中需求最大、應用最廣的Web開發爲例,一步一步的學習Go語言。當看完本系列,您可以清晰的瞭解Go語言Web開發的基本原理,您會驚歎於Go語言的簡潔、高效和新鮮。linux
《刻意練習》一書中說,學習須要及時反饋結果,才能提升學習體驗。程序員
本系列文章的每一節,都會包含一段可運行的有效代碼,跟着內容一步一步操做,你能夠在你本身的計算機上體驗每一句代碼的做用。golang
文章圍繞範例爲核心,介紹知識點。文中不羅列語法和關鍵字,當您還不知道它們用來幹什麼時,反而會干擾您的注意力。web
但願您在閱讀本系列文章後,對Go語言產生更多的學習慾望,成爲一名合格的Gophershell
Gopher:原譯是囊地鼠,也就是Go語言Logo的那個小可愛;這裏特指Go程序員給本身的暱稱。
訪問Go語言官方網站下載頁面:
能夠看到官網提供了Microsoft Windows、Apple MacOS、Linux和Source下載。
直接下載對應操做系統的安裝包。
在正式使用Go編寫代碼以前,還有一個重要的「環境變量」須要配置:「$GOPATH」
GOPATH環境變量指定工做區的位置。若是沒有設置GOPATH,則假定在Unix系統上爲
$HOME/go
,在Windows上爲%USERPROFILE%\go
。若是要將自定義位置用做工做空間,能夠設置GOPATH環境變量。
GOPATH環境變量是用於設置Go編譯能夠執行文件、包源碼以及依賴包所必要的工做目錄路徑,Go1.11後,新的木塊管理雖然能夠再也不依賴 $GOPATH/src
,可是依然須要使用 $GOPATH/pkg
路徑來保存依賴包。
首先,建立好一個目錄用做GOPATH目錄
而後設置環境變量 GOPATH
:
Linux & MacOS:
導入環境變量
$ export GOPATH=$YOUR_PATH/go
保存環境變量
$ source ~/.bash_profile
Windows:
控制面板->系統->高級系統設置->高級->環境變量設置
GOPATH所指定的目錄會生成3個子目錄:
go install
編譯的可執行二進制文件go install
編譯後的包文件,就會存放在這裏go get
命令下載的源碼包文件打開命令行工具,運行
$ go env
若是你看到相似這樣的結果,說明Go語言環境安裝完成.
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/zeta/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/zeta/workspace/go"
GOPROXY="https://goproxy.io"
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/7v/omg2000000000000019/T/go-build760324613=/tmp/go-build -gno-record-gcc-switches -fno-common"
複製代碼
如今不少通用的編輯器或IDE都支持Go語言好比
Go語言專用的IDE有
專用的IDE不管是配置和使用都比通用編輯器/IDE的簡單許多,可是我仍是推薦你們使用通用編輯器/IDE,由於在開發過程當中確定會須要編寫一些其餘語言的程序或腳本,專用IDE在其餘語言編寫方面較弱,來回切換不一樣的編輯器/IDE窗口會很低效。
另外,專用IDE提供不少高效的工具,在編譯、調試方面都很方便,可是學習階段,建議你們手動執行命令編譯、調試,有利於掌握Go語言。
命令行代碼僅適用於Linux和MacOS系統,Windows根聽說明在視窗下操做便可。
建立一個文件夾,進入該文件夾
$ mkdir gowebserver && cd gowebserver
新建一個文件 main.go
$ touch main.go
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
複製代碼
$ go run main.go
看到終端會輸出:
Hello, 世界
第一個Go代碼就完成了
這是一個很簡單的Hello World,可是包含了Go語言編程的許多核心元素,接下來就詳細講解。
package
申明包 & import
導入包Go程序是由包構成的。
代碼的第一行, 申明程序本身的包,用 package
關鍵字。package
關鍵字必須是第一行出現的代碼。
範例代碼中,申明的本包名 main
在代碼中第二行, 導入「fmt」包, 使用 import
關鍵字。默認狀況下,導入包的包名與導入路徑的最後一個元素一致,例如 import "math/rand"
,在代碼中使用這個包時,直接使用rand
,例如 rand.New()
導入包的寫法能夠多行,也能夠「分組」, 例如:
import "fmt"
import "math/rand"
複製代碼
或者 分組
import (
"fmt"
"math/rand"
)
複製代碼
fmt包是Go語言內建的包,做用是輸出打印。
func
關鍵字:定義函數func
是function的縮寫, 在Go語言中是定義函數的關鍵字。
func定義函數的格式爲:
func 函數名(參數1 類型,參數2 類型){
函數體
}
複製代碼
本例中定義了一個main函數。main
函數沒有參數。 而後在main
函數體裏調用fmt
包的Println
函數,在控制檯輸出字符串 「Hello, 世界」
全部Go語言的程序的入口都是main包下的main函數 main.main()
,因此每個可執行的Go程序都應該有一個main
包和一個main函數
。
咱們已經介紹了九牛一毛中的一毛,接下來正式經過搭建一個簡單的Web服務學習Go語言
打開以前建立好的main.go
文件,修改代碼以下:
package main
import (
"fmt"
"net/http"
)
func myWeb(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "這是一個開始")
}
func main() {
http.HandleFunc("/", myWeb)
fmt.Println("服務器即將開啓,訪問地址 http://localhost:8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("服務器開啓錯誤: ", err)
}
}
複製代碼
保存文件,而後在命令行工具下輸入命令,運行程序
$ go run main.go
這時候,你會看到用 fmt.Println
打印出來的提示,在瀏覽器中訪問 http://localhost:8080
你將訪問到一個頁面,顯示 "這是一個開始"
咱們從程序運行的順序去了解它的工做流程
首先,定義package main
,而後導入包。
這裏,導入了一個新的包 net/http
,這個包是官方的,實現http客戶端和服務端的各類功能。Go語言開發Web服務的全部功能就是基於這個包(其餘第三方的Go語言Web框架也都基於這個包,沒有例外)
main
函數裏發生了什麼第一句,匹配路由和處理函數
http.HandleFunc("/", myWeb)
調用http包的HandleFunc方法,匹配一個路由到一個處理函數myWeb
。
這句代碼的意思是,當經過訪問地址 http://localhost/ 時,就等同於調用了 myWeb 函數。
第二句,用fmt在控制檯打印一句話,純屬提示。
第三句,開啓服務而且監聽端口
err := http.ListenAndServe(":8080", nil)
複製代碼
在這句,調用了http
包中的ListenAndServe
函數,該函數有兩個參數,第一個是指定監聽的端口號,第二個是指定處理請求的handler,一般這個參數填nil,表示使用默認的ServeMux做爲handler。
什麼是nil?
nil
就是其餘語言裏的null
。
什麼是handler?什麼是ServeMux? ServeMux就是一個HTTP請求多路由複用器。它將每一個傳入請求的URL與已註冊模式的列表進行匹配,並調用與URL最匹配的模式的處理程序。 很熟悉吧?還記得前面的
http.HandleFunc
嗎?他就是給http包中默認的ServeMux(DefaultServeMux)添加URL與處理函數匹配。 一般都是使用http包中的默認ServeMux,因此在http.ListenAndServe
函數的第二個參數提供nil就能夠了
ListenAndServe
函數會一直監聽,除非強制退出或者出現錯誤。
若是這句開啓監聽出現錯誤,函數會退出監聽並會返回一個error類型的對象,所以用err
變量接收返回對象。緊接着,判斷err
是否爲空,打印出錯誤內容,程序結束。
這裏有兩個Go語言知識點
Go語言是靜態語言,須要定義變量,定義變量用關鍵字var
var str string = "my string"
//^ ^ ^
//關鍵字 變量名 類型
複製代碼
Go還提了一種簡單的變量定義方式:=
,自動根據賦值的對象定義變量類型,用起來很像腳本語言:
str := "my string"
複製代碼
if err != nil{
//處理....
}
複製代碼
在Go語言中,這是很常見的錯誤處理操做,另外一種panic異常,官方建議不要使用或儘可能少用,暫不作介紹,先從err開始。
Go語言中規定,若是函數可能出現錯誤,應該返回一個error對象,這個對象至少包含一個Error()方法錯誤信息。
所以,在Go中,是看不到try/catch語句的,函數使用error傳遞錯誤,用if語句判斷錯誤對象而且處理錯誤。
與大多數語言使用方式同樣,惟一的區別是,表達式不須要()包起來。
另外,Go語言中的if能夠嵌入一個表達式,用;號隔開,例如範例中的代碼能夠改成:
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("服務器開啓錯誤: ", err)
}
複製代碼
err這個變量的生命週期只在if
塊中有效。
在main
函數中,用http.HandleFunc
將 myWeb與路由/
匹配在一塊兒。
HandleFunc
函數定義了兩個參數w
,r
,參數類型分別是http.ResponseWriter
和*http.Request
,w
是響應留寫入器,r
是請求對象的指針。
響應流寫入器 w: 用來寫入http響應數據
請求對象 * r: 包含了http請求全部信息,注意,這裏使用了指針,在定義參數時用*
標記類型,說明這個參數須要的是這個類型的對象的指針。
當有請求路徑/
,請求對象和響應流寫入器被傳遞給myWeb
函數,並由myWeb
函數負責處理此次請求。
Go語言中紅的指針: 在Go語言中 除了map、slice、chan 其餘函數傳參都是值傳遞,因此,若是須要達到引用傳遞的效果,經過傳遞對象的指針實現。在Go語言中,取對象的指針用&
,取值用*
,例如:
mystring := "hi"
//取指針
mypointer := &mystring
//取值
mystring2 := *mypointer
fmt.Println(mystring,mypointer,mystring2)
複製代碼
把這些代碼放在main
函數裏,$ go run main.go
運行看看
fmt.Fprintf(w, "這是一個開始")
複製代碼
再一次遇到老熟人fmt
,此次使用他的Fprintf
函數將字符串「這是一個開始」,寫入到w
響應流寫入器對象。w
響應流寫入器裏寫入的內容最後會被Response輸出到用戶瀏覽器的頁面上。
/
路由雖然代碼不多不多,可是這就是一個最基本的Go語言Web服務程序了。
打開main.go
文件,修改myWeb
函數,以下:
func myWeb(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //它還將請求主體解析爲表單,得到POST Form表單數據,必須先調用這個函數
for k, v := range r.URL.Query() {
fmt.Println("key:", k, ", value:", v[0])
}
for k, v := range r.PostForm {
fmt.Println("key:", k, ", value:", v[0])
}
fmt.Fprintln(w, "這是一個開始")
}
複製代碼
運行程序
$ go run main.go
而後用任何工具(推薦Postman)提交一個POST請求,而且帶上URL參數,或者在命令行中用cURL提交
curl --request POST \
--url 'http://localhost:8080/?name=zeta' \
--header 'cache-control: no-cache' \
--header 'content-type: application/x-www-form-urlencoded' \
--data description=hello
複製代碼
頁面和終端命令行工具會答應出如下內容:
key: name , value: zeta
key: description , value: hello
複製代碼
http
請求的全部內容,都保存在http.Request
對象中,也就是myWeb
得到的參數 r
。
首先,調用r.ParseForm()
,做用是填充數據到 r.Form
和 r.PostForm
接下來,分別循環獲取遍歷打印出 r.URL.Query()
函數返回的值 和 r.PostForm
值裏的每個參數。
r.URL.Query()
和 r.PostForm
分別是URL參數對象和表單參數對象 ,它們都是鍵值對值,鍵的類型是字符串string
,值的類型是string
數組。
在http協議中,不管URL和表單,相同名稱的參數會組成數組。
循環遍歷:for...range
Go語言的循環只有for
關鍵字,如下是Go中4種for
循環
//無限循環,阻塞線程,用不停息,慎用!
for{
}
//條件循環,若是a<b,循環,不然,退出循環
for a < b{
}
//表達式循環,設i爲0,i小於10時循環,每輪循環後i增長1
for i:=0; i<10; i++{
}
//for...range 遍歷objs,objs必須是map、slice、chan類型
for k, v := range objs{
}
複製代碼
前3種,循環你能夠看做條件循環的變體(無限循環就是無條件的循環)。
本例種用到的是 for...range
循環,遍歷可遍歷對象,而且每輪循環都會將鍵和值分別賦值給變量 k
和 v
咱們頁面仍是隻是輸出一句「這是一個開始」。咱們須要一個能夠見人的頁面,這樣能夠不行
你也許也想到了,是否是能夠在輸出時,硬編碼HTML字符串?固然能夠,可是Go http包提供了更好的方式,HTML模版。
接下來,咱們就用HTML模版作一個真正的頁面出來
讀取HTML模版文件,用數據替換掉對應的標籤,生成完整的HTML字符串,響應給瀏覽器,這是全部Web開發框架的常規操做。Go也是這麼幹的。
Go html包提供了這樣的功能:
"html/template
"
main
函數不變,增長導入html/template
包,而後修改myWeb
函數,以下:
import (
"fmt"
"net/http"
"text/template" //導入模版包
)
func myWeb(w http.ResponseWriter, r *http.Request) {
t := template.New("index")
t.Parse("<div id='templateTextDiv'>Hi,{{.name}},{{.someStr}}</div>")
data := map[string]string{
"name": "zeta",
"someStr": "這是一個開始",
}
t.Execute(w, data)
// fmt.Fprintln(w, "這是一個開始")
}
複製代碼
在命令行中運行 $ go run main.go
,訪問 http://localhost:8080
看,<div id='templateTextDiv'>Hi,{{.name}},{{.someStr}}</div>
中的{{.name}}
和{{.someStr}}
被替換成了 zeta
和這是一個開始
。而且,再也不使用fmt.Fprintln
函數輸出數據到Response了
可是...這仍是在代碼裏硬編碼HTML字符串啊...
彆着急,template包能夠解析文件,繼續修改代碼:
index.html
,並寫入一些HTML代碼 (我不是個好前端)<html>
<head></head>
<body>
<div>Hello {{.name}}</div>
<div>{{.someStr}}</div>
</body>
</html>
複製代碼
myWeb
函數func myWeb(w http.ResponseWriter, r *http.Request) {
//t := template.New("index")
//t.Parse("<div>Hi,{{.name}},{{.someStr}}<div>")
//將上兩句註釋掉,用下面一句
t, _ := template.ParseFiles("./templates/index.html")
data := map[string]string{
"name": "zeta",
"someStr": "這是一個開始",
}
t.Execute(w, data)
// fmt.Fprintln(w, "這是一個開始")
}
複製代碼
在運行一下看看,頁面按照HTML文件的內容輸出了,而且{{.name}}和{{.someStr}}也替換了,對吧?
能夠看到,template
包的核心功能就是將HTML字符串解析暫存起來,而後調用Execute
的時候,用數據替換掉HTML字符串中的{{}}
裏面的內容
在第一個方式中 t:=template.New("index")
初始化一個template對象變量,而後用調用t.Parse
函數解析字符串模版。
而後,建立一個map對象,渲染的時候會用到。
最後,調用t.Execute
函數,不只用數據渲染模版,還替代了fmt.Fprintln
函數的工做,將輸出到Response數據流寫入器中。
第二個方式中,直接調用 template
包的ParseFiles
函數,直接解析相對路徑下的index.html文件並建立對象變量。
本節出現了兩個新東西 map
類型 和 賦值給「_
」
map類型: 字典類型(鍵值對),以前的獲取請求參數章節中出現的 url/values類型其實就是從map類型中擴展出來的
map
的初始化可使用make
:
var data = make(map[string]string)
data = map[string]string{}
複製代碼
make是內置函數,只能用來初始化 map、slice 和 chan,而且make函數和另外一個內置函數new不一樣點在於,它返回的並非指針,而只是一個類型。
map賦值於其餘語言的字典對象相同,取值有兩種方式,請看下面的代碼:
data["name"]="zeta" //賦值
name := data["name"] //方式1.普通取值
name,ok := data["name"] //方式2.若是不存在name鍵,ok爲false
複製代碼
代碼中的變量ok,能夠用來判斷這一項是否設置過,取值時若是項不存在,是不會異常的,取出來的值爲該類型的零值,好比 int類型的值,不存在的項就爲0;string類型的值不存在就爲空字符串,因此經過值是否爲0值是不能判斷該項是否設置過的。 ok,會得到true 或者 false,判斷該項是否設置過,true爲存在,false爲不存在於map中。
Go中的map還有幾個特色須要瞭解:
map
的項的順序是不固定的,每次遍歷排列的順序都是不一樣的,因此不能用順序判斷內容map
能夠用for...range
遍歷map
在函數參數中是引用傳遞(Go語言中,只有map、slice、chan是引用傳遞,其餘都是值傳遞)Go有一個特色,變量定義後若是沒使用,會報錯,沒法編譯。通常狀況下沒什麼問題,可是極少狀況下,咱們調用函數,可是並不須要使用返回值,可是不使用,又沒法編譯,怎麼辦?
"_
" 就是用來解決這個問題的,_
用來丟棄函數的返回值。好比本例中,template.ParseFiles("./templates/index.html")
除了返回模版對象外,還會返回一個error
對象,可是這樣簡單的例子,出錯的可能性極小,因此我不想處理error
了,將error
返回值用「_
」丟棄掉。
注意注意注意:在實際項目中,請不要丟棄error,任何意外都是可能出現的,丟棄error會致使當出現罕見的意外狀況時,很是難於Debug。全部的error都應該要處理,至少寫入到日誌或打印到控制檯。(切記,不要丟棄 error ,不少Gopher們在這個問題上有大把的血淚史)
OK,到目前爲止,用Go語言搭建一個簡單的網頁的核心部分就完成了。
對。例子裏的模版全是HTML代碼,一個漂亮的網頁還必須用到圖片、js腳本和css樣式文件,但是...和PHP不一樣,請求路徑是經過HandleFunc匹配處處理函數的,難道要把js、css和圖片都經過函數輸出後,再用HandleFunc和URL路徑匹配?
以在index.html文件裏引用一個index.js文件爲例。
func main() {
http.HandleFunc("/", myWeb)
//指定相對路徑./static 爲文件服務路徑
staticHandle := http.FileServer(http.Dir("./static"))
//將/js/路徑下的請求匹配到 ./static/js/下
http.Handle("/js/", staticHandle)
fmt.Println("服務器即將開啓,訪問地址 http://localhost:8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("服務器開啓錯誤: ", err)
}
}
複製代碼
在項目的根目錄下建立static目錄,進入static目錄,建立js目錄,而後在js目錄裏建立一個index.js文件。
alert("Javascript running...");
複製代碼
打開以前的index.html文件,在後面加上 <script src="/js/index.js"></script>
運行 $ go run main.go
,訪問 http://localhost:8080,頁面會彈出提示框。
頁面在瀏覽器中運行時,當運行到<script src="/js/index.js"></script>
瀏覽器會請求 /js/index.js
這個路徑
程序檢查到第一層路由匹配/js/
,因而用文件服務處理此次請求,匹配到程序運行的路徑下相對路徑./static/js
。
匹配的設置是 main.go
文件中這兩句
//指定相對路徑./static 爲文件服務路徑
staticHandle := http.FileServer(http.Dir("./static"))
//將/js/路徑下的請求匹配到 ./static/js/下
http.Handle("/js/", staticHandle)
複製代碼
也能夠寫成一句,更容易理解
//瀏覽器訪問/js/ 將會以靜態文件形式訪問目錄 ./static/js
http.Handle("/js/", http.FileServer(http.Dir("./static")))
複製代碼
很簡單...可是,可能仍是不知足需求,由於, 若是
http.Handle("/js/", http.FileServer(http.Dir("./static")))
對應到 ./static/js
http.Handle("/css/", http.FileServer(http.Dir("./static")))
對應到 ./static/css
http.Handle("/img/", http.FileServer(http.Dir("./static")))
對應到 ./static/img
http.Handle("/upload/", http.FileServer(http.Dir("./static")))
對應到 ./static/upload
這樣全部請求的路徑都必須匹配一個static目錄下的子目錄。
若是,我就想訪問static目錄下的文件,或者,js、css、img、upload目錄就在項目根目錄下怎麼辦?
http包下,還提供了一個函數 http.StripPrefix
剝開前綴,以下:
//http.Handle("/js/", http.FileServer(http.Dir("./static")))
//加上http.StripPrefix 改成 :
http.Handle("/js/", http.StripPrefix("/js/", http.FileServer(http.Dir("./static"))))
複製代碼
這樣,瀏覽器中訪問/js/時,直接對應到./static目錄下,不須要再加一個/js/子目錄。
因此,若是須要再根目錄添加多個靜態目錄,而且和URL的路徑匹配,能夠這樣:
http.Handle("/js/", http.StripPrefix("/js/", http.FileServer(http.Dir("./js"))))
對應到 ./js
http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("./css"))))
對應到 ./css
http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("./img"))))
對應到 ./img
http.Handle("/upload/", http.StripPrefix("/upload/", http.FileServer(http.Dir("./upload"))))
對應到 ./upload
到這裏,一個從流程上完整的Web服務程序就介紹完了。
整理一下,一個Go語言的Web程序基本的流程:
當有http請求時:
以前調試都使用的是 go run
命令運行程序。
您會發現,每次運行go run
都會從新編譯源碼,如何將程序運行在沒有Go環境的計算機上?
使用 go build
命令,它會編譯源碼,生成可執行的二進制文件。
最簡單的 go build
命令什麼參數都不用加,它會自動查找目錄下的main包下的main()函數,而後依次查找依賴包編譯成一個可執行文件。
其餘依賴文件的相對路徑須要和編譯成功後的可執行文件一致,例如範例中的templates文件夾和static文件夾。
默認狀況下,go build
會編譯爲和開發操做系統對應的可執行文件,若是要編譯其餘操做系統的可執行文件,須要用到交叉編譯。
例如將Linux和MacOSX系統編譯到windows
GOOS=windows GOARCH=amd64 go build
在Windows上須要使用SET命令, 例如在Windows上編譯到Linux系統
SET GOOS=linux
SET GOARCH=amd64
go build main.go
複製代碼
本系列內容不多,很簡潔,但願您能對Go多一點點了解,對Go多增長一點點興趣。
還有不少內容成爲一個合格的Gopher必需要了解的知識
之後的文章中會涉及更多關於Go語言編程的內容
歡迎關注曉代碼公衆號,和你們一塊兒學習吧