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

2023-12-21 00:05:27 浏览数 (1)

背景

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

HelloWorld

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

假如业务更多

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

代码语言:go复制
package main

import (
	"fmt"
	"net/http"
)

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

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

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

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

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

代码语言:go复制
type Server interface {
	Route(pattern string, handlerFunc http.HandlerFunc)

	Start(address string) error
}

简单实现一下。

代码语言:go复制
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,
	}
}

修改业务代码

代码语言:go复制
func main() {
	server := server.NewHttpServer("demo")
	server.Route("/", notfound)
	server.Route("/hello", hello)
	server.Route("/greet", greet)
	server.Start(":80")
}

格式化输入输出

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

代码语言:go复制
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
}

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

代码语言:go复制
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的返回结果实在是太普遍了,我们当然也可以进一步封装输出方法。但是我觉得没必要。

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

代码语言:go复制
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。

代码语言:go复制
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,
	}
}

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

代码语言:go复制
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的声明。

代码语言:go复制
type Server interface {
	Route(method string, pattern string, handlerFunc func(ctx *Context))
	Start(address string) error
}
代码语言:go复制
server.Route(http.MethodGet, "/hello", hello)

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

代码语言:go复制
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服务还有很大的进步空间。

0 人点赞