36

【轻知识】Go入门学习整理——第四节web开发,http请求处理流程、一个简单的名单录入。

 5 years ago
source link: https://studygolang.com/articles/20171?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

先从 hello world开始

关于web的开发知识,其实绕不过astaxie(beego作者)写的 《build-web-application-with-golang》 。我开发公司的beego项目之前看了一遍,现在再次拜读。

用http服务要引入net/http包。

我们看下那个启动http服务的方法是哪个。

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

ok ,接着看handler是什么鬼?

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter呢?

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

ok , 那也就是说有个结构体实现了ServeHTTP方法就可以传入到ListenAndServe中了,对吧?buddy!

package main

import (
    "fmt"
    "net/http"
)

type HttpHandler struct {
}
// 回到的参数名名自己随便命名对吧。w也好response也好。只要按照参数的格式写就OK。http.ResponseWriter,当然就是package http中的ResponseWriter这个结构体。Request也一样。
func (h HttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello world!") //这个写入到w的是输出到客户端的
}

func main() {
    h := new(HttpHandler)
    http.ListenAndServe(":9090", h)
}

get 请求 http://localhost:9090 ,ok没问题是 hello world!

但,我们的接口通常都是好几个对吧。比如,我要添加数据,我要查数据。走不同的接口。也即是根据不同的Path做不同的事情。OK,没问题。

package main

import (
    "fmt"
    "net/http"
)

type HttpHandler struct {
}

func search(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "select from db")
}
func add(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "insert into db")
}
func (h HttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/search":
        search(w, r)
    case "/add":
        add(w, r)
    default:
        fmt.Fprintf(w, "Hello world!")
    }
}

func main() {
    h := new(HttpHandler)
    http.ListenAndServe(":9090", h)
}

这样相当于我手动写了一个路由。那么有没有像laravel那样的路由方式呢?

Route::get('foo', function () {
    return 'Hello World';
});

http包里面有个HandleFunc函数可以实现。那把上面的代码改造下。

http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "select from db")
})
http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "select from db")
})
http.ListenAndServe(":9090", nil)

ok,这样分开之后看起来舒服多了。关于web服务的执行流程。astaxie写的文章可以琢磨下哈。

我是看明白了哈。简单的追下代码哈!主要是我想知道路由怎么分发的。

1.点入ListenAndServe

2.点入server.ListenAndServe()

3.点入 srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})

4.点入go c.serve(ctx)(函数中找哈)(这行代码就是高性能的点,开启协程)

5.然后你会发现这行代码serverHandler{c.server}.ServeHTTP(w, w.req)(就是执行了我们的配置的handler)继续点入ServeHTTP

ok。重点来了,路由如何分发的。奥妙就在ServeHTTP中。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

回顾下,我们上来先自定义的那个结构体了么?然后传给了ListenAndServe。实际上server保存了它。于是,在ServeHTTP方法中的体现就是这行代码 handler := sh.srv.Handler 。因为我传了所以不为nil。于是走handler.ServeHTTP(rw, req)。执行了我自定义的HttpHandler中的ServeHTTP。

OK,那么 handler == nil ,也就是handler为空呢?也就是我们用HandleFunc改造过的写法。ListenAndServe里面穿的nil。也就意味着server保存的handler为nil。于是在ServeHTTP方法中走这里 handler = DefaultServeMux

这里再提两点。剩下的自己追下代码。

1.HandleFunc 作用就是给DefaultServeMux配置了路由。保存到了map中。 2.DefaultServeMux 类型是*ServeMux。ServeMux里面实现了ServeHTTP(想起来了么,只要实现了ServeHTTP就能处理http请求与响应),实现的ServeHTTP方法里面根据path做了路由转发,也就是从之前保存的map里面拿出来对应的handler。最后执行。

更多看 3.4 Go的http包详解

来了?表单!

OK,接下来要做的就是,准备好简单的前端页面。一个框框(输入名字)一个按钮(提交),一个列表(成功后刷新列表)。姑且就叫做名单功能吧。

接着之前的代码哈。已经有了add方法(添加名单),search方法(展示列表)。当然还没有实现。还需要一个display方法(渲染页面)。三个方法就OK了。

功能

Br2Qjyz.png!web

image.png

1.包级作用域的nameList数组,存放名单。

2.页面加载请求search方法(也就是列表),添加把name添加到nameList中。

目录结构

─web
    └───views
              └─── list.html
    └───main.go

贴代码

先准备页面,js用vue。贴代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>名单列表</title>
    <script type="text/javascript" src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
</head>
<body>
    <div id="app">
        <div>
            <input type="text" placeholder="name" v-model="name"> <button @click="add()">add</button>
        </div>
        <div>
            <ul>
                <li v-for="value in nameList">
                    <p>ID: {{value.id}}</p>
                    <p>Name: {{value.name}}</p>
                </li>
            </ul>
        </div>
    </div>
    <script>
        Vue.http.options.emulateJSON = true;
        Vue.http.options.headers = {
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
        };  //Vue-resource中post请求将data数据以request payload转换为form data的形式
        var app = new Vue({
                el: '#app',
                mounted: function() {
                    let that = this
                    this.$http.post("/search", {}).then(function(res) {
                        that.nameList = JSON.parse(res.bodyText).name_list
                    })
                },
                data: {
                    name:'',
                    nameList: []
                },
                methods: {
                    add: function() {
                        let that = this
                        // 发送请求给 后台
                        this.$http.post("/add", {name: this.name}).then(function(res) {
                            this.$http.post("/search", {}).then(function(res) {
                                that.nameList = JSON.parse(res.bodyText).name_list
                            })

                        }) 
                    }
                }
        });
    </script>
</body>
</html>

贴go代码

package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "net/http"
)

var nameList []map[string]interface{}

func main() {
    http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) {
        pageFile := "./views/list.html"
        t, error := template.New("list.html").Delims("<<<", ">>>").ParseFiles(pageFile)
        if error != nil {
            http.Error(w, error.Error(), http.StatusInternalServerError)
        }
        t.Execute(w, nil)
    })
    http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
        data := map[string]interface{}{"name_list":nameList}
        list, err := json.Marshal(data)
        if err != nil {
            fmt.Println("json.Marshal failed:", err)
            return
        }
        w.Header().Add("Content-Type","application/json")
        fmt.Fprintf(w, string(list)) //这个写入到w的是输出到客户端的
    })
    http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(r.FormValue("name"))
        fmt.Println(r.Method)
        name := r.FormValue("name")
        if name == "" {
            fmt.Fprintf(w, "don't ")
            return 
        }
        var nameMap map[string]interface{} = map[string]interface{}{"id": len(nameList) + 1, "name": name}
        nameList = append(nameList, nameMap) // evaluated but not used 这种报错是需要接收返回值
        fmt.Println(nameList)
        fmt.Fprintf(w, "add from db")
    })
    http.ListenAndServe(":9090", nil)
}

总结,关于这个程序呢。有些不足之处,比如校验没做,比如错误处理没做。比如没有统一返回的函数与格式。对吧!但先到这里。

下一步要做的就是操作mysql或者再加redis。而不是放到包级作用域的数组中。

参考资料:

*《build-web-application-with-golang》 https://github.com/astaxie/build-web-application-with-golang

*《Go语言实战笔记(二十)| Go Context》 https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

*《Change default delimiters templating with “template.Delims”》 https://medium.com/@etiennerouzeaud/change-default-delimiters-templating-with-template-delims-857938a0b661

*《Vue-resource中post请求将data数据以request payload转换为form data的形式》 https://blog.csdn.net/qq_35844177/article/details/70170416


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK