[Golang] 简单Web服务到初级Web框架(Ⅰ)

要做什么

net/http库
  • Go语言语法基础
  • net/http库知识

最终实现的结构:
在这里插入图片描述
项目参考:https://github.com/flycash/toy-web @大明

最简单的HTTP服务

使用过Go语言开发就会知道,Go启动一个HTTP服务是十分简单的。

package main

import "net/http"

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

func Hello(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello Go Web"))
}
http://localhost:8088/hello

原生HTTP库实现注册功能

net/http

实现代码如下:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", Hello)
	http.HandleFunc("/SignUp", SignUp)
	http.ListenAndServe(":8088", nil)
}

func Hello(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello Go Web"))
}

type signUpReq struct {
	Email             string `json:"email"`
	Password          string `json:"password"`
	ConfirmedPassword string `json:"con"`
}

func SignUp(w http.ResponseWriter, r *http.Request) {
	req := &signUpReq{}

	// json
	body, err := io.ReadAll(r.Body)
	if nil != err {
		fmt.Fprintf(w, "read body error %v \n", err)
		return
	}
	err = json.Unmarshal(body, req)
	if nil != err {
		fmt.Fprintf(w, "json marshal errer %v", err)
		return
	}

	// form
	// err := r.ParseForm()
	// if nil != err {
	// 	w.WriteHeader(http.StatusBadRequest)
	// 	fmt.Println("表单解析失败")
	// 	return
	// }
	//email := r.FormValue("email")

	// 返回一个虚拟的表示注册成功
	fmt.Fprintf(w, "%d", err)
}

http://localhost:8088/SignUp

存在哪些问题?

现在我们可以看看,上面代码存在哪些问题:

  1. 没有响应码
  2. 不能返回格式化数据——比如json
  3. 仅仅只是解析传递过来的json,就需要那么多if else,有点冗余
  4. 强耦合了fmt包

上述问题是直观的问题,另外从从设计上来考虑,也有问题:

func (w http.ResponseWriter, r *http.Request)http.ListenAndServer()

解决问题

那么现在开始来解决问题。

针对响应码与序列化数据的

上面提到的问题,针对返回请求的响应码,并且能够返回序列化的json。

w.WriteHeader(http.StatutsOK)
type commonResponse struct {
	BizCode int         `json:"biz_code"`
	Msg     string      `json:"msg"`
	Data    interface{} `json:"data"`
}

// ······
resp := &commonResponse{
			BizCode: 4,
			Msg:     fmt.Sprintf("invalid request %v\n", err),
		}
respBytes, _ := json.Marshal(resp)
fmt.Fprintf(w, string(respBytes))
请求封装为Context
(w http.ResponseWriter, r *http.Request)
type Context struct {
	W http.ResponseWriter
	R *http.Request
}

// 封装出来之后,就需要有一个构造方法
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
	return &Context{
		W: w,
		R: r,
	}
}

这样操作带来什么好处呢?
内聚的东西,就允许我们当成一个整体来看了。从代码上的体现,就是能够针对Context对象定义出一系列方法(用来操作Request还有ResponseWrite)。
比如,我们上面提到的另外一个问题:只是简单读取出请求中的json数据,就需要if else 一大堆,十分冗余。那是因为代码的抽象不够,现在抽象出一个“请求”的对象,那么我们可以使用下面操作,为这个Context定义一些方法,将某些动作如解析,校验,返回等,封装起来。

// 返回对象
type commonResponse struct {
	BizCode int         `json:"biz_code"`
	Msg     string      `json:"msg"`
	Data    interface{} `json:"data"`
}

// 从json中读取信息
func (c *Context) ReadJson(data interface{}) error {
	body, err := io.ReadAll(c.R.Body)
	if nil != err {
		return err
	}
	return json.Unmarshal(body, data)
}

// 返回json
func (c *Context) WriteJson(states int, data interface{}) error {
	bs, err := json.Marshal(data) 
	if err != nil {
		return err
	}
	_, err = c.W.Write(bs)
	if nil != err {
		return err
	}
	c.W.WriteHeader(states)
	return nil
}

另外,为了调用方便,还是可以进一步封装一些辅助方法:

func (c *Context) OkJson(data interface{}) error {
	return c.WriteJson(http.StatusOK, data)
}

func (c *Context) SystemErrJson(data interface{}) error {
	return c.WriteJson(http.StatusInternalServerError, data)
}

func (c *Context) BadRequestJson(data interface{}) error {
	return c.WriteJson(http.StatusBadRequest, data)
}

另外,不得不提一句的是,Context到底需不需要抽象为一个接口呢? 看上去好像可以抽象。
这里的考虑其实是取决于框架开发者,抽不抽象出来表示你愿不愿意别人使用你这个框架的时候,自定义自己的Context。
(目前代码并没有抽象出Context接口 😃 )

