注册

从零开始写一个web服务到底有多难?

背景



服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写web服务到底有多难?


HelloWorld


官网给出的helloworld例子。http标准库提供了两个方法,HandleFunc注册处理方法和ListenAndServe启动侦听接口。


请在此添加图片描述


请在此添加图片描述


假如业务更多


下面我们模拟一下接口增多的情况。可以看出有大量重复的部分。这样自然而然就产生了抽象服务的需求。


package main

import (
"fmt"
"net/http"
)

func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Greet!: %s\n", r.URL.Path)
}

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!: %s\n", r.URL.Path)
}

func notfound(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}

func main() {
http.HandleFunc("/", notfound)
http.HandleFunc("/hello", hello)
http.HandleFunc("/greet", greet)
http.ListenAndServe(":80", nil)
}

我们想要一个服务,它代表的是对某个端口监听的实例,它可以根据访问的路径,调用对应的方法。在需要的时候,我可以生成多个服务实例,监听多个端口。那么我们的Server需要实现下面两个方法。


type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)

Start(address string) error
}

简单实现一下。


package server

import "net/http"

type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}

type httpServer struct {
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)
}

func NewHttpServer(name string) Server {
return &httpServer{
Name: name,
}
}

修改业务代码


func main() {
server := server.NewHttpServer("demo")
server.Route("/", notfound)
server.Route("/hello", hello)
server.Route("/greet", greet)
server.Start(":80")
}

格式化输入输出


在我们实际使用过程中,输入输出一般都是以json的格式。自然也需要通用的处理过程。


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

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

func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}

模拟了一个常见的业务代码。定义了入参和出参。


type helloReq struct {
Name string
Age string
}

type helloResp struct {
Data string
}

func hello(w http.ResponseWriter, r *http.Request) {
req := &helloReq{}
ctx := &server.Context{
W: w,
R: r,
}

err := ctx.ReadJson(req)

if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

}

用postman试一下,是不是和我们平常开发的接口有一点像了。


请在此添加图片描述


由于200,404,500的返回结果实在是太普遍了,我们当然也可以进一步封装输出方法。但是我觉得没必要。


在我们设计的过程中,是否要提供辅助性的方法,还是只聚焦核心功能,是非常值得考虑的问题。


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

func (c *Context) NotFoundJson(resp interface{}) error {
return c.WriteJson(http.StatusNotFound, resp)
}

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


让框架来创建Context


观察下业务代码,还有个非常让人不舒服的地方。Context是框架内部使用的数据结构,居然要业务来创建!真的是太不合理了。


那么下面我们把Context移入框架内部创建,同时业务侧提供的handlefunction入参应该直接是由框架创建的Context。


首先修改我们的路由注册接口的定义。在实现中,我们注册了一个匿名函数,在其中构建了ctx的实例,并调用入参中业务的handlerFunc。


type Server interface {
Route(pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

func (s *httpServer) Route(pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
W: w,
R: r,
}
}

这样修改之后我们的业务代码也显得更干净了。


func hello(ctx *server.Context) {
req := &helloReq{}
err := ctx.ReadJson(req)

if err != nil {
ctx.ServerErrorJson(err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
ctx.ServerErrorJson(err)
return
}
}

RestFul API 实现


当然我们现在发现,不管用什么方法调用我们的接口,都可以正常返回。但是我们平常都习惯写restful风格的接口。


那么在注册路由时,自然需要加上一个method的参数。注册时候也加上一个GET的声明。


type Server interface {
Route(method string, pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

server.Route(http.MethodGet, "/hello", hello)

那么我们自然可以这样写,当请求方法不等于我们注册方法时,返回error。


func (s *httpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
w.Write([]byte("error"))
return
}
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

那么我们现在就有了一个非常简单的可以实现restful api的服务了。


但是距离一个好用好写的web服务还有很大的进步空间。


作者:4cos90
来源:juejin.cn/post/7314902560405684251

0 个评论

要回复文章请先登录注册