从零开始写一个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服务还有很大的进步空间。

相关推荐
祥哥的说19 分钟前
万字深度解析 OpenClaw 架构:为什么它能成为全球最火的开源 AI Agent?
人工智能·架构·开源·openclaw
cyber_两只龙宝24 分钟前
【MySQL】MySQL主从复制架构
linux·运维·数据库·mysql·云原生·架构
树獭叔叔26 分钟前
扩散模型完全指南:从直觉到数学的深度解析
后端·aigc·openai
毕设源码_严学姐28 分钟前
计算机毕业设计springboot心理健康辅导系统 高校学生心灵关怀服务平台的设计与实现 校园智慧心理服务系统的设计与实现
spring boot·后端·课程设计
程序员牛奶28 分钟前
如何使用Redis Set实现简单的抽奖系统?
后端
程序员海军29 分钟前
深度测评:在微信里直接操控 OpenClaw
人工智能·后端
野犬寒鸦37 分钟前
面试常问:HTTP 1.0 VS HTTP 2.0 VS HTTP 3.0 的核心区别及底层实现逻辑
服务器·开发语言·网络·后端·面试
砍材农夫41 分钟前
多层缓存设计
后端
来了老板42 分钟前
超越日志与权限:深度解析Python装饰器原理与高阶实战场景
后端
兆子龙1 小时前
antd 组件也做了同款效果!深入源码看设计模式在前端组件库的应用
java·前端·架构