将服务抽象为Server
http.WriteHeader()json包Context

现在剩下最后一个问题:

gin.Default()

怎么实现呢?上面出现的关键词就是 : 内聚、路由、监听。
还是一样,内聚封装,定义方法。

type HttpServer struct {
	// Name 标识字段
	Name string

	// 拓展别的字段······
}

// 路由
func (s *HttpServer) Route(pattern string, handlerfunc http.HandlerFunc) {
	http.HandleFunc(pattern, handlerfunc)
}
// 监听
func (s *HttpServer) Start(address string) error {
	return http.ListenAndServe(address, nil)
}

这里定义一个接口:

type Server interface {
	// Route 设定路由,直接执行handerfunec
	Route(pattern string, hendlerFunc http.HandlerFunc)
	// strat 启动服务器的指令
	Start(address string) error
}

只要实现了这两个方法,就实现了Server的接口——允许使用者自定义Server。

修改后代码

package main

import (
	"encoding/json"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", Hello)
	// http.HandleFunc("/SignUp", SignUp)
	s := NewSignServer("web1")
	s.Route("/SignUp", SignUp)
	s.Start(":8089")
}

func Hello(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello Go Web"))
}

type signUpReq struct {
	Email             string `json:"email"`
	Password          string `json:"password"`
	ConfirmedPassword string `json:"con"`
}

// -----------------------------增加Server-------------------------------
type Server interface {
	// Route 设定路由
	// Route(pattern string, hendlerFunc http.HandlerFunc)
	Route(pattern string, hendlerFunc func(c *Context))
	// strat 启动服务器的指令
	Start(address string) error
}

type HttpServer struct {
	// Name 标识字段
	Name string

	// 拓展别的字段······
}

// 路由
func (s *HttpServer) Route(pattern string, handlerfunc func(c *Context)) {
	// http.HandleFunc(pattern, handlerfunc)
	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { //这里的参数应该怎么传参?
		c := NewContext(w, r)
		handlerfunc(c) 
	})
}

// 监听
func (s *HttpServer) Start(address string) error {
	return http.ListenAndServe(address, nil)
}

// 构造
func NewSignServer(name string) Server {
	return &HttpServer{
		Name: name,
	}
}

// ---------------------------------------------------------------------

// -----------------------------增加Context-------------------------------

type Context struct {
	W http.ResponseWriter
	R *http.Request
}

// 封装出来之后,就需要有一个构造方法
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
	return &Context{
		W: w,
		R: r,
	}
}

// 返回对象
type commonResponse struct {
	BizCode int         `json:"biz_code"`
	Msg     string      `json:"msg"`
	Data    interface{} `json:"data"`
}

// 从json中读取信息
func (c *Context) ReadJson(data interface{}) error {
	body, err := io.ReadAll(c.R.Body)
	if nil != err {
		return err
	}
	return json.Unmarshal(body, data)
}

// 返回json
func (c *Context) WriteJson(states int, data interface{}) error {
	bs, err := json.Marshal(data)
	if err != nil {
		return err
	}
	_, err = c.W.Write(bs)
	if nil != err {
		return err
	}
	c.W.WriteHeader(states)
	return nil
}

func (c *Context) OkJson(data interface{}) error {
	return c.WriteJson(http.StatusOK, data)
}

func (c *Context) SystemErrJson(data interface{}) error {
	return c.WriteJson(http.StatusInternalServerError, data)
}

func (c *Context) BadRequestJson(data interface{}) error {
	return c.WriteJson(http.StatusBadRequest, data)
}

// ---------------------------------------------------------------------

// 这个实现不连贯(w,r)
// func SignUp(w http.ResponseWriter, r *http.Request) {
// 	c := NewContext(w, r)
// 	req := signUpReq{}
// 	err := c.ReadJson(req)
// 	if nil != err {
// 		_ = c.BadRequestJson(&commonResponse{
// 			BizCode: 4,
// 			Msg:     "invaild request",
// 		})
// 		return
// 	}
// 	_ = c.OkJson(&commonResponse{
// 		Data: 1234,
// 	})
// }
func SignUp(c *Context) {
	req := signUpReq{}
	err := c.ReadJson(req)
	if nil != err {
		_ = c.BadRequestJson(&commonResponse{
			BizCode: 4,
			Msg:     "invaild request",
		})
		return
	}
	_ = c.OkJson(&commonResponse{
		Data: 1234,
	})
}

可以把Context的内容放在另外一个文件中调用,这里为了方便,就都写在一个文件了。
上面的代码其实还有点问题需要进一步抽象与修改,可以找找看 :) 。

func (w http.ResponseWriter, r *http.Request)func (c *Context)

下一篇的内容(Ⅱ)

  • 上面代码还存在的问题就是抽象度还不够——从上层往下看,请求的GET、POST怎么识别与处理。
  • Gin相关的路由树操作
  • 中间件与Hook函数等等