零基础经过开发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语言编程的内容
欢迎关注晓代码公众号,和你们一块儿学习吧