27

Go语言10-http和mysql

 5 years ago
source link: https://studygolang.com/articles/16482?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.

http 编程

Go 原生支持http:

import "net/http"

Go 的http服务性能和nginx比较接近:

就是说用Go写的Web程序上线,程序前面不需要再部署nginx的Web服务器,这里省掉的是Web服务器。如果服务器上部署了多个Web应用,还是需要反向代理的,一般这也是nginx或apache。

几行代码就可以实现一个web服务:

package main

import (
    "fmt"
    "net/http"
)

func Hello(w http.ResponseWriter, r *http.Request) {
    fmt.Println(*r)
    fmt.Fprintf(w, "Hello World")
}

func main() {
    http.HandleFunc("/", Hello)
    err := http.ListenAndServe("0.0.0.0:8000", nil)
    if err != nil {
        fmt.Println("http Listen failed")
    }
}

http client

http 常见的请求方法:

  • Get请求
  • Post请求
  • Put请求
  • Delete请求
  • Head请求

Get 请求

使用Get请求网站的示例:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    res, err := http.Get("http://edu.51cto.com")
    if err != nil {
        fmt.Println("http get ERRPR:", err)
        return
    }
    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        fmt.Println("get data ERROR:", err)
        return
    }
    fmt.Println(string(data))
}

Head请求

Head请求只返回响应头。如果只想要获取一些状态信息的话,可以用Head请求。这样避免返回响应体,响应体的数据是比较多的,适合做监控。Head请求的示例:

package main

import (
    "fmt"
    "net/http"
)

var urls = []string{
    "http://×××w.baidu.com",
    "http://×××w.google.com",
    "http://×××w.sina.com.cn",
    "http://×××w.163.com",
}

func main() {
    for _, v := range urls {
        resp, err := http.Head(v)
        if err != nil {
            fmt.Println("Head request ERROR:", err)
            continue
        }
        fmt.Println(resp.Status)
    }
}

http 常见状态码

http.StatusContinue = 100

http.StatusOK = 200

http.StatusFound = 302 跳转

http.StatusBadRequest = 400 非法请求

http.StatusUnanthorized = 401 没有权限

http.StatusForbidden = 403 禁止访问

http.Status.NotFound = 404 页面不存在

http.StatusInternalServerError = 500 内部错误

处理form表单

package main

import (
    "fmt"
    "io"
    "net/http"
)

const form = `
<html>
<body>
<form action="#" method="post" name="bar">
    <input type="text" name="in" />
    <input type="text" name="in" />
    <input type="submit" value="Submit" />
</form>
</body>
</html>`

func FormServer(w http.ResponseWriter, request *http.Request) {
    w.Header().Set("content-Type", "text/html")
    switch request.Method {
    case "GET":
        io.WriteString(w, form)
    case "POST":
        request.ParseForm()
        io.WriteString(w, request.Form["in"][0])  // 注意上面的2个input的name是一样的
        io.WriteString(w, request.Form["in"][1])  // 所以这是一个数组
        io.WriteString(w, "</br>")
        io.WriteString(w, request.FormValue("in"))  // 一般去一个值,就用这个方法
    }
}

func main() {
    http.HandleFunc("/form", FormServer)
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println("监听端口ERROR:", err)
    }
}

panic 处理

如果处理函数里有panic,会导致整个程序崩溃,所以要 defer revoer() 来处理 panic。在处理函数开头defer一个匿名函数:

func FormServer(w http.ResponseWriter, request *http.Request) {
    // 增加一个defer来处理panic
    defer func() {
        if x := recover(); x != nil {
            log.Println(request.RemoteAddr, "捕获到异常:", x)
        }
    }()
    // 原本的处理函数的内容
    w.Header().Set("content-Type", "text/html")
    switch request.Method {
    case "GET":
        io.WriteString(w, form)
    case "POST":
        request.ParseForm()
        io.WriteString(w, request.FormValue("in"))  // 一般去一个值,就用这个方法
    }
    // 搞个panic出来
    zero := 0
    tmp := 1 / zero
    io.WriteString(w, string(tmp))
}

