实践是最好的学习方式

零基础通过开发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

Gopher:原译是囊地鼠,也就是Go语言Logo的那个小可爱;这里特指Go程序员给自己的昵称。

如何10分钟搭建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程序基本的流程:

  1. 定义请求处理函数
  2. 用http包的HandleFunc匹配处理函数和路由
  3. ListenAndServe开启监听

当有http请求时:

  1. http请求到监听的的端口
  2. 根据路由将请求对象和响应写入器传递给匹配的处理函数
  3. 处理函数经过一番操作后,将数据写入到响应写入器
  4. 响应给请求的浏览器

最后编译程序

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

结语,学到了什么?还要学什么?

学到了什么?

  1. 快速简单搭建Go开发环境
  2. 导入包、申明包
  3. func 定义函数
  4. 变量的申明方法
  5. Go语言的异常处理
  6. for循环
  7. map类型
  8. 用http包,编写一个网站程序

本系列内容很少,很简洁,希望您能对Go多一点点了解,对Go多增加一点点兴趣。

没有涉及的其他知识

还有很多内容成为一个合格的Gopher必须要了解的知识

  1. struct 结构体
  2. 给struct定义方法
  3. interface 接口定义和实现
  4. chan类型
  5. slice类型
  6. goroutine
  7. panic处理

以后的文章中会涉及更多关于Go语言编程的内容

欢迎关注晓代码公众号,和大家一起学习吧