通过前两讲的学习和练习,相信大家已经对Go语言“包”的概念和使用有了一定的了解。实际上,在Go SDK中内置了很多实用且常用的源码包。本讲我将介绍四个包的使用,分别是net、fmt、strconv及json。将它们结合使用,便能实现一个简单的服务器软件——人力资源管理系统。本讲将手把手带大家一起完成这个简易服务器软件。

💡 提示:考虑到篇幅和结构,小册毕竟不是API文档。本讲使用到的函数并非上述4个源码包提供的所有能力,仅满足基本的日常开发需要。当需要时,请随时查询API文档获取完整的包能力。

# 准备工作

在正式开始之前,请大家了解下列概念:

  1. http协议,重点关注请求和响应,包括Get/Post的区别、各自适用的场景等等;
  2. json格式,重点理解语法、json对象和数组的数据结构。

以上内容可阅读下面的参考资料:

  1. HTTP 教程 | 菜鸟教程 (runoob.com) (opens new window)
  2. JSON 教程 | 菜鸟教程 (runoob.com) (opens new window)

为了测试服务器软件运行的正确性,我们将使用Postman接口测试工具进行验证。Postman是一款免费的软件,下载地址:Download Postman | Get Started for Free (opens new window)。相关教程请参考:postman使用教程 - 掘金 (juejin.cn) (opens new window)

强烈建议大家在完成上述准备工作后再开始下面的学习。

本讲案例源码位于:gitee.com/wh1990xiao2… (opens new window),没有版权限制,感兴趣的朋友可以克隆到本地进行实验。

# 启动和停止网络服务器

Go SDK内置了启动服务器并监听自定义端口号的工具包,位于net\http包中。若要启动一个本地服务器,只需调用:

go
复制代码http.ListenAndServe()

函数即可,该函数的声明格式如下:

go
复制代码func ListenAndServe(addr string, handler Handler) error

显而易见地,该函数需要两个参数。string类型的addr表示要监听的端口号;Handler类型的handler用来自定义路由的处理逻辑,在大多数时候只需传入nil即可。函数的返回值是error类型,当服务器启动成功后,将为nil,反之则会包含具体的错误信息。

大家都知道,http通常使用的端口号是80,我们不妨就监听80端口,然后根据error类型的返回值来判断服务器是否已经启动成功。具体代码如下:

go
复制代码//启动本地服务器(localhost)
err := http.ListenAndServe(":80", nil)
if err != nil {
   fmt.Println("启动服务失败,错误信息:", err)
}

完成编码后,运行程序,控制台没有任何输出,表示服务器启动应该是成功的。同时,也没有程序停止运行的输出,这表示80端口一直处于被监听的状态。

此时,打开浏览器,访问localhost(或127.0.0.1),会发现网页显示404错误信息。这是正常的现象,因为我们只是启动了服务器,并没有明确定义路径和返回的数据。这个404表示找不到路径,它其实就是http包在找不到匹配的路径时默认返回的数据。

和终止一般程序运行一样,停止服务器软件依然可以点击GoLand上方的Stop按钮实现。当以可执行文件运行时,可以按键盘上的Control+C组合键进行停止。

# 响应“/”路径(站点首页)

乍一看标题,可能会发蒙,什么是“/”路径呢?其实就是网站的根路径,也就是很多网站都在使用的首页路径。

比如知乎,当我们输入juejin.cn时,就会来到掘金首页的综合推荐栏目。这就是掘金网站的根路径。接着再来到沸点,网址会变为juejin.cn/pin,我们就来到了/pin路径下。

所以,一旦服务器成功响应根路径的访问请求,并返回一些数据,那么当我们使用浏览器进行访问网站时,就能成功地看到返回的数据了!

在Go语言中响应访问请求非常简单,下面的代码就实现了返回一句网站欢迎语的响应:

go
复制代码//响应
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "欢迎使用人力资源管理系统")
})