优化统一处理

按照上面的做法,要在每个处理函数的开头都加上panic的处理。由于每个处理函数的panic处理方法都一样,所以可以写一个自定义的处理函数:

// 自定义的panic处理的函数
func logPanics(handle http.HandlerFunc) http.HandlerFunc {
    return func(writer http.ResponseWriter, request *http.Request) {
        defer func() {
            if x := recover(); x != nil {
                log.Println(request.RemoteAddr, "捕获到异常:", x)
            }
        }()
        // 上面先处理panic,再接着下面调用业务逻辑
        handle(writer, request)
    }
}

func main() {
    // http.HandleFunc("/form", FormServer)  // 修改调用处理函数的方法
    http.HandleFunc("/form", logPanics(FormServer))  // 把处理函数传给自己写的封装了panic处理的函数里
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println("监听端口ERROR:", err)
    }
}

原本直接调用处理函数。现在调用自定义的函数,把处理函数传进去。在自定义的函数里先加载defer,然后再调用执行原本的处理函数。逻辑很简单,就是把处理函数作为参数传给自定义的函数,在自定义的函数里再调用处理函数。在自定义的函数里写上defer,这样就相当于所有的处理函数都有defer了。

模板

使用模板需要用到 "text/template" 包。然后调用模板的t.Execute()方法输出。

替换

先准备一个简单的模板:

<p>Hello {{.Name}}</p>
<p>Age: {{.Age}}</p>

然后在Go里使用模板:

package main

import (
    "fmt"
    "os"
    "text/template"
)

type Person struct {
    Name string
    Age int
}

func main() {
    t, err := template.ParseFiles("index.html")
    if err != nil {
        fmt.Println("模板解析异常:", err)
        return
    }
    p := Person{"Bob", 32}
    if err := t.Execute(os.Stdout, p); err != nil {
        fmt.Println("模板加载数据异常:", err)
    }
}

/* 执行结果
PS H:\Go\src\go_dev\day10\http\use_template> go run main.go
<p>Hello Bob</p>
<p>Age: 32</p>
PS H:\Go\src\go_dev\day10\http\use_template>
*/

如果直接用 {{.}} 不加字段名的话,就是输出结构体打印的效果。

输出到浏览器里

要输出到浏览器里,只需要在 t.Execute(os.Stdout, p) 里,把原本输出到终端换成输出到处理函数的 w http.ResponseWriter 类型,就好了。

html模板的内容不变,下面是go的代码:

package main

import (
    "fmt"
    "net/http"
    "text/template"
)

func Hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

type Person struct {
    Name string
    Age int
}

func Index(w http.ResponseWriter, r *http.Request) {
    p := Person{"Cara", 18}
    t, err := template.ParseFiles("index.html")
    if err != nil {
        fmt.Println("加载模板ERROR:", err)
        return
    }
    t.Execute(w, p)
}

func main() {
    http.HandleFunc("/", Hello)
    http.HandleFunc("/index", Index)
    err := http.ListenAndServe("0.0.0.0:8000", nil)
    if err != nil {
        fmt.Println("http Listen failed")
    }
}

判断

用法示例:

<body>
{{if gt .Age 18}}
<p>已成年</p>
{{else}}
<p>未成年</p>
{{end}}
</body>

更多判断逻辑:

not 非

{{if not .condition}}

{{end}}

and 与

{{if and .condition1 .condition2}}

{{end}}

or 或

{{if or .condition1 .condition2}}

{{end}}

eq 等于

{{if eq .var1 .var2}}

{{end}}

ne 不等于

{{if ne .var1 .var2}}

{{end}}

lt 小于

{{if lt .var1 .var2}}

{{end}}

le 小于等于

{{if le .var1 .var2}}

{{end}}

gt 大于

{{if gt .var1 .var2}}

