背景
服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写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服务还有很大的进步空间。