将这段代码放在启动服务器代码之前,然后运行程序。并打开浏览器访问localhost或127.0.0.1,可以看到浏览器中将显示“欢迎使用人力资源管理系统”字样。若使用Postman工具访问,也会得到相同的结果。

❗️ 注意:请务必将接口响应的代码放在启动服务器代码之前,相应的接口请求才能得到响应。

上述代码中有两个地方值得注意,一个是HandleFunc()函数,另一个是Fprintf()函数

我们先来看HandleFunc(),该函数是http包提供的。从源码的角度看,该函数的声明格式为:

go
复制代码func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

该函数需要string类型的pattern作为参数之一,它表示路径。请注意,这里的pattern还可以是正则表达式,而非一个绝对固定的路径值。传入非正则表达式即表示固定的路径值。第二个参数则表示接收到的所有请求参数(包含Header、请求方式、参数值等等)和响应输出管道。整个函数没有任何返回值。

我们再来看Fprintf()函数,该函数来自fmt包。这个包我们已经不陌生了,已经使用过无数次Println()函数。Fprintf()函数的功能与之类似,都是输出一些内容。不同的是,该函数需要两个参数,来看它的声明格式:

go
复制代码func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

该函数需要的两个参数中,我们重点看第一个。它是io.Writer类型的。该参数指定了输出的“目标” 。在示例代码中,我们将网络响应的输出管道传给它,目的就是将要输出的内容作为网络响应返回给发起请求的一方,即浏览器或Postman工具。

# 增加人员

所谓“人力资源管理系统”,对人员数据的增、删、改、查是最最基本的操作了。本讲介绍人员的增加和查询,删除和修改留作思考题给大家。

先来说说增加,我们把增加人员的请求路径命名为“insert”,并只允许通过POST请求来进行。一个人员的信息包括名字(Name)、年龄(Age)和性别(Gender)。名字是string型,年龄和性别均为int,性别中0表示男,1表示女。

先来定义“人员”结构体吧,按照上述规则,我们将人员定义名为hr的结构体,同时为了使用标准化的json结构,在结构体字段后面添加json字段名,具体格式如下:

go
复制代码type hr struct {
   Name   string `json:"name"`
   Age    int    `json:"age"`
   Gender int    `json:"gender"`
}

请注意,结构体的字段名必须要大写,允许外部可见。否则将影响json解析和赋值。另外,用于指定json字段的部分应该由英文的反单引号包括,json字段名用英文的双引号包括

接下来,照猫画虎实现响应/insert路径的函数:

go
复制代码var db []hr
//响应/insert,从传入的参数新增人员信息
http.HandleFunc("/insert", func(w http.ResponseWriter, r *http.Request) {
   if r.Method == "POST" {
      err := r.ParseForm()
      if err != nil {
         fmt.Fprintln(w, "错误的请求")
      } else {
         name := r.FormValue("name")
         // 将string转为int
         age, _ := strconv.Atoi(r.FormValue("age"))
         gender, _ := strconv.Atoi(r.FormValue("gender"))
         db = append(db, hr{Name: name, Age: age, Gender: gender})
         fmt.Fprintln(w, "添加了"+name)
      }
   }
})

上述代码中,有一些没见过的函数调用。

函数体一开始通过

go
复制代码r.Method

的值过滤掉非POST请求,达到只允许POST提交数据的目的。接着,又调用了

go
复制代码r.ParseForm()

这一步的作用是将POST请求的表单解析出来,此处的作用就是初步验证表单信息的正确性,便于后面的赋值。

当解析无误后,进入else分支,通过

go
复制代码r.FormValue()

函数将特定键的值从表单中取出来。然而,该函数的返回值却是string类型的。对于“名字(Name)”字段还好说,但是年龄和性别就难办了。此时,就需要派strconv包来救场了。

strconv包的作用是将字符串转至其它基本数据类型。示例中调用了

go
复制代码strconv.Atoi()

函数,作用就是将string类型的值转换为int类型的值

当然,前提是原值确实是可以被正常转换的。 比如:1、125、367、……通常不会出错,但像“56只”、“128兆字节”这些包含文字的值将转换失败,当发生转换失败时,我们可以从函数返回值中获取到具体的出错信息,转换的结果将会是int的默认值——0

当传入的数据被成功解析和取值后,创建hr类型的变量,并追加到db切片中。最后,输出“添加了xxx”(xxx是Name值)的文字。

完成编码后,将/insert响应的代码添加至开启服务器代码前,然后运行。接着,打开Postman,使用POST方式访问这个接口,并给定请求表单数据,具体如下图所示:

image-20220329145203992.png

显然,小王的人员信息已经被成功地添加到系统中。

接着,我们不妨将请求方式改为GET,大家猜一猜,可以得到什么样的响应呢?

答案是:没有任何输出。

# 查询人员

好了,写过两次网络响应,想必大家已经熟知实现的“套路”了吧?我们继续实现查询人员的网络响应,该接口的请求路径为/query。

稍微来点“超纲”的要求,作为服务器软件,我们在此处提供了两种格式的输出,一种是普通的文本输出,另一种是json格式的输出,通过请求参数中的format字段来控制。只有当format值为json的时候,输出json格式。具体如下图所示:

文本输出:

image-20220329150114349.png

json格式输出:

image-20220329150146837.png

将某种类型转换为json格式及反向转换需借助encoding/json包来实现,转换为json格式的函数声明为:

go
复制代码func Marshal(v interface{}) ([]byte, error)

反向转换的函数声明为:

go
复制代码func Unmarshal(data []byte, v interface{}) error

在增加人员部分,我们将新增的人员信息都保存在了db切片中。因此,查询人员时只需将db切片中的数据按要求的格式进行输出就可以了。请大家先尝试自己动手实操,然后再参考下面的答案。

💡 提示:db变量会随着每次程序停止运行而清空。在测试运行时,请先访问/insert接口新增一些数据,这是能成功查询到已有数据的前提。我将在下一讲介绍数据持久化的方法。

具体代码如下:

go
复制代码//响应/query,获取所有已存在的人员信息。
//给定format,可按json格式输出,默认格式为字符串
http.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) {
   err := r.ParseForm()
   if err != nil {
      fmt.Fprintln(w, "错误的请求")
   } else {
      format := r.FormValue("format")
      if format == "json" {
         data, err := json.Marshal(db)
         if err != nil {
            fmt.Println(err)
         } else {
            fmt.Fprintln(w, string(data))
         }
      } else {
         for i := 0; i < len(db); i++ {
            fmt.Fprintln(w, db[i].Name, db[i].Age, db[i].Gender)
         }
      }
   }
})

接下来,我们再将问题升个级。如果要实现通过name参数来筛选查询出的结果,该如何做呢?请大家课后练习,然后一起讨论。

# 总结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  • net、fmt、strconv及json包的使用

本讲是包系列专题的第三篇。在本讲中,我们创建了一个简单的服务器应用。实际上,想要短时间内彻底讲清一个包中的所有API是不可能的,记住某个包的所有用法更是天方夜谭。本讲主要为大家讲清一种使用Go SDK内置包的思路,希望能起到抛砖引玉的作用。在实际开发时,只需做到对某个包有个印象,知道有这样一个包能够帮我做成某个功能点就行了,然后随用随查即可。

另一方面,随着Go语言生态的日益丰富,各式各样的“轮子”会越来越多。我们在实现某个功能时,甚至可以搜索一下有没有现成的包可以为我所用。别忘了,Go 语言的源码复用就建立在包的基础之上。

官方的源码包网站:pkg.go.dev (opens new window),可以搜索包名或关键字。

好了,本讲就到这里。

➡️ 在下次课程中,我们会介绍Go语言中包的更多使用技巧,具体内容是:

  • Go语言中的持久化存储之文件