{{end}}

ge 大于等于

{{if ge .var1 .var2}}

{{end}}

with 封装

with语句就是创建一个封闭的作用域,在其范围内,{{.}}代表with的变量,而与外面的{{.}}无关,只与with的参数有关:

<body>
{{with .Name}}
<p>{{.}}</p>
{{end}}
</body>

上面这样包在 {{with .Var}} 里,with 里的 {{.}} 代表的就是 Var 这个变量。

with 可以封装常数:

{{ with "world"}}
    Now the dot is set to {{ . }}
{{ end }}

循环(遍历)

golang的template支持range循环来遍历map、slice内的内容,在range循环内,还可以使用$设置循环变量,我们可以通过 $i $v 来访问遍历的值。语法为:

{{range $i, $v := .slice}}
    <li>key: {{ $key }}, value: {{ $value }}</li>
{{end}}

这是另外一种遍历方式,这种方式无法访问到index或者key的值,需要通过点来访问对应的value:

{{range .slice}}
{{.field}}
{{end}}

在循环内,点是代表遍历的值。原本使用点来访问的变量,那么在循环内部就要用 $. 来访问。下面的例子表示循环内和循环外 ArticleConten 这个变量访问的方式:

{{.ArticleContent}}
{{range .slice}}
{{$.ArticleContent}}
{{end}}

定义变量

模板的参数可以是go中的基本数据类型,如字串,数字,布尔值,数组切片或者一个结构体。在模板中设置变量可以使用 $variable := value。我们在range迭代的过程使用了设置变量的方式。

{{$article := "hello"}}
{{$name := .Name}}

mysql 使用

这里只简单讲了数据的增删改查,所以测试代码前,需要先把数据库准备好。

先创建一个数据库,指定了编码,这样应该可以支持中文:

CREATE DATABASE 库名 CHARSET "utf8";

然后建2张表:

CREATE TABLE person (
    user_id int primary key auto_increment,
    username varchar(260),
    gender varchar(260),
    email varchar(260)
);

CREATE TABLE place (
    country varchar(200),
    city varchar(200),
    telcode int
);

导入数据库驱动

sql 包提供了通用的SQL(或类SQL)数据库接口。

sql 包必须与数据库驱动结合使用。

驱动包需要安装:

go get -u github.com/go-sql-driver/mysql

使用前,先要导入mysql的包:

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

上面导入了2个包。第一个是sql包,就是我们调用操作数据库用的。

第二个是驱动包,这里前面加了占位符,所以这个包只是引入,但是不使用它。并且如果要操作别的数据库的话,只需要修改驱动包就行了。

连接数据库

构建连接, 格式是:”用户名:密码@tcp(IP:端口)/数据库?charset=utf8” :

package main

