零基础通过开发Web服务学习Go语言
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9jb019j4-1638772166928)(https://studygolang.com/img/bVbswsd?w=910&h=892)]
本文适合有一定编程基础,但是没有Go语言基础的同学。
也就是俗称的“骗你”学Go语言系列。
这是一个适合阅读的系列,我希望您能够在车上、厕所、餐厅都阅读它,涉及代码的部分也是精简而实用的。
学习需要动机
Go语言能干什么?为什么要学习Go语言?
本系列文章,将会以编程开发中需求最大、应用最广的Web开发为例,一步一步的学习Go语言。当看完本系列,您能够清晰的了解Go语言Web开发的基本原理,您会惊叹于Go语言的简洁、高效和新鲜。
结果反馈才能让你记住
《刻意练习》一书中说,学习需要及时反馈结果,才能提高学习体验。
本系列文章的每一节,都会包含一段可运行的有效代码,跟着内容一步一步操作,你可以在你自己的计算机上体验每一句代码的作用。
不要学习不需要的东西
文章围绕范例为核心,介绍知识点。文中不罗列语法和关键字,当您还不知道它们用来干什么时,反而会干扰您的注意力。
希望您在阅读本系列文章后,对Go语言产生更多的学习欲望,成为一名合格的Gopher
如何10分钟搭建Go开发环境Gopher:原译是囊地鼠,也就是Go语言Logo的那个小可爱;这里特指Go程序员给自己的昵称。
1.下载Go语言安装文件
访问Go语言官方网站下载页面:
https://golang.org/dl
可以看到官网提供了Microsoft Windows、Apple MacOS、Linux和Source下载。
直接下载对应操作系统的安装包。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q5LIGPh8-1638772166930)(https://studygolang.com/img/bVbsxC6?w=1988&h=474)]
2.和其他软件一样,根据提示安装
3.配置环境变量
在正式使用Go编写代码之前,还有一个重要的“环境变量”需要配置:“$GOPATH”
$HOME/go%USERPROFILE%\go
$GOPATH/src$GOPATH/pkg
首先,创建好一个目录用作GOPATH目录
GOPATH
Linux & MacOS:
导入环境变量
$ export GOPATH=$YOUR_PATH/go
保存环境变量
$ source ~/.bash_profile
Windows:
控制面板->系统->高级系统设置->高级->环境变量设置
$GOPATH设置好后,它是一个空目录,当在开发工作中执行go get、go install命令后, $GOPATH所指定的目录会生成3个子目录:
go installgo installgo get
4.检查环境
打开命令行工具,运行
$ 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"
5.选择一款趁手的编辑器或IDE
现在很多通用的编辑器或IDE都支持Go语言比如
Go语言专用的IDE有
专用的IDE无论是配置和使用都比通用编辑器/IDE的简单许多,但是我还是推荐大家使用通用编辑器/IDE,因为在开发过程中肯定会需要编写一些其他语言的程序或脚本,专用IDE在其他语言编写方面较弱,来回切换不同的编辑器/IDE窗口会很低效。
另外,专用IDE提供很多高效的工具,在编译、调试方面都很方便,但是学习阶段,建议大家手动执行命令编译、调试,有利于掌握Go语言。
四行代码的Hello World!所能表达出来的核心命令行代码仅适用于Linux和MacOS系统,Windows根据说明在视窗下操作即可。
1.创建项目
创建一个文件夹,进入该文件夹
$ mkdir gowebserver && cd gowebserver
新建一个文件 main.go
$ touch main.go
2. 用编辑器打开文件,并输入以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
3.打开命令行终端,输入以下命令
$ go run main.go
看到终端会输出:
Hello, 世界
第一个Go代码就完成了
这是一个很简单的Hello World,但是包含了Go语言编程的许多核心元素,接下来就详细讲解。
解读知识点: 包 与 函数
packageimport
Go程序是由包构成的。
packagepackage
main
importimport "math/rand"randrand.New()
导入包的写法可以多行,也可以“分组”, 例如:
import "fmt"
import "math/rand"
或者 分组
import (
"fmt"
"math/rand"
)
fmt包是Go语言内建的包,作用是输出打印。
func
func
func定义函数的格式为:
func 函数名(参数1 类型,参数2 类型){
函数体
}
mainmainfmtPrintln
main.main()mainmain函数
我们已经介绍了九牛一毛中的一毛,接下来正式通过搭建一个简单的Web服务学习Go语言
0依赖,创建一个Web服务先从代码开始
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.Printlnhttp://localhost:8080
解读
我们从程序运行的顺序去了解它的工作流程
package main
net/http
main
第一句,匹配路由和处理函数
http.HandleFunc("/", myWeb)
myWeb
这句代码的意思是,当通过访问地址 http://localhost/ 时,就等同于调用了 myWeb 函数。
第二句,用fmt在控制台打印一句话,纯属提示。
第三句,开启服务并且监听端口
err := http.ListenAndServe(":8080", nil)
httpListenAndServe
什么是nil?
nilnull
http.HandleFunchttp.ListenAndServe
ListenAndServe
errerr
这里有两个Go语言知识点
1.定义变量
var
var str string = "my string"
//^ ^ ^
//关键字 变量名 类型
:=
str := "my string"
2.错误处理
if err != nil{
//处理....
}
在Go语言中,这是很常见的错误处理操作,另一种panic异常,官方建议不要使用或尽量少用,暂不做介绍,先从err开始。
Go语言中规定,如果函数可能出现错误,应该返回一个error对象,这个对象至少包含一个Error()方法错误信息。
因此,在Go中,是看不到try/catch语句的,函数使用error传递错误,用if语句判断错误对象并且处理错误。
3. if 语句
与大多数语言使用方式一样,唯一的区别是,表达式不需要()包起来。
另外,Go语言中的if可以嵌入一个表达式,用;号隔开,例如范例中的代码可以改为:
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("服务器开启错误: ", err)
}
if
请求处理 myWeb函数
mainhttp.HandleFunc/
HandleFuncwrhttp.ResponseWriter*http.Requestwr
响应流写入器 w: 用来写入http响应数据
*
/myWebmyWeb
&*
mystring := "hi"
//取指针
mypointer := &mystring
//取值
mystring2 := *mypointer
fmt.Println(mystring,mypointer,mystring2)
main$ go run main.go
myWeb函数体
fmt.Fprintf(w, "这是一个开始")
fmtFprintfww
总结一下,从编码到运行,你和它都干了些什么:
/
虽然代码很少很少,但是这就是一个最基本的Go语言Web服务程序了。
Web互动第一步,Go http 获得请求参数还是先从代码开始
main.gomyWeb
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
解读
httphttp.RequestmyWebr
r.ParseForm()r.Formr.PostForm
r.URL.Query()r.PostForm
r.URL.Query()r.PostFormstringstring
在http协议中,无论URL和表单,相同名称的参数会组成数组。
循环遍历:for…range
forfor
//无限循环,阻塞线程,用不停息,慎用!
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...rangekv
我们页面还是只是输出一句“这是一个开始”。我们需要一个可以见人的页面,这样可以不行
你也许也想到了,是不是可以在输出时,硬编码HTML字符串?当然可以,但是Go http包提供了更好的方式,HTML模版。
接下来,我们就用HTML模版做一个真正的页面出来
动态响应数据给访客,Go http HTML模版+数据绑定读取HTML模版文件,用数据替换掉对应的标签,生成完整的HTML字符串,响应给浏览器,这是所有Web开发框架的常规操作。Go也是这么干的。
Go html包提供了这样的功能:
html/template
从代码开始
mainhtml/templatemyWeb
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.gohttp://localhost:8080
Hi,{{.name}},{{.someStr}}{{.name}}{{.someStr}}zeta这是一个开始fmt.Fprintln
但是…这还是在代码里硬编码HTML字符串啊…
别着急,template包可以解析文件,继续修改代码:
index.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}}也替换了,对吧?
解读
templateExecute{{}}
t:=template.New("index")t.Parse
然后,创建一个map对象,渲染的时候会用到。
t.Executefmt.Fprintln
templateParseFiles
知识点
map_
map类型
map类型: 字典类型(键值对),之前的获取请求参数章节中出现的 url/values类型其实就是从map类型中扩展出来的
mapmake
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还有几个特点需要了解:
mapmapfor...rangemap
赋值给 “_”
Go有一个特点,变量定义后如果没使用,会报错,无法编译。一般情况下没什么问题,但是极少情况下,我们调用函数,但是并不需要使用返回值,但是不使用,又无法编译,怎么办?
__template.ParseFiles("./templates/index.html")errorerrorerror_
注意注意注意:在实际项目中,请不要丢弃error,任何意外都是可能出现的,丢弃error会导致当出现罕见的意外情况时,非常难于Debug。所有的error都应该要处理,至少写入到日志或打印到控制台。(切记,不要丢弃 error ,很多Gopher们在这个问题上有大把的血泪史)
OK,到目前为止,用Go语言搭建一个简单的网页的核心部分就完成了。
等等 .js、.css、图片怎么办?
对。例子里的模版全是HTML代码,一个漂亮的网页还必须用到图片、js脚本和css样式文件,可是…和PHP不同,请求路径是通过HandleFunc匹配到处理函数的,难道要把js、css和图片都通过函数输出后,再用HandleFunc和URL路径匹配?
处理好js、css和图片,才能做漂亮的网页,Go http静态文件的处理办法以在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文件,在后面加上 ``
$ go run main.go
解读
/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")))
http.Handle("/css/", http.FileServer(http.Dir("./static")))
http.Handle("/img/", http.FileServer(http.Dir("./static")))
http.Handle("/upload/", http.FileServer(http.Dir("./static")))
这样所有请求的路径都必须匹配一个static目录下的子目录。
如果,我就想访问static目录下的文件,或者,js、css、img、upload目录就在项目根目录下怎么办?
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"))))
http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("./css"))))
http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("./img"))))
http.Handle("/upload/", http.StripPrefix("/upload/", http.FileServer(http.Dir("./upload"))))
到这里,一个从流程上完整的Web服务程序就介绍完了。
整理一下,一个Go语言的Web程序基本的流程:
- 定义请求处理函数
- 用http包的HandleFunc匹配处理函数和路由
- ListenAndServe开启监听
当有http请求时:
- http请求到监听的的端口
- 根据路由将请求对象和响应写入器传递给匹配的处理函数
- 处理函数经过一番操作后,将数据写入到响应写入器
- 响应给请求的浏览器
最后编译程序
go run
go run
go build
go build
其他依赖文件的相对路径需要和编译成功后的可执行文件一致,例如范例中的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开发环境
- 导入包、申明包
- func 定义函数
- 变量的申明方法
- Go语言的异常处理
- for循环
- map类型
- 用http包,编写一个网站程序
本系列内容很少,很简洁,希望您能对Go多一点点了解,对Go多增加一点点兴趣。
没有涉及的其他知识
还有很多内容成为一个合格的Gopher必须要了解的知识
- struct 结构体
- 给struct定义方法
- interface 接口定义和实现
- chan类型
- slice类型
- goroutine
- panic处理
以后的文章中会涉及更多关于Go语言编程的内容
欢迎关注晓代码公众号,和大家一起学习吧