美文网首页
Go web学习(二)

Go web学习(二)

作者: 非典型_程序员 | 来源:发表于2018-10-27 16:05 被阅读0次

    今天继续学习go web方面的知识,主要是熟悉一下go语言中常用的一些知识点,上次主要是通过自定义一个路由进行用户请求转发,今天会加上和数据库交互的知识点,以及web开发中常用的一些功能,比如图片上传(使用multipart-form进行post提交)以及保存到本地,最后返回该图片;还有就是接收用户url请求参数,根据参数返回相应的结果;最后就是使用接收用户request body中提交json类型参数,进行处理,最终返回json格式的自定义响应结果。现在开发中基本都是前后端分离,后端需要返回给前端的参数一般也都使用json格式,很多时候都需要自己来定义这样一个数据结构,我们看下go中是如何使用的,看看它和java有上面不同。数据库的话我还是继续使用postgres,数据库驱动使用的开源的“github.com/lib/pq”。好了,下面正式开始今天的学习,继续打开上次的项目。

    首先我们先学习下怎么连接数据库,go提供了sql.Open()来获取数据库的连接池,这个方法需要两个参数一个是数据驱动名称,还有一个就是数据源。我们看下它的源码吧,方法注释写的很清楚了,我就不再多说了:

    2018-10-27 10-44-40 的屏幕截图.png

    github.com/lib/pq驱动名称就是"postgres",它有两种方式来获取数据库连接,我使用的是URL形式,也可以采用其他形式,可以看下它github官方给文文档:https://github.com/lib/pq 首先建一个database文件夹,里面是数据库相关的代码,代码如下:

     func GetConn() *sql.DB {
         sqlURL := "postgres://postgres:123456@localhost/pgsql?sslmode=disable"
         fmt.Println("----> get postgresql connection <----")
         db, err := sql.Open("postgres", sqlURL)
         checkErr(err, "-----> open datasources failed <-----")
    
         // 返回数据库连接
         return db
    }
    

    sqlURL中"postgres:123456",分别对应数据库的用户名和密码,"pgsql"是数据库的名字,最后表示禁用SSL模式。通过这个方法我们拿到数据库的连接实例以后就可以对数据库进行增删改查了。
    一、用户表单数据提交,并存储到数据库:
    接下来我在template文件夹下新加几个html文件,让用户同感form表单的形式提交用户信息,然后将信息保存到数据库。修改下路由器,并添加几个handler处理用户请求,代码如下:

    func (mux *CustomMux) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
        switch request.URL.Path {
        case "/":
            IndexHandler(writer, request)
            return
        case "/home":
            HomePageHandler(writer, request)
            return
        case "/login":
            LoginHandler(writer, request)
            return
         // 新增handler 
        case "/add":
            RenderAddHandler(writer, request)
            return
        case "/user/insert":
            InsertUserHandler(writer, request)
            return
        default:
            http.NotFound(writer, request)
        }
    }
    

    "/add"是跳转到用户输入页面的,用户输入信息,然后提交表单,由"/user/insert"来处理用户传递的数据,handler代码如下:

    // 跳转新加用户页面
    func RenderAddHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> render to insert page handler start <-----")
        t, err := template.ParseFiles("template/user/addUser.html")
        checkErr(err)
        t.Execute(writer, nil)
    }
    // 添加新用户
    func InsertUserHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> insert new user handler start <-----")
        // 获取表单参数
        username := request.PostFormValue("username")
        password := request.PostFormValue("password")
        age := request.PostFormValue("age")
        mobile := request.PostFormValue("mobile")
        address := request.PostFormValue("address")
        //省略校验逻辑....
    
        // 插入数据库
        db := database.GetConn()
        insert := "insert into t_user (username,password,address,age,mobile,status) values($1,$2,$3,$4,$5,$6)"
        //stmt,err := db.Prepare(insert)
        //checkErr(err)
        //rs,err := stmt.Exec(username,password,address,age,mobile,1)
        //checkErr(err)
        //checkErr(stmt.Close())
        rs, err := db.Exec(insert, username, password, address, age, mobile, 1)
        checkErr(err)
        row, err := rs.RowsAffected()
        checkErr(err)
        fmt.Println("----> insert account = " + strconv.FormatInt(row, 10) + " <----")
        if row != 1 {
            log.Fatal("----> error occurred <----")
        } else {
            fmt.Println("----> insert user to database succeed <----")
        }
        //关闭数据库连接
        checkErr(db.Close())
    }
    

    跳转页面的handler就不再细说了,主要说下插入用户方法里面和数据库相关的内容,首先获取数据库连接实例,sql语句是需要手写的,这一点很像JDBC(不过JDBC我也忘的差不多了),插入的数据按照顺序使用"$"占位符表示,下面注释掉的代码和JDBC也很像,db.Prepare(insert)先获取到 prepare statemen对象,然后执行stmt.Exec("参数"…),但是执行操之后必须要关闭prepare statemen,所以最后需要调用stmt.Close()。但是我直接使用的db.Exec()方法,传入需要执行的sql语句和相应的参数就可以了,熟悉JDBC的话应该很好理解。最后db.Exec()返回的结果result是一个接口类型,它有两个方法一个是:LastInsertId() (int64, error),另一个是:RowsAffected() (int64, error)。根据名称很好理解,就是返回上次插入数据的id值(postgres不支持,mysql是支持的),还有一个就是执行的行数。我获取到了执行影响的行数,并判断是不是等于1。这里我觉得有点问题就是事务的问题,如果我插入一条数据,但是影响的行数却不是1的话肯定出错了,按理应该进行回滚,插入的数据应该作废的,这里省略了(关于事务相关内容后面再学习吧)。最后是关闭数据库连接,其实是没有必要的,sql.Open()的注释也说了,很少需要关闭数据库连接。

    二、获取UEL请求参数,并将数据返回 在springmvc框架的controller里特别喜欢使用restful格式的参数,比如:"/user/select/{id}"这种形式,配合"@PathVariable"注解就可以获取对应的参数值,而vert.x使用的是"/user/select/:id"这种形式,其实哪种形式都无所谓,关键是能获取到,但是我看了下go好像不支持这种形式,所以我就使用自带的restful工具测试了,使用request header传递参数,然后后台看下如何接收URL上的参数。
    新增一个"/user/select" handler来接收请求url上的参数,并返回根据请求参数查询的结果,代码如下:

    func QueryByIdHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> query by id handler start <-----")
        fmt.Println(request.URL.RawQuery)
        params := request.URL.Query()
        id := params["id"][0]
        //id := 1
        //query := request.URL.Query()
        //id := query["id"][0]
        fmt.Println(id)
        db := database.GetConn()
        query := "select username,sex,age,address,mobile,role from t_user where id = $1"
        //rows := db.QueryRow(query,id)
        rows, err := db.Query(query, id)
        checkErr(err)
        // 获取用户信息
        var user model.User
        for rows.Next() {
            //var username string
            //var sex string
            //var age int
            //var address string
            //var mobile string
            //var role string
            //err = rows.Scan(&username,&sex,&age,&address,&mobile,&role)
    
            err = rows.Scan(&user.Username, &user.Sex, &user.Age, &user.Address, &user.Mobile, &user.Role)
            checkErr(err)
        }
        // 将用户数据写回页面
        t, _ := template.ParseFiles("template/user/userDetail.html")
        t.Execute(writer, user)
    
        // 返回json数据
        //writer.Header().Set("content-type","application/json")
    //json.NewEncoder(writer).Encode(user)
    }
    

    一debug模式启动项目,并使用goLand自带的rest client请求访问,在request header里面添加请求的参数:id=1,我们看到是这样的:

    2018-10-27 12-29-39 的屏幕截图.png

    request header里面的参数会自动添加到请求的URL上,并且会自动添加上"?",多个参数也会以"&"分割。根据上图可以看到请求参数是放到RawQuery上面的,然后我们怎么能把传递参数的key和value对应起来呢?就是requet.URL.Query(),看下源码和注释就很好理解了

    2018-10-27 12-36-56 的屏幕截图.png

    也就是说requet.URL.Query()方法会将RawQuery转换成Values,而Values是一个以string为key,string数组为value的map对象,这样就很容易获取到我们的参数值了。
    然后根据参数值执行查询我们的数据库,db.QueryRow()和db.Query()这两个方法都会返回查询的结果,但是有一个很重要的区别:db.QueryRow()最多返回一条数据,如果有多条那么它只返回第一条,其他的会被忽略调,并且它的返回结果永远是"non-nill",如果没用数据那么会返回"ErrNoRows"。具体使用哪个方法就看具体情况了。
    因为我使用了db.Query(),意味着可能返回多条数据,所以我需要使用一个循环来获取对应的值(不知道有没有其他好方法)。获取值有一个很不一样的地方,就是使用rows.Scan()方法。这个方法会将查询结果返回的值复制到Scan("地址")里面参数指向的值上去,而且数量必须一致。在我这个例子里面我查询了6列数据的值,那么Scan()方法参数值也必须为6个,所以最开始我定义了6个变量来接收这些值,但是后来想想为什么不定义一个model来接收呢,所以后来定义了一个"user"变量,所以传递进去的是user对象各字段的地址(到这里就感觉到使用ORM的好处了)。
    最后返回到一个页面,并显示查询到的用户信息,t.Execute(writer, user),这样就将user传递到页面上,这也是为什么我使用对象来接收参数的原因。HTML页面如下:

    2018-10-27 13-03-29 的屏幕截图.png

    页面就是将输入页面简单改了一下,然后我们启动项目,并使用rest client进行访问看下运行结果如下:

    2018-10-27 13-06-31 的屏幕截图.png

    之所以没在浏览器打开是因为浏览器不能编辑传递参数,而如果使用显式的http://localhost:9090/user/select?id=1访问,路由器的"/user/select"无法处理,等有时间看使用通配符能不能匹配吧,所以代码里面显式声明了一个id=1。这次使用浏览器访问,结果显示没有问题:

    2018-10-27 13-14-04 的屏幕截图.png

    当然我也完全可以将user以json格式返回,使用json.NewEncoder(writer).Encode("对象"),也就是使用go自带的json工具将user转成json数据,我们再次使用rest clinet再访问一次:

    2018-10-27 13-18-03 的屏幕截图.png

    三、使用multipar-form实现图片上传 还是添加相应的handler,"/upload/picture"跳转到上传图片的html页面,"/do/upload"处理上传图片逻辑,即保存图片,后返回图片的URL,并在页面展示。
    代码如下:

    // 跳转上传图片
    func UploadPictureHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> forward to picture file upload handler start <-----")
        t, err := template.ParseFiles("template/upload.html")
        checkErr(err)
        t.Execute(writer, nil)
    }
    // 处理上传逻辑
    func DoUploadActionHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> upload picture handler start <-----")
        if request.Method == "GET" {
        t, _ := template.ParseFiles("template/error.html")
        t.Execute(writer, request)
        } else {
            file1, head, err := request.FormFile("picture1")
            checkErr(err)
            fmt.Println("----> filename: " + head.Filename + " <----")
            defer file1.Close()
    
            fileBytes, err := ioutil.ReadAll(file1)
            checkErr(err)
            // 图片类型
            fileType := http.DetectContentType(fileBytes)
            fmt.Println(fileType)
    
            // 创建存放图片文件夹
            dest := "/home/ypcfly/ypcfly/upload/"
            exist := dirExist(dest)
            if exist {
                fmt.Println("----> directory has exist <----")
            } else {
                error := os.Mkdir(dest, os.ModePerm)
                checkErr(error)
            }
    
            newFile, err := os.Create(dest + head.Filename)
            checkErr(err)
            defer newFile.Close()
            len, err := newFile.Write(fileBytes)
            if err != nil {
                fmt.Println("----> error occurred while write file to disk <----")
            }
    
            fmt.Println(len)
            http.ServeFile(writer,request,dest + head.Filename)
            //writer.Header().Set("content-type","application/json")
            //json.NewEncoder(writer).Encode("imageURL:" + dest + head.Filename)
        }
    }
    // 文件是否存在
    func dirExist(s string) bool {
        var exist = true
        if _, err := os.Stat(s); os.IsNotExist(err) {
        exist = false
        }
        return exist
    }
    

    主要说下上传的逻辑,go获取参数尤其是form参数有好几个形式,有Form,PostForm,还有MultipartForm,感觉都差不多,用法上感觉大同小异,有空的话还是在学下源码吧,这里就不细说了,我这里使用的FormFile(),它其实下面还是调用了MultipartForm的内容,这个方法有三个返回值,一个是对应的文件对象,文件头和错误信息,拿到文件以后我们就可以对文件进行读取,然后再写入到新的文件了。读取文件我使用的是ioutil.ReadAll("文件"),然后将二进制数据写入到新的文件。再写入文件之前我创建一个文件夹来存放上传图片,并判断文件夹是否存在,不存在则新建对应的文件夹,这里说下os.Mkdir(),这个方法需要两个参数,第一个是文件夹名称,第二个是权限。关于权限这一点我还不是特别的理解,os.ModePerm常量值为"0777",在linux上修改文件权限的时候有时候会用到"chmod 777"这个命令,其实就是一个意思,有兴趣的同学请移步:http://chen498402552-163-com.iteye.com/blog/1164407,查看相关说明。
    接下来是创建新的文件,其实我们一般会重新命名图片,比如使用UUID,或者时间戳或者其他方式,我这里使用的依然是原文件名称保存,最后将二进制数据写入新的文件,这样新的图片就完成了。注意一点就是defer的使用,这是和java很不同的地方,go中异常处理没用try…catch…finally,这里使用defer file.Close(),我的感觉是避免对文件进行操作过程中发生异常或者错误信息,而这时文件或者文件流没有关闭,defer就保证了即使出现异常信息也保证对应的资源是关闭的,在这里感觉和java中的finally很相似,不知道理解的对不对。最后使用http.ServeFile()显示上传的图片。后来发现可以直接使用io.Copy()来复制文件内容,不需要读写二进制数据。
    最后再次启动项目,完成上传图片操作后看会是怎么样的:

    2018-10-27 14-48-52 的屏幕截图.png

    可以看到response headers的"Content-Type"为image/jpg。

    四、接收request body提交的参数 有时候我们会使用body来提交一些参数,比如我上次遇到接收base64格式图片的问题,我们看使用go怎么接收,前面我们分别使用了Form和URL的相关形式来收表单数据和URL参数。因为接收的是json格式的数据,所以我准备自定义一个model作为自定义响应结果,在model文件夹下创建自定义对象,代码如下:

    // 自定义返回结果
    type ComRes struct {
        Code    string
        Success bool
        Message string
    }
    

    分别表示状态码,bool类型结果和返回消息三部分。
    在路由器添加相关的路由和handler映射关系,即"/json/param",代码这里就省略了,然后创建对应的handler,代码如下:

    // 请求参数是json类型
    func JsonHandler(writer http.ResponseWriter, request *http.Request) {
        log.Println("-----> request json param handler start <-----")
        bytes, err := ioutil.ReadAll(request.Body)
        checkErr(err)
        var user model.User
        //讲json参数和对应的model进行映射
        error := json.Unmarshal(bytes, &user)
        checkErr(error)
    
        // 保存到数据库
        db := database.GetConn()
        insert := "insert into t_user (username,password,address,age,mobile,sex,status) values($1,$2,$3,$4,$5,$6,$7)"
        rs, _ := db.Exec(insert, user.Username, user.Password, user.Address, user.Age, user.Mobile, user.Sex, 1)
        count, _ := rs.RowsAffected()
        // 自定义响应对象
        var res model.ComRes
        if count != 1 {
            res.Code = "0002"
            res.Success = false
            res.Message = "insert to database failed"
        } else {
            res.Code = "0001"
            res.Success = true
            res.Message = "insert to database success"
        }
        // 返回自定义的响应结果
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(res)
    }
    

    request.Body对象对应的就是用户请求的request body。request.Body类型为io.ReadCloser,所以选择使用ioutil.ReadAll()读取request body提交的参数,返回的是二进制数据,然后使用go自带的json工具将json格式数据进行转化,并存储到对应地址的对象中,因为我request body提交的是一个json格式的user信息,所以继续我用user对象接收。拿到用户信息后,将相关信息插入到数据库,然后根据受影响的行数判断成功还是失败,最后再使用json工具将我们自定义的res变量,编码成json格式并返回。启动项目测试一下:

    2018-10-27 15-19-12 的屏幕截图.png

    OK,结果也是json格式的数据,没有问题。

    五:总结

    其实这次学习还是学到了不少基础相关的知识,尤其是文件读写,感觉封装的比较好,但是有些参数自己也是云里雾里,很多时候都要去看源码的注释,没有注释就懵了,特别像涉及到文件权限的时候,这也说明自己的基础知识还有待加强。我觉得不管学什么语言,基础真的是很重要,一定要重视。不过从目前来看至少我觉得有一点不方便的问题就是数据库操作,比如事务,事务的重要性无需多言,但是事务的操作我还没学习到。另外就是ORM,在和数据库交互的时候如果字段很多将会非常的不方便,需要自己一个个的去做映射,后面有时间看有没有相关的框架,自己再去学下一下,不过我觉得还是有必要在继续学习基础知识,比如defer panic error区别,异常恢复等等。

    关于这次的代码已经提交到我的github,地址:https://github.com/ypcfly/learnGo
    如果文章之中有什么错误之处欢迎指正,也希望大家能够互相的学习交流,谢谢!

    相关文章

      网友评论

          本文标题:Go web学习(二)

          本文链接:https://www.haomeiwen.com/subject/egydtqtx.html