import (
    "fmt"
    "time"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

var DB *sql.DB

func init() {
    database, err := sql.Open("mysql", "admin:admin123@tcp(192.168.3.103:3306)/Golang_week10")
    if err != nil {
        fmt.Println("连接数据库失败:", err)
        return
    }
    DB = database
}

func main() {
    fmt.Println(DB)
    DB.SetMaxIdleConns(16)  //设置闲置连接数
    DB.SetMaxOpenConns(100)  //设置最大连接数
    DB.SetConnMaxLifetime(100*time.Second)  //最大连接周期,超过时间的连接就close
    fmt.Println(DB)
}

插入数据

下面是插入数据,并且再获取id的示例:

// 数据库连接的init函数就省略了
func insert() {
    r, err := DB.Exec("insert into person(username,gender,email) values(?,?,?)", "Barry", "Male", "[email protected]")
    if err != nil {
        fmt.Println("插入数据ERROR:", err)
        return
    }
    fmt.Println(r)
    id, err := r.LastInsertId()
    if err != nil {
        fmt.Println("获取id ERROR:", err)
        return
    }
    fmt.Println(id)
}

func main() {
    insert()
}

上面的 values(?,?,?) 里的问号,是占位符,具体的值可以写在后面的参数里。当然如果不用占位符,直接就传1个字符串作为参数也是可以的。

查询

查询单个字段:

func query() {
    row := DB.QueryRow("select username from person where user_id=?", 1)
    var name string  // 创建变量用于存放查询到的数据
    if err := row.Scan(&name); err != nil {
        fmt.Println("Scan Failed:", err)
        return
    }
    fmt.Println(name)
}

func main() {
    query()
}

也可以一次查询多个字段或所有字段,查询之前按照表的类型创建结构体,用查询到的数据为结构体赋值:

type Person struct {
    ID int `db:"user_id"`
    Username sql.NullString `db:"username"`
    Gender sql.NullString `db:"gender"`
    Email sql.NullString `db:"email"`
}

func query() {
    row := DB.QueryRow("select * from person where user_id=?", 6)
    var person = new(Person)
    // row.scan中的字段必须是按照数据库存入字段的顺序,否则报错
    if err := row.Scan(&person.ID, &person.Username, &person.Gender, &person.Email); err != nil {
        fmt.Println("Scan Failed:", err)
        return
    }
    fmt.Println(person)
}

func main() {
    query()
}

数据模型,就是上面定义的结构体。这里的类型可以是Go的标准数据类型。但是如果数据库的字段允许为空,并且该字段的值也为空,那么查询后该字段会返回nil。如果是string类型,则无法接收nil,但sql.NullString则可以接收nil值。

另外,结构体里的tag标签在这里没有意义。不过上面的tag标注了该字段在数据库里对应的字段名,可能在别处会有用。

查询多行

func query() {
    rows, err := DB.Query("select * from person where user_id > ?", 1)
    defer func() {
        if rows != nil {
            rows.Close()
        }
    }()
    if err != nil {
        fmt.Println("Query 查询 ERROR:", err)
        return
    }
    var person = new(Person)
    for rows.Next() {
        if err = rows.Scan(&person.ID, &person.Username, &person.Gender, &person.Email); err != nil {
            fmt.Println("Scan Failed:", err)
            return
        }
        fmt.Println(person)
    }
}

func main() {
    query()
}

查询用起来还是不太方法,不过还可以选择其他第三方库,那里会有一些很好的扩展。后面会举例。

其他操作

由于基本都是用SQL的命令进行操作,所以其他操作就不一个一个举例了

update 更新数据

result, err := DB.Exec("UPDATE person set email=? where username=?", "Cara", "[email protected]")

delete 删除数据

result,err := DB.Exec("DELETE FROM person where id=?",1)

注意:更新数据不返回LastInsertID,所以result.LastInsertID一直为0。删除数据可以拿到LastInsertID,用法和插入数据里一样。

第三方库 sqlx

sqlx是一个go语言包,在内置database/sql包之上增加了很多扩展,简化数据库操作代码的书写。

由于database/sql接口是sqlx的子集,所有database/sql的用法,在sqlx中一样可以用。不过sqlx还有更多扩展,用起来更方便。

安装:

go get github.com/jmoiron/sqlx

查询 Select() 方法

Select是一个非常省时的扩展。它们把query和非常灵活的scan语法结合起来。Select用来获取结果切片:

// 这里的tag标签就有意义了,下面的Select()方法应该就是根据tag标签对号入座的
type Person struct {
    ID int `db:"user_id"`
    Username sql.NullString `db:"username"`
    Gender sql.NullString `db:"gender"`
    Email sql.NullString `db:"email"`
}

func select() {
    var persons []Person  // 这里创建的是存放结构体的切片
    if err := DB.Select(&person, "select * from person where userid > ?", 1); err != nil {
        fmt.Println("Select ERROR:", err)
        return
    }
    fmt.Println(person)
}

Select可以提高编码效率,还有更多扩展。sqlx 号称 golang 数据库开发神器,这里就提一下,等到真正用的时候再去深入学习了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK