简易 http 服务

使用 go 自带的 http 模块实现一个简单的 http 服务器,对请求者说一句 Hello, go http!

func main(){
	http.HandleFunc("/hello", sayHello)
	http.ListenAndServe(":8080", nil)
}

func sayHello(resp http.ResponseWriter, req *http.Request) {
	log.Println("In say hello")
	resp.WriteHeader(http.StatusOK)
	resp.Write([]byte("Hello, go http!"))
}

运行上述代码,在终端使用 curl 访问该地址,返回正常:

>curl http://localhost:8080/hello
Hello, go http!

且程序使用 log 打印了一句: 2022/03/27 15:32:48 In say hello

但是,我们的http方法有很多, GET/POST/PUT/PATCH/DELETE 等,我们使用任意一个方法,访问上述地址,都能得到 Hello, go http! 这句返回,如 POST:

>curl -X POST http://localhost:8080/hello
Hello, go http!

http.Request 这个结构体中,包含了一个 Method 字段,让我们可以根据不同的方法,决定不同的处理方式,我们将 sayHello 方法改造如下:

func sayHello(resp http.ResponseWriter, req *http.Request) {
	log.Println("In say hello")
	switch req.Method {
	case "GET":
		fmt.Fprintln(resp, "GET: Hello, go http!")
	case "POST":
		fmt.Fprintln(resp, "POST: Hello, go http!")
	default:
		resp.WriteHeader(http.StatusNotFound)
		fmt.Fprintln(resp, "Not found")
	}
}

请求输出:

>curl http://localhost:8080/hello
GET: Hello, go http!

>curl -X POST http://localhost:8080/hello
POST: Hello, go http!

>curl -X PUT http://localhost:8080/hello
Not found

基于 gin 框架的 RESTFUL API 服务

虽然go自带的net/http包可以方便的创建HTTP服务,但是其包含以下不足:

  • 不能单独地对请求方法(POST/GET等)注册特定的处理函数
  • 不支持 path 变量参数
  • 不能自动对 path 进行校准
  • 性能一般
  • 扩展性不足
  • 。。。

gin是 github 上开源的一个 go web 框架,它是一个包,我们可以通过 go.mod 进行引入

接下来,我们使用gin框架实现一个简单的用户查询系统

引入 gin框架

安装命令:

go get github.com/gin-gonic/gin

引入:

import "github.com/gin-gonic/gin"

使用gin框架

使用gin框架,我们可以简单的这样写:

func main(){
    engine := gin.Default()
    engine.Run(":8080")
}

gin.Default()方法会返回一个 gin Engine 实例,我们调用这个 实例的 Run方法,传入监听的地址,就可以实现一个简单的gin服务。

处理具体路径的请求,我们可以使用 engine.Handle方法

定义 User 结构体:

type User struct {
	ID uint			`json:"id"`
	Age uint		`json:"age"`
	Name string		`json:"name"`
}

// 初始用户列表
var userList []User = []User{ {1, 19, "Mike"}, {2, 20, "Jack"} }

上述代码,我们定义了一个用户User的基本接口,包含字段 ID, Age, Name, 然后定义了 json struct tag,让gin返回的json字段小写。

获取用户列表

我们接下来实现一个 GET 方法,这个方法请求路径 /users,返回所有的用户列表:

// 获取用户列表
func getAllUsers(c *gin.Context) {
	log.Println("Getting user list")
	c.JSON(http.StatusOK, userList)
}

非常简单,getAllUsers 是一个Handler, 接收一个 *gin.Contest类型的参数,在这个Handler里面,我们直接将用户列表userList返回,使用 c.JSON方法,这个方法接收两个参数:

  • 第一个是 http状态码
  • 第二个是要返回的对象

这个方法会自动将返回对象序列化成为Json字符串

完成了一个Handler,我们接下来在gin中注册这个Handler

main.go

func main(){
	engine := gin.Default()
	engine.Handle(http.MethodGet, "/users", getAllUsers)
	engine.Run(":8080")
}

运行上述程序,然后在终端测试:

>curl http://localhost:8080/users
[{"id":1,"age":19,"name":"Mike"},{"id":2,"age":20,"name":"Jack"}]

可以看到,正常返回了用户列表

查询具体用户

// 查询用户
func queryUser(c *gin.Context) {
	id := c.Param("id")
	log.Printf("Querying user with id = %s", id)
	for _, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Found user: %v", u)
			c.JSON(http.StatusOK, u)
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{
		"message": "user not found.",
	})
}

我们实现了一个 queryUser handler,这个handler处理的方法是 GET,请求的api是 /users/:id 里面的 id 是路径参数,在gin中,我们通过 c.Param() 方法,获取路径参数,这个方法返回的是一个 string类型的变量,我们用定义的 ID字段是 uint 类型的,因此,我们需要使用 strconv.Itoa 对其进行一个转换,然后通过对比查找对应的用户。

找到对应的用户之后,我们通过 c.JSON 返回用户,否则,我们返回一个 404 Not Found

