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

背景

​

服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写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!: %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需要实现下面两个方法。

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服务还有很大的进步空间。

相关推荐
橙序员小站11 分钟前
Windows 安装包反复报错?一次排查补上了 5 个 CI 检查点
后端
陈随易15 分钟前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
SimonKing27 分钟前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
吴建旭 智宅焕1 小时前
智能家居全国交付中报价工具与交付基础设施的架构分离设计:接口化路径与交付确定性系统实现
架构·智能家居
张彦峰ZYF1 小时前
功能齐了不等于能上线:智能工单系统的AI回归测试、安全防护、成本可观测与CI/CD生产部署
后端·企业智能工单系统·评测线·安全线·成本线·部署线
Rain的Java大神之路1 小时前
LeetCode 37 解数独面试攻略:回溯 + 位掩码 + MRV,从暴力到毫秒级求解
java·后端·leetcode
你看我名字1 小时前
产品参数昨天还对,今天已过期:数眼智能与大模型应用如何做资料版本校验
开发语言·架构
量化分析码农1 小时前
【Python量化波动率实战 05】你的组合真的分散了吗:滚动相关与分散化收益
后端
xbzb1 小时前
Docker 架构与部署:一条 docker run 到底走了三层
docker·容器·架构·镜像·加速·daemon
IT_陈寒2 小时前
Vue这个奇葩的响应式问题让我加班到凌晨
前端·人工智能·后端