里面用到的 gin.H 本质是一个 map[string]interface{} 类型,用来方便返回一些信息

main中注册新的Handler:

func main(){
    //...
    engine.Handle(http.MethodGet, "/users/:id", queryUser)
    //...
}

运行程序后,测试:

>curl http://localhost:8080/users/1
{"id":1,"age":19,"name":"Mike"}				# 查询到id为1的用户
>curl http://localhost:8080/users/3			# id为3的用户不存在
{"message":"user not found."}

可以看到,在用户存在或不存在的情况下,都可以正常返回

创建新用户

// 创建用户
func createUser(c *gin.Context) {
	var name string
	var age uint

	if name_, ok := c.GetPostForm("name"); !ok {
		msg := "[name] field missed."
		log.Println(msg)
		c.JSON(http.StatusBadRequest, gin.H{"message": msg})
		return
	} else {
		name = name_
	}
	if age_, ok := c.GetPostForm("age"); !ok {
		msg := "[age] field missed."
		log.Println(msg)
		c.JSON(http.StatusBadRequest, gin.H{"message": msg})
		return
	} else {
		_age, err := strconv.Atoi(age_)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid age: " + age_})
			return
		}
		age = uint(_age)
	}
	// find last ID
	var lastID uint = userList[len(userList) - 1].ID
	// create user
	newUser := User{ID: lastID+1, Age: age, Name: name}
	userList = append(userList, newUser)
	c.JSON(http.StatusOK, gin.H{"message": "ok, new user id : " + strconv.Itoa(int(newUser.ID))})
	log.Printf("New user created, id = %d", newUser.ID)
}

创建用户,我们通过 POST方法,请求 /users ,传入的参数有两个字段: name,age。

gin框架中,我们通过 c.GetPostForm 方法,获取POST方法里面的内容,返回 string, bool,我们可以通过返回的flag,判断用户是否传了对应的值,以实现特定的逻辑。当然,我们也可以通过 c.DefaultPostForm, 在获取不到指定值的情况下,返回一个空字符串。

然后通过取到最后一个用户的ID,生成下一个用户的ID,实现自增。(其实这样有很大的缺陷,比如我们删除了最后一个用户之后,新建的用户ID可能会跟刚刚删除的用户ID一样,导致冲突。)

注册Handler:

func main(){
    //...
    engine.Handle(http.MethodPost, "/users", createUser)
    //...
}

运行、测试:

>curl -X POST  http://localhost:8080/users
{"message":"[name] field missed."}		# 错误:不带name参数
>curl -X POST  http://localhost:8080/users -d "name=Jack"
{"message":"[age] field missed."}		# 错误:不带age参数
>curl -X POST  http://localhost:8080/users -d "name=Jack" -d "age=aa"
{"message":"Invalid age: aa"}			# 错误的 age 参数
>curl -X POST  http://localhost:8080/users -d "name=Jack" -d "age=29"
{"message":"ok, new user id : 3"}		# 创建成功,新用户ID 为 3
>curl http://localhost:8080/users		# 查看用户列表,新用户成功创建
[{"id":1,"age":19,"name":"Mike"},{"id":2,"age":20,"name":"Jack"},{"id":3,"age":29,"name":"Jack"}]

修改用户

// 修改用户
func modifyUser(c *gin.Context) {
	id := c.Param("id")
	var age int
	name := c.DefaultPostForm("name", "")
	_age := c.DefaultPostForm("age", "")
    // 两个必传参数
	if name == "" || _age == "" {
		c.JSON(http.StatusBadRequest, gin.H{"message": "[name] and [age] is required."})
		return
	}
	// string to int
	age, err := strconv.Atoi(_age)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid age: " + _age})
		return
	}
	log.Printf("Modify user with id = %s", id)
    // 查找用户进行修改
	for idx, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Found user: %v", u)
			u.Name = name
			u.Age = uint(age)
			userList[idx] = u
			log.Printf("User modified to: %v", u)
			c.JSON(http.StatusOK, gin.H{"message": "ok"})
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{"message": "user not found."})
}

逻辑简单,直接从post方法传过来的数据中,获取name和age字段,然后查找对应的用户,对其进行修改,当用户不存在的情况下,返回 404 Not Found

注册 Handler:

func main(){
    //...
    engine.Handle(http.MethodPut, "/users/:id", modifyUser)
    //...
}

运行,测试:

>curl http://localhost:8080/users		# 当前的用户列表
[{"id":1,"age":19,"name":"Mike"},{"id":2,"age":20,"name":"Jack"}]
>curl -X PUT http://localhost:8080/users/4 -d "name=Amy"				# 参数缺失
{"message":"[name] and [age] is required."}
>curl -X PUT http://localhost:8080/users/4 -d "name=Amy" -d "age=10"	# id=4的用户不存在
{"message":"user not found."}
>curl -X PUT http://localhost:8080/users/2 -d "name=Amy" -d "age=10"	# 修改成功
{"message":"ok"}
>curl http://localhost:8080/users
[{"id":1,"age":19,"name":"Mike"},{"id":2,"age":10,"name":"Amy"}]		# 修改成功后的用户列表

可以看到,表现符合预期。

删除用户

// 删除用户
func deleteUser(c *gin.Context) {
	id := c.Param("id")
	log.Printf("Querying user with id = %s", id)
	for idx, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Deleting user: %v", u)
			for ; idx < len(userList) - 1; idx++ {
				userList[idx] = userList[idx+1]
			}
			userList = userList[:len(userList) - 1]
			c.JSON(http.StatusOK, gin.H{"message": "ok"})
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{
		"message": "user not found.",
	})
}

这里删除用户的逻辑也是很简单的:从传入的用户id里面找到当前用户的位置,然后从数组里面将该用户删除掉即可。

这里使用的是数据覆盖,实际业务逻辑中,我们可以使用Mongo、MySQL等数据引擎,实现CRUD操作。

func main(){
    //...
    engine.Handle(http.MethodDelete, "/users/:id", deleteUser)
    //...
}

运行、调试:

>curl http://localhost:8080/users
[{"id":1,"age":19,"name":"Mike"},{"id":2,"age":20,"name":"Jack"}]	# 当前的所有用户
>curl -X DELETE http://localhost:8080/users/2	# 删除第二个用户
{"message":"ok"}
>curl http://localhost:8080/users
[{"id":1,"age":19,"name":"Mike"}]		# 剩下一个用户

附代码:

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"strconv"
	"strings"
)


func main(){
	engine := gin.Default()
	engine.Handle(http.MethodGet, "/users", getAllUsers)
	engine.Handle(http.MethodPost, "/users", createUser)
	engine.Handle(http.MethodGet, "/users/:id", queryUser)
	engine.Handle(http.MethodPut, "/users/:id", modifyUser)
	engine.Handle(http.MethodDelete, "/users/:id", deleteUser)
	engine.Run(":8080")
}

type User struct {
	ID uint			`json:"id"`
	Age uint		`json:"age"`
	Name string		`json:"name"`
}

// 初始用户列表
var userList []User = []User{ {1, 19, "Mike"}, {2, 20, "Jack"} }

// 获取用户列表
func getAllUsers(c *gin.Context) {
	log.Println("Getting user list")
	c.JSON(http.StatusOK, userList)
}

// 查询用户
func queryUser(c *gin.Context) {
	id := c.Param("id")
	log.Printf("Querying user with id = %s", id)
	for _, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Found user: %v", u)
			c.JSON(http.StatusOK, u)
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{
		"message": "user not found.",
	})
}

// 创建用户
func createUser(c *gin.Context) {
	var name string
	var age uint

	if name_, ok := c.GetPostForm("name"); !ok {
		msg := "[name] field missed."
		log.Println(msg)
		c.JSON(http.StatusBadRequest, gin.H{"message": msg})
		return
	} else {
		name = name_
	}
	if age_, ok := c.GetPostForm("age"); !ok {
		msg := "[age] field missed."
		log.Println(msg)
		c.JSON(http.StatusBadRequest, gin.H{"message": msg})
		return
	} else {
		_age, err := strconv.Atoi(age_)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid age: " + age_})
			return
		}
		age = uint(_age)
	}
	// find last ID
	var lastID uint = userList[len(userList) - 1].ID
	// create user
	newUser := User{ID: lastID+1, Age: age, Name: name}
	userList = append(userList, newUser)
	c.JSON(http.StatusOK, gin.H{"message": "ok, new user id : " + strconv.Itoa(int(newUser.ID))})
	log.Printf("New user created, id = %d", newUser.ID)
}

// 修改用户
func modifyUser(c *gin.Context) {
	id := c.Param("id")
	var age int
	name := c.DefaultPostForm("name", "")
	_age := c.DefaultPostForm("age", "")
	if name == "" || _age == "" {
		c.JSON(http.StatusBadRequest, gin.H{"message": "[name] and [age] is required."})
		return
	}
	// string to int
	age, err := strconv.Atoi(_age)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid age: " + _age})
		return
	}
	log.Printf("Modify user with id = %s", id)
	for idx, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Found user: %v", u)
			u.Name = name
			u.Age = uint(age)
			userList[idx] = u
			log.Printf("User modified to: %v", u)
			c.JSON(http.StatusOK, gin.H{"message": "ok"})
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{
		"message": "user not found.",
	})
}

// 删除用户
func deleteUser(c *gin.Context) {
	id := c.Param("id")
	log.Printf("Querying user with id = %s", id)
	for idx, u := range userList {
		if strings.EqualFold(id, strconv.Itoa(int(u.ID))) {
			log.Printf("Deleting user: %v", u)
			for ; idx < len(userList) - 1; idx++ {
				userList[idx] = userList[idx+1]
			}
			userList = userList[:len(userList) - 1]
			c.JSON(http.StatusOK, gin.H{"message": "ok"})
			return
		}
	}
	log.Printf("User %s not found", id)
	c.JSON(http.StatusNotFound, gin.H{
		"message": "user not found.",
	})
}