Golang经典校验库validator用法解析

一、概述

在接口开发经常会遇到一个问题是后端需要写大量的繁琐代码进行数据校验,所以就想着有没有像前端校验一样写规则进行匹配校验,然后就发现了validator包,一个比较强大的校验工具包下面是一些学习总结,详细内容可以查看validator

二、操作符说明

标记 标记说明
, 多操作符分割
| 或操作
跳过字段验证

三、常用标记说明

标记 标记说明
required 必填 Field或Struct validate:"required"
omitempty 空时忽略 Field或Struct validate:"omitempty"
len 长度 Field validate:"len=0"
eq 等于 Field validate:"eq=0"
gt 大于 Field validate:"gt=0"
gte 大于等于 Field validate:"gte=0"
lt 小于 Field validate:"lt=0"
lte 小于等于 Field validate:"lte=0"
eqfield 同一结构体字段相等 Field validate:"eqfield=Field2"
nefield 同一结构体字段不相等 Field validate:"nefield=Field2"
gtfield 大于同一结构体字段 Field validate:"gtfield=Field2"
gtefield 大于等于同一结构体字段 Field validate:"gtefield=Field2"
ltfield 小于同一结构体字段 Field validate:"ltfield=Field2"
ltefield 小于等于同一结构体字段 Field validate:"ltefield=Field2"
eqcsfield 跨不同结构体字段相等 Struct1.Field validate:"eqcsfield=Struct2.Field2"
necsfield 跨不同结构体字段不相等 Struct1.Field validate:"necsfield=Struct2.Field2"
gtcsfield 大于跨不同结构体字段 Struct1.Field validate:"gtcsfield=Struct2.Field2"
gtecsfield 大于等于跨不同结构体字段 Struct1.Field validate:"gtecsfield=Struct2.Field2"
ltcsfield 小于跨不同结构体字段 Struct1.Field validate:"ltcsfield=Struct2.Field2"
ltecsfield 小于等于跨不同结构体字段 Struct1.Field validate:"ltecsfield=Struct2.Field2"
min 最小值 Field validate:"min=1"
max 最大值 Field validate:"max=2"
structonly 仅验证结构体,不验证任何结构体字段 Struct validate:"structonly"
nostructlevel 不运行任何结构级别的验证 Struct validate:"nostructlevel"
dive 向下延伸验证,多层向下需要多个dive标记 [][]string validate:"gt=0,dive,len=1,dive,required"
dive Keys & EndKeys 与dive同时使用,用于对map对象的键的和值的验证,keys为键,endkeys为值 map[string]string validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"
required_with 其他字段其中一个不为空且当前字段不为空 Field validate:"required_with=Field1 Field2"
required_with_all 其他所有字段不为空且当前字段不为空 Field validate:"required_with_all=Field1 Field2"
required_without 其他字段其中一个为空且当前字段不为空 Field `validate:”required_without=Field1 Field2″
required_without_all 其他所有字段为空且当前字段不为空 Field validate:"required_without_all=Field1 Field2"
isdefault 是默认值 Field validate:"isdefault=0"
oneof 其中之一 Field validate:"oneof=5 7 9"
containsfield 字段包含另一个字段 Field validate:"containsfield=Field2"
excludesfield 字段不包含另一个字段 Field validate:"excludesfield=Field2"
unique 是否唯一,通常用于切片或结构体 Field validate:"unique"
alphanum 字符串值是否只包含 ASCII 字母数字字符 Field validate:"alphanum"
alphaunicode 字符串值是否只包含 unicode 字符 Field validate:"alphaunicode"
alphanumunicode 字符串值是否只包含 unicode 字母数字字符 Field validate:"alphanumunicode"
numeric 字符串值是否包含基本的数值 Field validate:"numeric"
hexadecimal 字符串值是否包含有效的十六进制 Field validate:"hexadecimal"
hexcolor 字符串值是否包含有效的十六进制颜色 Field validate:"hexcolor"
lowercase 符串值是否只包含小写字符 Field validate:"lowercase"
uppercase 符串值是否只包含大写字符 Field validate:"uppercase"
email 字符串值包含一个有效的电子邮件 Field validate:"email"
json 字符串值是否为有效的 JSON Field validate:"json"
file 符串值是否包含有效的文件路径,以及该文件是否存在于计算机上 Field validate:"file"
url 符串值是否包含有效的 url Field validate:"url"
uri 符串值是否包含有效的 uri Field validate:"uri"
base64 字符串值是否包含有效的 base64值 Field validate:"base64"
contains 字符串值包含子字符串值 Field validate:"contains=@"
containsany 字符串值包含子字符串值中的任何字符 Field validate:"containsany=abc"
containsrune 字符串值包含提供的特殊符号值 Field validate:"containsrune=☢"
excludes 字符串值不包含子字符串值 Field validate:"excludes=@"
excludesall 字符串值不包含任何子字符串值 Field validate:"excludesall=abc"
excludesrune 字符串值不包含提供的特殊符号值 Field validate:"containsrune=☢"
startswith 字符串以提供的字符串值开始 Field validate:"startswith=abc"
endswith 字符串以提供的字符串值结束 Field validate:"endswith=abc"
ip 字符串值是否包含有效的 IP 地址 Field validate:"ip"
ipv4 字符串值是否包含有效的 ipv4地址 Field validate:"ipv4"
datetime 字符串值是否包含有效的 日期 Field validate:"datetime"

四、标记使用注意

1、当搜索条件与特殊标记冲突时,如:逗号(,),或操作(|),中横线(-)等则需要使用 UTF-8十六进制表示形式

例:

type Test struct {
   Field1 string  `validate:"excludesall=|"`    // 错误
   Field2 string `validate:"excludesall=0x7C"` // 正确.
}

2、可通过validationErrors := errs.(validator.ValidationErrors)获取错误对象自定义返回响应错误 3、自定义校验结果翻译

// 初始化翻译器
func validateInit() {
	zh_ch := zh.New()
	uni := ut.New(zh_ch)               // 万能翻译器,保存所有的语言环境和翻译数据
	Trans, _ = uni.GetTranslator("zh") // 翻译器
	Validate = validator.New()
	_ = zh_translations.RegisterDefaultTranslations(Validate, Trans)
	// 添加额外翻译
	_ = Validate.RegisterTranslation("required_without", Trans, func(ut ut.Translator) error {
		return ut.Add("required_without", "{0} 为必填字段!", true)
	}, func(ut ut.Translator, fe validator.FieldError) string {
		t, _ := ut.T("required_without", fe.Field())
		return t
	})
}

五、使用示例

package main
import (
   "fmt"
   "github.com/go-playground/validator/v10"
)
// 实例化验证对象
var validate = validator.New()
func main() {
   // 结构体验证
   type Inner struct {
      String string `validate:"contains=111"`
   }
   inner := &Inner{String: "11@"}
   errs := validate.Struct(inner)
   if errs != nil {
      fmt.Println(errs.Error())
   }
   // 变量验证
   m := map[string]string{"": "", "val3": "val3"}
   errs = validate.Var(m, "required,dive,keys,required,endkeys,required")
   if errs != nil {
      fmt.Println(errs.Error())
   }
}

包下载:go get github.com/go-playground/validator/v10
demo示例:https://github.com/Jambo-Git/validator-demo

Centos上配置开机自启动的几种方式

CentOS开机自动启动脚本

前言

Linux作为服务器实在是太香了,唯一麻烦的就是服务器重启的时候,一些程序又得手动启动。其实可以通过添加开机自动启动脚本的方法来进行自动启动。

自启动方法

在/etc/rc.d/rc.local中添加启动脚本

chmod +x /etc/rc.d/rc.local

二、在 /etc/rc.d/rc.local 中添加要执行的指定命令

在 /etc/rc.d/rc.local 中添加要执行的指定命令,格式如下:

su - 用户 -c “执行命令”

示例如下:

su - root -c "/home/source/start-api.sh"

手动配置

1、在/etc/rc.d/rc.local中添加服务启动命令

/etc/rc.d/rc.local脚本会在Centos系统启动时被自动执行,所以可以把需要开机后执行的命令直接放在这里。

示例:配置开机启动apollo

vi /etc/rc.d/rc.local

想简单点可以像上面这样直接将服务的启动命令添加到/etc/rc.d/rc.local中。

也可以自己编写服务启动的脚本。由于重启时是以root用户重启,需要保证root用户有脚本执行权限。

1)、编写服务启动的脚本

vi /opt/script/autostart.sh

#!/bin/bash
/root/Downloads/docker-quick-start/docker-compose up -d

2)、赋予脚本可执行权限(/opt/script/autostart.sh是你的脚本路径)

chmod +x /opt/script/autostart.sh

3)、打开/etc/rc.d/rc.local文件,在末尾增加如下内容

/opt/script/autostart.sh

3)、在centos7中,/etc/rc.d/rc.local的权限被降低了,所以需要执行如下命令赋予其可执行权限

chmod +x /etc/rc.d/rc.local

通过chkconfig配置

在CentOS7之前,可以通过chkconfig来配置开机自启动服务。

chkconfig相关命令:

chkconfig –-add xxx //把服务添加到chkconfig列表
chkconfig --del xxx //把服务从chkconfig列表中删除
chkconfig xxx on //开启开机自动启动
chkconfig xxx off //关闭开机自动启动
chkconfig --list //查看所有chklist中服务
chkconfig --list xxx 查看指定服务

chkconfig运行级别level和启动顺序的概念:

chkconfig --list

这里的0到6其实指的就是服务的level。
–level<等级代号> 指定系统服务要在哪一个执行等级中开启或关毕。
等级0表示:表示关机
等级1表示:单用户模式
等级2表示:无网络连接的多用户命令行模式
等级3表示:有网络连接的多用户命令行模式
等级4表示:不可用
等级5表示:带图形界面的多用户模式
等级6表示:重新启动
比如如下命令:

//设定mysqld在等级3和5为开机运行服务
chkconfig --level 35 mysqld on 
//设置network服务开机自启动,会把2~5的等级都设置为on
chkconfig network on

表示开机启动配置成功。
服务的启动顺序又指的什么呢?
服务的启动顺序是指在服务器启动后服务启动脚本执行的顺序。
以系统默认服务network说明:

cat /etc/init.d/network

其中 # chkconfig: 2345 10 90用来指定服务在各个level下的启动顺序。
该配置的含义是network服务在2、3、4、5的level下的启动顺序是10,在1和6的level等级下的启动顺序是90。
chkconfig配置的服务启动顺序最后都会在/etc/rc.d/目录下体现出来:

cd /etc/rc.d/

文件中脚本命名规则,首字母K表示关闭脚本,首字母S表示启用脚本,数字表示启动的顺序.
chkconfig配置实例
通常kibana的官方配置是没有介绍如何配置开机自启动的。这里我配置kibana开机自启动来说明。
1、在/etc/init.d目录下,新建脚本kibana

cd /etc/init.d
vi kibana

脚本内容如下:

#!/bin/bash
# chkconfig: 2345 98 02
# description:  kibana
KIBANA_HOME=/usr/local/kibana-6.2.4-linux-x86_64
case $1 in
 start)
         $KIBANA_HOME/bin/kibana &
         echo "kibana start"
         ;;
 stop)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    kibana_pid=`echo ${kibana_pid_str%%/*}`
    kill -9 $kibana_pid
    echo "kibana stopped"
    ;;
 restart)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    kibana_pid=${kibana_pid_str%%/*}
    kibana_pid=`echo ${kibana_pid_str%%/*}`
    kill -9 $kibana_pid
    echo "kibana stopped"
    $KIBANA_HOME/bin/kibana &
    echo "kibana start"
    ;;
 status)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    if test -z $kibana_pid_str; then
       echo "kibana is stopped"
    else
       pid=`echo ${kibana_pid_str%%/*}`
       echo "kibana is started,pid:"${pid}
    fi
    ;;
*)
    echo "start|stop|restart|status"
    ;;
esac

注意⚠️:
每个被chkconfig管理的服务需要在对应的init.d下的脚本加上两行或者更多行的注释。
第一行告诉chkconfig缺省启动的运行级以及启动和停止的优先级。如果某服务缺省不在任何运行级启动,那么使用 – 代替运行级。
第二行对服务进行描述,可以用\ 跨行注释。

#!/bin/bash
#chkconfig:2345 98 02
#description:kibana

解释说明:
配置kibana服务在2、3、4、5的level等级下脚本执行顺序是98,
1、6的level等级下脚本执行顺序是01。
2、增加脚本的可执行权限

chmod +x kibana

3、查看chkconfig list

chkconfig --list

4、把服务添加到chkconfig列表

chkconfig --add kibana

5、设置kibana服务自启动

chkconfig kibana on //开启开机自动启动

6、查看kibana服务自启动状态

chkconfig --list kibana

7、服务的启动、停止、重启和状态查看

//查看服务状态
service kibana status
//服务启动
service kibana start
//服务停止
service kibana stop
//服务重启
service kibana restart

Golang操作MongoDB:mongo-driver

这篇文章主要介绍了使用GO操作MongoDB,包括安装MongoDB驱动程序连接mongodb的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

1.安装 MongoDB Go 驱动程序

MongoDB Go Driver 由几个包组成。如果您只是使用 go get,则可以使用以下命令安装驱动程序:

go get github.com/mongodb/mongo-go-driver

这个输出可能看起来像一个警告,说明类似于 package github.com/mongodb/mongo-go-driver: no Go files in (…)。这是预期的输出。

如果您使用 govendor 包管理器,则需要使用以下命令安装主 mongo 包以及 bson 和 mongo/options 包:

govendor fetch github.com/mongodb/mongo-go-driver/mongo

govendor fetch go.mongodb.org/mongo-driver/bson

govendor fetch go.mongodb.org/mongo-driver/mongo/options

2.设置连接

导入 MongoDB Go 驱动程序后,我们可以使用 Client.Connect(context) 连接到 MongoDB 部署。我们需要设置一个新客户端,我们需要在设置客户端时传递 mongo 数据库的 URI。然后我们只需要使用上下文调用 client.Connect(context),这将建立我们的连接。

以下代码为我们建立了一个连接:

//Set up a context required by mongo.Connect
ctx, cancel := context.WithTimeout(context.Background(), 10\*time.Second)

//To close the connection at the end
defer cancel()

//We need to set up a client first
//It takes the URI of your database
client, error := mongo.NewClient(options.Client().ApplyURI("your\_database\_uri"))

if error != nil {
 log.Fatal(err)
}

//Call the connect function of client
error = client.Connect(ctx)

//Checking the connection
error = client.Ping(context.TODO(), nil)
fmt.Println("Database connected")

3.在 Go 中使用 BSON 对象

MongoDB 中的 JSON 文档存储在称为 BSON(二进制编码 JSON)的二进制表示中。与将 JSON 数据存储为简单字符串和数字的其他数据库不同,BSON 编码扩展了 JSON 表示以包括其他类型,例如 int、long、date、floating point 和 decimal128。这使得应用程序更容易可靠地处理、排序和比较数据。 Go Driver 有两个表示 BSON 数据的类型:D 类型和 Raw 类型。

D 系列类型用于使用本机 Go 类型简洁地构建 BSON 对象。这对于构造传递给 MongoDB 的命令特别有用。 D 系列包括四种类型:

  • D:BSON 文档。这种类型应该在顺序很重要的情况下使用,例如 MongoDB 命令。
  • M:无序映射。它与 D 相同,只是它不保持顺序。
  • A:一个 BSON 数组。
  • E:D 中的单个元素。

这是一个使用 D 类型构建的过滤器文档的示例,可用于查找名称字段与 Alice 或 Bob 匹配的文档:

bson.D{{
 "name", 
 bson.D{{
 "$in", 
 bson.A{"Alice", "Bob"}
 }}
}}

4. CRUD 操作

对于 CRUD 和其他操作,我们需要使用集合对象,我们可以通过引用数据库中各自的集合来创建它,例如:

BooksCollection := client.Database("test").Collection("books")

插入
对于创建,我们可以将 collection.InsertOne() 用于单个条目,也可以使用 collection.InsertMany() 来接收对象切片。

/\*\*
\* Create - Adding a new book
\* res -\> the insert command returns the inserted id of the oject
\*/

res, err := BooksCollection.InsertOne(ctx, bson.M{"name": "The Go Language", "genre": "Coding", "authorId": "4"})

if err != nil {
log.Fatal(err)
}

在 collection.InsertOne() 中,我们可以通过 bson.M{} 传递一个字符串对象,或者我们可以创建一个我们各自类型结构的对象并传递该对象。

阅读
为了查找文档,我们需要一个过滤文档以及一个指向可以将结果解码成的值的指针。要查找单个文档,请使用 collection.FindOne()。此方法返回一个可以解码为值的结果。过滤器对象指定我们要查找的内容。

filter := bson.D{{"name", "Book 1"}}

// create a value into which the result can be decoded
var result bookType

err = collection.FindOne(context.TODO(), filter).Decode(&result)
if err != nil {
 log.Fatal(err)
}

fmt.Printf("Found a single Book: %+v\n", result)

要查找多个文档,请使用 collection.Find()。此方法返回一个光标。游标提供了一个文档流,我们可以通过它一次迭代和解码一个。一旦游标用尽,我们应该关闭游标。

cur, error := BooksCollection.Find(ctx, bson.D{{}})

var allbooks []\*bookType

//Loops over the cursor stream and appends to result array
for cur.Next(context.TODO()) {
var booksResultHolder bookType

err := cur.Decode(&bookResultHolder)

if err != nil {
log.Fatal(err)
}

allbooks = append(allbooks, &booksResultHolder)
}

//dont forget to close the cursor
defer cur.Close(context.TODO())

// Loop over the result array and perform whatever required
for \_, element := range allbooks {
book := \*element
fmt.Println(book)
}

更新
collection.UpdateOne() 方法允许您更新单个文档。它需要一个过滤文档来匹配数据库中的文档,并需要一个更新文档来描述更新操作。这些可以按照我们在阅读时制作过滤器对象的方式构建。

/\*\*
\* Update
\* Collection has functions like UpdateOne and UpdateMany
\* Returns the Matched and Modified Count
\*/

filter := bson.D{{"name", "Book 1"}}

// Need to specify the mongodb output operator too
newName := bson.D{
{"$set", bson.D{
{"name", "Updated Name of Book 1"},
}},
}

res, err := BooksCollection.UpdateOne(ctx, filter, newName)

if err != nil {
log.Fatal(err)
}

updatedObject := \*res

fmt.Printf("The matched count is : %d, the modified count is : %d", updatedObject.MatchedCount, updatedObject.ModifiedCount)

删除
最后,我们可以使用 collection.DeleteOne() 或 collection.DeleteMany() 删除文档。在这里,我们可以传递 nil 作为过滤器参数,它将匹配集合中的所有文档或任何其他特定参数。我们还可以使用collection.Drop()删除整个集合。

filter = bson.D{{"name", "Updated Name of Book 2"}}

deleteResult, error := BooksCollection.DeleteOne(ctx, filter)

后续步骤
本教程的源代码可以在这里找到。

MongoDB Go 驱动程序的文档可在GoDoc上找到。您可能对有关使用聚合或交易的文档特别感兴趣。

希望本教程对您来说更简单,祝大家编码愉快!

mongodb – 内存占用过高,wiredTigerCacheSizeGB限制

问题

在使用 MongoDB 过程中,会遇到 内存占用随着数据操作而线性增加 的情况;
如果数据持续的大量写入的话,会大量占用服务器内存,出现 OOM 问题,在服务器内存保护机制作用下,MongoDB 会被 kill 掉。

内存增加的原因

mongo为了优化他的读写效率,将内存当做缓存,所以你读写次数越多,缓存就越大。默认值:
从3.4开始,WiredTiger内部缓存默认使用较大的一个, 我用的是4.1
50%(RAM – 1 GB),或256 MB。
例如,我是8G内存,那么最大缓存0.5*(8-1)=3.5G,看到了么。。。mongo默认3.5G都是他的缓存。

cacheSizeGB的介绍

storage.wiredTiger. engineConfig.cacheSizeGB

wiredtiger将使用所有数据的最大缓存大小,wiredTiger缓存工作集(working set)数据的内存大小,单位:GB,

此值决定了wiredTiger与mmapv1的内存模型不同,它可以限制mongod对内存的使用量,而mmapv1则不能(依赖于系统级的mmap)。

默认情况下,cacheSizeGB的值为假定当前节点只部署一个mongod实例,在MongoDB 3,默认情况下,wiredtiger缓存,使用1 GB或安装的物理内存的一半,以较大者为准。

如果当前节点部署了多个mongod进程,那么需要合理配置此值。

如果mongod部署在虚拟容器中(比如,lxc,cgroups,Docker)等,它将不能使用整个系统的物理内存,则需要适当调整此值。默认值为物理内存的一半。

解决

在配置中限制mongo的缓存大小, 引擎需要更换为wiredTiger 默认的mmapv1依赖于mmap不能指定
官方也声称wiredTiger更加优秀
修改(增加)cacheSizeGB配置。

配置如下:

# Where and how to store data.
  engine: wiredTiger
#  mmapv1:
  wiredTiger:
    engineConfig:
      cacheSizeGB: 1

使用Go编译为可执行文件(windows/linux)

案例场景:创建一个两层目录,并在该目录下创建一个文件,将“Hello World”字符写入该文件,并读取出来。

目标:(1)测试案例是否能执行成功;(2)编译代码成windows与linux两种环境下的可执行文件。

测试代码文件名为main.go,内容如下:

package main
 
import (
	"fmt"
	"io/ioutil"
	"os"
)
 
func main() {
	// 文件夹名
	_dir := "data/test"
	exist, err := pathExists(_dir)
	if err != nil {
		fmt.Printf("get dir error![%v]\n", err)
		return
	}
 
	if exist {
		fmt.Printf("has dir![%v]\n", _dir)
	} else {
		fmt.Printf("no dir![%v]\n", _dir)
		// 创建文件夹
		//err := os.Mkdir(_dir, os.ModePerm)
		err := os.MkdirAll(_dir, 0666)
		if err != nil {
			fmt.Printf("mkdir failed![%v]\n", err)
		} else {
			fmt.Printf("mkdir success!\n")
			fileName := _dir + "/test.txt"
			// 创建文件
			os.Create(fileName)
			// 打开文件
			file, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0666)
			// 当执行完,关闭文件
			defer file.Close()
			// 写内容到文件中
			file.WriteString("Hello World!")
			//读取文件
			data, _ := ioutil.ReadFile(fileName)
			// 打印内容
			fmt.Println(string(data))
		}
	}
}
 
func pathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

经过调试,上述代码可正常执行。测试通过。

编译成windows环境exe可执行文件过程,打开文件所在目录,在资源路径框中输入cmd,打开cmd命令框,通过“go env”查看当期环境变量,以windows10环境为例,默认为windows环境。

// 配置环境变量
SET CGO_ENABLED=1
SET GOOS=windows
SET GOARCH=amd64
// 编译命令
go build main.go

编译出来后就是一个可执行文件main.exe,可用鼠标双击直接执行,传到其他电脑上操作,依然可执行,不依赖第三方包(不像Java会依赖JDK)。

执行后,在main.exe所在目录下,生成一个data/test/test.txt,并且打开test.txt文件可看到Hello World。

编译成Linux环境可执行文件,此处除编译环境参数外,其他步骤与上面类似,编译参数如下

// 配置参数
SET CGO_ENABLED=0 
SET GOOS=linux 
SET GOARCH=amd64 
// 编译命令
go build main.go

编译输出的可执行文件名为main,上传至centos7.x系统,使用 “chmod +x main”添加可执行权限,执行 ” ./main “,输出结果与上述windows结果一样。

备注:上述编译环境所在的操作系统均为Windows10,即在windows10上开发代码,编译输出windows与linux两种环境的可执行文件。

Go 语言MySQL 事务操作

1.MySQL驱动下载
描述: Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口, 并不提供具体的数据库驱动, 所以使用database/sql包时必须注入(至少)一个数据库驱动。

Go语言中我们常用的数据库操作, 基本上都有完整的第三方实现,例如本节的MySQL驱动(https://github.com/go-sql-driver/mysql)

# 下载mysql驱动依赖, 第三方的依赖默认保存在 `$GOPATH/src` (注意是在项目目录里)
➜ go get -u github.com/go-sql-driver/mysql
go: downloading github.com/go-sql-driver/mysql v1.6.0
➜ weiyigeek.top go get github.com/go-sql-driver/mysql

# 项目地址
➜ weiyigeek.top pwd
/home/weiyigeek/app/program/project/go/src/weiyigeek.top

# 第三方包地址
➜ go-sql-driv pwd
/home/weiyigeek/app/program/project/go/pkg/mod/github.com/go-sql-driv 作者:WeiyiGeek https://www.bilibili.com/read/cv14620719/ 出处:bilibili

2.MySQL驱动格式
描述: 使用MySQL驱动格式函数原型如下所示:

func Open(driverName, dataSourceName string) (*DB, error) : Open方法是打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。

func (db *DB) SetMaxOpenConns(n int) : SetMaxOpenConns方法是设置与数据库建立连接的最大数目。

如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。

如果n<=0,不会限制最大开启连接数,默认为0(无限制)。

func (db *DB) SetMaxIdleConns(n int) : SetMaxIdleConns方法是设置连接池中的最大闲置连接数。

如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。

如果n<=0,不会保留闲置连接。

基础示例

import (
	"database/sql"
	"fmt"

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

func main() {
	// 数据库DSN(Data Source Name)连接数据源
	dsn := "root:WwW.weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True"

	// 连接数据库
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed, Error: %v \n", dsn, err)
		panic(err)
	}
	// 此行代码要写在上面err判断的下面(注意点)。
	defer db.Close()

	// 判断连接的数据库
	err = db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed, Error: %v \n", dsn, err)
		return
	}

	fmt.Println("数据库连接成功!")
} 

Tips: 为什么上面代码中的defer db.Close()语句不应该写在if err != nil的前面呢?

 

3.MySQL初始化连接
描述: 上面的例子可以看到Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接,此时我们如果要检查数据源的名称是否真实有效,应该调用Ping方法。

下述代码中sql.DB是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个goroutine同时使用。

MySQL 用户密码更改:

-- MySQL 5.7.x & MySQL 8.x
ALTER USER `root`@`%` IDENTIFIED BY 'weiyigeek.top';

初始化示例:

// Go 语言利用 MySQL Driver 连接 MySQL 示例
package main
import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象db
var db *sql.DB
// 定义一个初始化数据库的函数
func initDB() (err error) {
	// DSN(Data Source Name) - 数据库连接数据源
	// MySQL 5.7.X 与 MySQL 8.x 都是支持的
	dsn := "root:weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True"
	// 注册第三方mysql驱动到sql中,此处并不会校验账号密码是否正确,此处赋值给全局变量db。
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed\n %v \n", dsn, err)
		return err
	}
	// 尝试与数据库建立连接(校验DSN是否正确)
	err = db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed,\n%v \n", dsn, err)
		return err
	}
	// 设置与数据库建立连接的最大数目
	db.SetMaxOpenConns(1024)
	// 设置连接池中的最大闲置连接数,0 表示不会保留闲置。
	db.SetMaxIdleConns(0)
	fmt.Println("数据库初始化连接成功!")
	return nil
}

func main() {
	// 调用输出化数据库的函数
	err := initDB()
	defer db.Close()

	if err != nil {
		fmt.Println("Database Init failed!")
		return
	}
} 

执行结果:

# 连接成功时
数据库初始化连接成功!

# 连接失败时
Connection root:www.weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True Failed,
Error 1045: Access denied for user 'root'@'10.20.172.108' (using password: YES)
Database Init failed! 

4.MySQL的CRUD操作
库表准备
我们首先需要在MySQL(8.x)数据库中创建一个名为test数据库和一个user表,SQL语句如下所示:

-- 建库建表
CREATE DATABASE test;
USE test;
CREATE TABLE `user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(20) DEFAULT '',
  `age` INT(11) DEFAULT '0',
  PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

-- 测试数据插入
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (1, 'WeiyiGeek', 20);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (2, 'Elastic', 18);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (3, 'Logstash', 20);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (4, 'Beats', 10);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (5, 'Kibana', 19);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (6, 'C', 25);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (7, 'C++', 25);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (8, 'Python', 26); 

示例结构体声明:

type user struct {
	id   int
	age  int
	name string
} 

单行查询
函数原型: func (db *DB) QueryRow(query string, args …interface{}) *Row
函数说明: 单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。
Tips: QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

简单示例:

// 查询单条数据示例
func queryRowDemo() {
  var u user
	sqlStr := "select id, name, age from user where id=?"
	// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放 [注意点]
	err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
} 

多行查询
函数原型: func (db *DB) Query(query string, args …interface{}) (*Rows, error)

函数说明: 多行查询db.Query()执行一次查询,返回多行结果(即 Rows), 一般用于执行select命令, 参数args表示 query中的占位参数(空接口)。

简单示例:

// 查询多条数据示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:关闭rows释放持有的数据库链接 [否则将一直占有连接池资源导致后续无法正常连接]
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
} 

插入/更新/删除数据
函数原型: func (db *DB) Exec(query string, args …interface{}) (Result, error)
函数说明: Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。

具体插入数据示例代码如下:

// 插入数据
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "王五", 38)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
  // 新插入数据的id
	theID, err := ret.LastInsertId()
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

具体更新数据示例代码如下:

// 更新数据
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
} 

具体删除数据的示例代码如下:

// 删除数据
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 3)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
} 

综合实践
下述代码简单实现利用Go语言操作MySQL数据库的增、删、改、查等。

数据库连接封装:weiyigeek.top/studygo/Day09/MySQL/mypkg

// 自定义mypkg包 initdb.go
package mypkg
import (
	"database/sql"
	"fmt"

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

// 定义一个mysqlObj结构体
type MysqlObj struct {
	Mysql_host             string
	Mysql_port             uint16
	Mysql_user, Mysql_pass string
	Database               string
	Db                     *sql.DB
}

// 定一个Person结构体
type Person struct {
	Uid  int
	Name string
	Age  int
}

// 定义一个初始化数据库的函数
func (conn *MysqlObj) InitDB() (err error) {

	// DSN(Data Source Name) 数据库连接字符串
	// MySQL 5.7.X 与 MySQL 8.x 都是支持的
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", conn.Mysql_user, conn.Mysql_pass, conn.Mysql_host, conn.Mysql_port, conn.Database)

	// 注册第三方mysql驱动到sql中,此处并不会校验账号密码是否正确,此处赋值给全局变量db。
	conn.Db, err = sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed\n%v \n", dsn, err)
		return err
	}

	// 尝试与数据库建立连接(校验DSN是否正确)
	err = conn.Db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed,\n%v \n", dsn, err)
		return err
	}

	// 设置与数据库建立连接的最大数目
	conn.Db.SetMaxOpenConns(1024)

	// 设置连接池中的最大闲置连接数
	conn.Db.SetMaxIdleConns(0) // 不会保留闲置

	return nil
} 

实践 main 入口函数:

package main
import (
	"database/sql"
	"fmt"
	db "weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// 单结果语句查询函数示例
func queryPersonOne(conn *sql.DB, Uid int) (res db.Person) {
	// 1.单条SQL语句
	sqlStr := `select Uid,name,age from test.user where Uid=?;`
	// 2.执行SQL语句并返回一条结果
	rowObj := conn.QueryRow(sqlStr, Uid)
	// 3.必须对rowObj调用Scan方法,因为查询后我们需要释放数据库连接对象,而它调用后会自动释放。
	rowObj.Scan(&res.Uid, &res.Name, &res.Age)
	// 4.返回一个person对象
	return res
}

// 多结果语句查询函数示例
func queryPersonMore(conn *sql.DB, id int) {
	// 1.SQL 语句
	sqlStr := `select Uid,name,age from test.user where Uid > ?;`
	// 2.执行 SQL
	rows, err := conn.Query(sqlStr, id)
	if err != nil {
		fmt.Printf("Exec %s query failed!,err : %v \n", sqlStr, err)
		return
	}
	// 3.调用结束后关闭rows,释放数据库连接资源
	defer rows.Close()
	// 4.循环读取结果集中的数据
	for rows.Next() {
		var u db.Person
		err := rows.Scan(&u.Uid, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("Uid:%d name:%s age:%d\n", u.Uid, u.Name, u.Age)
	}
}

// 执行插入操作的函数示例
func insertPerson(conn *sql.DB) {
	// 1.SQL 语句
	sqlStr := `insert into user(name,age) values("Go语言",15)`
	// 2.执行插入语句
	ret, err := conn.Exec(sqlStr)
	if err != nil {
		fmt.Printf("Insert Failed, err : %v \n", err)
		return
	}
	// 3.插入数据操作,拿到插入数据库的id值
	Uid, err := ret.LastInsertId()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印插入数据的id值
	fmt.Println("插入语句Uid值: ", Uid)
}

// 执行更新操作的函数示例
func updatePerson(conn *sql.DB, age, Uid int) {
	// 1.SQL 语句
	sqlStr := `update user set age=? where Uid = ?`
	// 2.执行插入语句
	ret, err := conn.Exec(sqlStr, age, Uid)
	if err != nil {
		fmt.Printf("Update Failed, err : %v \n", err)
		return
	}
	// 3.更新数据操作,获取到受影响的行数
	count, err := ret.RowsAffected()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印数据影响的行数
	fmt.Println("更新数据影响的行数: ", count)
}

// 执行删除数据的操作函数示例
func deletePerson(conn *sql.DB, Uid int) {
	// 1.SQL 语句
	sqlStr := `delete from user where Uid > ?`
	// 2.执行删除的语句
	ret, err := conn.Exec(sqlStr, Uid)
	if err != nil {
		fmt.Printf("Delete Failed, err : %v \n", err)
		return
	}
	// 3.删除数据操作,获取到受影响的行数
	count, err := ret.RowsAffected()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印删除数据的影响的行数:
	fmt.Println("删除数据影响的行数: ", count)
}

func main() {
	// 1.mysqlObj 结构体实例化
	conn := &db.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}
	// 2.初始化数据库
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("数据库初始化连接成功!")
	}

	// 3.程序结束时关闭数据库连接
	defer conn.Db.Close()

	// 4.单行查询
	res := queryPersonOne(conn.Db, 1)
	fmt.Printf("单行查询: %#v\n", res)

	// 5.多行查询
	fmt.Println("多行查询")
	queryPersonMore(conn.Db, 6)

	// 6.插入数据
	fmt.Println("插入数据")
	insertPerson(conn.Db)

	// 7.更新数据
	fmt.Println("更新数据")
	updatePerson(conn.Db, 16, 9)

	// 8.删除数据
	fmt.Println("删除数据")
	deletePerson(conn.Db, 10)
}

执行结果&数据库查询结果:

数据库初始化连接成功!
单行查询: main.person{uid:1, name:"WeiyiGeek", age:20}
多行查询
uid:7 name:C++ age:25
uid:8 name:Python age:26
uid:9 name:Golang age:15
插入数据
插入语句uid值:  10
更新数据
更新数据影响的行数:  1
删除数据
删除数据影响的行数:  1 

5.MySQL预处理

 

基础介绍
什么是预处理?

普通SQL语句执行过程:

客户端对SQL语句进行占位符替换得到完整的SQL语句。

客户端发送完整SQL语句到MySQL服务端

MySQL服务端执行完整的SQL语句并将结果返回给客户端。

预处理执行过程:

把SQL语句分成两部分,命令部分与数据部分。

先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。

然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。

MySQL服务端执行完整的SQL语句并将结果返回给客户端。

 

为什么要预处理?

优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。

避免SQL注入问题。

SQL注入
描述: 非常注意, 我们任何时候都不应该自己拼接SQL语句, 可能会导致SQL注入的问题。

此处演示一个自行拼接SQL语句的示例,编写一个根据name字段查询user表的函数如下:

// 可被 sql 注入示例
func sqlInjectDemo(name string) {
  var u user
	sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)  // 关键点
	fmt.Printf("SQL:%s\n", sqlStr)
	err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("exec failed, err:%v\n", err)
		return
	}
	fmt.Printf("user:%#v\n", u)
} 

当name变量输入以下字符串时便会引发SQL注入问题:

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #") 

示例演示
Go是如何实现MySQL预处理
描述: database/sql 中使用下面的Prepare方法来实现预处理操作。
函数原型: func (db *DB) Prepare(query string) (*Stmt, error)
函数说明: Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

示例演示:
描述: 此处引用上面封装的结构体成员以及方法,进行数据库的初始化操作。

package main

import (
	"database/sql"
	"fmt"

	db "weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// ## 预处理查询示例函数
func prepareQuery(conn *sql.DB, id int) {
	// SQL语句
	sqlStr := "select uid,name,age from user where uid > ?;"
	// 预处理
	stmt, err := conn.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	// 释放预处理
	defer stmt.Close()

	// 查询 uid 为 id 以上的数据
	rows, err := stmt.Query(id)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 释放 rows
	defer rows.Close()

	// 循环读取结果集中的数据,此处利用map来装我们遍历获取到的数据,注意内存申请。
	res := make(map[int]db.Person, 5)
	for rows.Next() {
		var u db.Person
		err := rows.Scan(&u.Uid, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		_, ok := res[u.Uid]
		if !ok {
			res[u.Uid] = u
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.Uid, u.Name, u.Age)
	}
	fmt.Printf("%#v\n", res)
}


// ## 插入、更新和删除操作的预处理十分类似,这里以插入操作的预处理为例:
func prepareInsert(conn *sql.DB) {
	// 插入的SQL语句
	sqlStr := "insert into user(name,age) values (?,?)"
	// 进行SQL语句的预处理
	stmt, err := conn.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	// 释放 stmt 资源
	defer stmt.Close()

	// 执行预处理后的SQL (可以多次执行)
	_, err = stmt.Exec("WeiyiGeek", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	// 执行预处理后的SQL
	_, err = stmt.Exec("插入示例", 82)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	// 插入成功会显示如下
	fmt.Println("insert success.")
}

// 入口函数
func main() {
	// MysqlObj 结构体初始化
	conn := &db.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}
	// 数据库初始化
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("[INFO] - 已成功连接到数据库!")
	}
	// 关闭数据库对象
	defer conn.Db.Close()

	// 预处理查询
	fmt.Println("预处理查询示例函数 prepareQuery:")
	prepareQuery(conn.Db, 5)

	// 预处理插入
	fmt.Println("预处理插入示例函数 prepareInsert:")
	prepareInsert(conn.Db)
} 

执行结果:

[INFO] - 已成功连接到数据库!

-- 预处理查询示例函数 prepareQuery:
id:6 name:C age:25
id:7 name:C++ age:25
id:8 name:Python age:26
id:9 name:Golang age:16
id:12 name:WeiyiGeek age:18
id:13 name:插入示例 age:82
map[int]mypkg.Person{6:mypkg.Person{Uid:6, Name:"C", Age:25}, 7:mypkg.Person{Uid:7, Name:"C++", Age:25}, 8:mypkg.Person{Uid:8, Name:"Python", Age:26}, 9:mypkg.Person{Uid:9, Name:"Golang", Age:16}, 12:mypkg.Person{Uid:12, Name:"WeiyiGeek", Age:18}, 13:mypkg.Person{Uid:13, Name:"插入示例", Age:18}}

-- 预处理插入示例函数 prepareInsert:
insert success. 

6.MySQL事务处理
什么是事务?

事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元),同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B,这里面就需要执行两次update操作。

在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务, 事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。

 

事务特性复习 ACID

描述: 通常事务必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性: 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

 

事务方法原型
描述:Go语言中使用以下三个方法实现MySQL中的事务操作。

func (db *DB) Begin() (*Tx, error) : 开始事务

func (tx *Tx) Commit() error : 提交事务

func (tx *Tx) Rollback() error : 回滚事务

 

实践示例
描述: 下面的代码演示了一个简单的事务操作,该事物操作能够确保两次更新操作要么同时成功要么同时失败,不会存在中间状态。
例如: A 转账给 B 50 RMB,即从A账号余额-50,B账号余额+50。

数据库表创建:

-- 测试表
create table `money` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(20) DEFAULT '',
  `balance` INT(16) DEFAULT '0',
  PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

-- 测试数据
insert into `test`.`money`(`name`,`balance`) values("WeiyiGeek",1200);
insert into `test`.`money`(`name`,`balance`) values("辛勤的小蜜蜂",3650);

-- 查看插入的测试数据
SELECT * from money;
1	WeiyiGeek	1200
2	辛勤的小蜜蜂	3650 

示例代码:

package main
import (
	"database/sql"
	"fmt"

	"weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// ## 事务操作示例
func transactionDemo(conn *sql.DB, money int) {
	// 开启事务
	tx, err := conn.Begin()
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}

	// (1) A 用户转账 50 给 B 则 - 50
	sqlStr1 := "UPDATE `money` SET balance=balance-? WHERE id=?;"
	ret1, err := tx.Exec(sqlStr1, money, 1)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	// B 用户接收到 A 转账的 50 给 则 + 50
	sqlStr2 := "UPDATE `money` SET balance=balance+? WHERE id=?;"
	ret2, err := tx.Exec(sqlStr2, money, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	// 事务处理影响行数判断是否修改成功
	fmt.Println("事务处理影响行数判断是否修改成功: ", affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事务正在提交啦...")
		tx.Commit() // 提交事务
	} else {
		tx.Rollback()
		fmt.Println("事务回滚啦...")
	}

	fmt.Println("[INFO] - 事务完成了 ,exec trans success!")
}

func main() {
	// (1) MysqlObj 结构体初始化
	conn := &mypkg.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}

	// (2) 数据库初始化
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("[INFO] - 已成功连接到数据库!")
	}
	// 关闭数据库对象
	defer conn.Db.Close()

	// (3) 简单的事务操作示例
	transactionDemo(conn.Db, 50)
} 

执行结果:

[INFO] - 已成功连接到数据库!
事务处理影响行数判断是否修改成功:  1 1
事务正在提交啦...
[INFO] - 事务完成了 ,exec trans success!

# 可以看到用户的在数据库中金额变化
1	WeiyiGeek	1150
2	辛勤的小蜜蜂	3700 

至此使用database/sql标准库操作MySQL数据库完毕!

原文连接: https://mp.weixin.qq.com/s/XfvbsppDMgn3EuN7A5R02Q

YFCMF-TP6

YFCMF-TP6 基于ThinkPHP6+FastAdmin的快速开发框架

主要特性

  • 基于Auth验证的权限管理系统
  • 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
  • 支持单管理员多角色
  • 支持管理子级数据或个人数据
  • 强大的一键生成功能
  • 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
  • 一键压缩打包JS和CSS文件,一键CDN静态资源部署
  • 一键生成控制器菜单和规则
  • 一键生成API接口文档
  • 完善的前端功能组件开发
  • 基于AdminLTE二次开发
  • 基于Bootstrap开发,自适应手机、平板、PC
  • 基于RequireJS进行JS模块管理,按需加载
  • 基于Less进行样式开发
  • 基于Bower进行前端组件包管理
  • 强大的插件扩展功能,在线安装卸载升级插件
  • 通用的会员模块和API模块
  • 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
  • 二级域名部署支持,同时域名支持绑定到插件
  • 多语言支持,服务端及客户端支持
  • 整合第三方短信接口(阿里云、腾讯云短信)
  • 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
  • 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
  • 第三方登录(QQ、微信、微博)整合
  • 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
  • 丰富的插件应用市场

各种设备自适应

响应式的网站设计能够对用户产生友好度,并且对于不同的分辨率能够灵活的进行操作应用。 简洁通俗表达就是页面宽度可以自适应屏幕大小,一个网站 PC、手机、PAD 通吃,页面地址一致。
一个字 “酷 “,可以用 PC 浏览器拉动窗口大小,网站内容显示依旧在设计之内,用户体验非常不错。 一个字 “省”,一个网站 PC、手机、PAD 通吃,这样就不用花那么多心思去维护多个网站,无论是制作还是数据内容。
基于 HTML5 技术
HTML5 对于用户来说,提高了用户体验,加强了视觉感受。HTML5 技术在移动端,能够让应用程序回归到网页,并对网页的功能进行扩展,操作更加简单,用户体验更好。
HTML5 技术跨平台,适配多终端。对于搜索引擎来说,HTML5 新增的标签,使搜索引擎更加容易抓取和索引网页,从而驱动网站获得更多的点击流量。
人性化的后台管理
传统的企业网站管理系统是以技术人员的角度出发,设计了很多复杂的功能,并且操作流程上也很复杂,对于最终要操控这个系统的管理员来说并不是很人性化,YFCMF 所做的只是简化不必要的功能,从操作习惯下合理地布局和设计界面,让最普通的用户,即使没有网站管理的经营,也能很容易上手我们的系统。

问题反馈

在使用中有任何问题,请使用以下联系方式联系我们

交流社区: https://bbs.iuok.cn

QQ群: 345183861

Email: (ice#sbing.vip, 把#换成@)

Github: https://github.com/0377/yfcmf-tp6

Gitee: https://gitee.com/nymondo/yfcmf-tp6

特别鸣谢

感谢以下的项目,排名不分先后

Fastadmin:https://www.fastadmin.net

ThinkPHP:http://www.thinkphp.cn

AdminLTE:https://adminlte.io

Bootstrap:http://getbootstrap.com

jQuery:http://jquery.com

Bootstrap-table:https://github.com/wenzhixin/bootstrap-table

Nice-validator: https://validator.niceue.com

SelectPage: https://github.com/TerryZ/SelectPage

Flask 微型 Python 框架

Flask是一个轻量级的基于Python的web框架。

一.Flask框架的诞生:

Flask诞生于2010年, Armin Ronacher的一个愚人节玩笑。不过现在已经是一个用python语言基于Werkzeug工具箱编写的轻量级web开发框架,它主要面向需求简单,项目周期短的小应用。

二.安装

通过pip3安装Flask即可:

$ sudo pip3 install Flask

进入python交互模式看下Flask的介绍和版本:

$ python3

>>> import flask
>>> print(flask.__doc__)

flask
~~~~~
A microframework based on Werkzeug. It's extensively documented
and follows best practice patterns.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
>>> print(flask.__version__)
1.0.2

3.从 Hello World 开始

本节主要内容:使用Flask写一个显示”Hello World!”的web程序,如何配置、调试Flask。

1 Hello World

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

static和templates目录是默认配置,其中static用来存放静态资源,例如图片、js、css文件等。templates存放模板文件。
我们的网站逻辑基本在server.py文件中,当然,也可以给这个文件起其他的名字。

在server.py中加入以下内容:

from flask import Flask
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
if __name__ == '__main__':
    app.run()

运行server.py:

$ python3 server.py 
 * Running on http://127.0.0.1:5000/

打开浏览器访问http://127.0.0.1:5000/,浏览页面上将出现Hello World!。
终端里会显示下面的信息:

127.0.0.1 - - [16/May/2014 10:29:08] "GET / HTTP/1.1" 200 -

变量app是一个Flask实例,通过下面的方式:

@app.route('/')
def hello_world():
return 'Hello World!'

当客户端访问/时,将响应hello_world()函数返回的内容。注意,这不是返回Hello World!这么简单,Hello World!只是HTTP响应报文的实体部分,状态码等信息既可以由Flask自动处理,也可以通过编程来制定。

2 修改Flask的配置

app = Flask(__name__)

上面的代码中,python内置变量__name__的值是字符串__main__ 。Flask类将这个参数作为程序名称。当然这个是可以自定义的,比如app = Flask(“my-app”)。
Flask默认使用static目录存放静态资源,templates目录存放模板,这是可以通过设置参数更改的:

app = Flask("my-app", static_folder="path1", template_folder="path2")

更多参数请参考__doc__:

from flask import Flask
print(Flask.__doc__)

3 调试模式

上面的server.py中以app.run()方式运行,这种方式下,如果服务器端出现错误是不会在客户端显示的。但是在开发环境中,显示错误信息是很有必要的,要显示错误信息,应该以下面的方式运行Flask:

app.run(debug=True)

将debug设置为True的另一个好处是,程序启动后,会自动检测源码是否发生变化,若有变化则自动重启程序。这可以帮我们省下很多时间。

4 绑定IP和端口

默认情况下,Flask绑定IP为127.0.0.1,端口为5000。我们也可以通过下面的方式自定义:

app.run(host='0.0.0.0', port=80, debug=True)

0.0.0.0代表电脑所有的IP。80是HTTP网站服务的默认端口。什么是默认?比如,我们访问网站http://www.example.com,其实是访问的http://www.example.com:80,只不过:80可以省略不写。

由于绑定了80端口,需要使用root权限运行server.py。也就是:

$ sudo python3 server.py

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-001

 

4.获取 URL 参数

URL参数是出现在url中的键值对,例如http://127.0.0.1:5000/?disp=3中的url参数是{'disp':3}

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 列出所有的url参数

在server.py中添加以下内容:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return request.args.__str__()
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中访问http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,将显示:

ImmutableMultiDict([('user', 'Flask'), ('time', ''), ('p', '7'), ('p', '8')])

较新的浏览器也支持直接在url中输入中文(最新的火狐浏览器内部会帮忙将中文转换成符合URL规范的数据),在浏览器中访问http://127.0.0.1:5000/?info=这是爱,,将显示:

ImmutableMultiDict([('info', '这是爱,')])

浏览器传给我们的Flask服务的数据长什么样子呢?可以通过request.full_path和request.path来看一下:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    print(request.path)
    print(request.full_path)
    return request.args.__str__()
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器访问http://127.0.0.1:5000/?info=这是爱,,运行server.py的终端会输出:

/
/?info=%E8%BF%99%E6%98%AF%E7%88%B1%EF%BC%8C

3 获取某个指定的参数

例如,要获取键info对应的值,如下修改server.py:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return request.args.get('info')
 
if __name__ == '__main__':
    app.run(port=5000)

运行server.py,在浏览器中访问http://127.0.0.1:5000/?info=hello,浏览器将显示:

hello

不过,当我们访问http://127.0.0.1:5000/时候却出现了500错误,浏览器显示:

如果开启了Debug模式,会显示:

为什么为这样?

这是因为没有在URL参数中找到info。所以request.args.get(‘info’)返回Python内置的None,而Flask不允许返回None。

解决方法很简单,我们先判断下它是不是None:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('info')
    if r==None:
        # do something
        return ''
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

另外一个方法是,设置默认值,也就是取不到数据时用这个值:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('info', 'hi')
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

函数request.args.get的第二个参数用来设置默认值。此时在浏览器访问http://127.0.0.1:5000/,将显示:

hi

4 如何处理多值

还记得上面有一次请求是这样的吗? http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,仔细看下,p有两个值。

如果我们的代码是:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('p')
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中请求时,我们只会看到7。如果我们需要把p的所有值都获取到,该怎么办?

不用get,用getlist:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    r = request.args.getlist('p')  # 返回一个list
    return str(r)
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器输入 http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,我们会看到[‘7’, ‘8’]。

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-002

五.获取POST方法传送的数据

作为一种HTTP请求方法,POST用于向指定的资源提交要被处理的数据。我们在某网站注册用户、写文章等时候,需要将数据传递到网站服务器中。并不适合将数据放到URL参数中,密码放到URL参数中容易被看到,文章数据又太多,浏览器不一定支持太长长度的URL。这时,一般使用POST方法。

本文使用python的requests库模拟浏览器。
安装方法:

$ sudo pip3 install requests

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

<h3″>2 查看POST数据内容

以用户注册为例子,我们需要向服务器/register传送用户名name和密码password。如下编写HelloWorld/server.py。

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/register', methods=['POST'])
def register():
    print(request.headers)
    print(request.stream.read())
    return 'welcome'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

`@app.route(‘/register’, methods=[‘POST’])是指url/register只接受POST方法。可以根据需要修改methods`参数,例如如果想要让它同时支持GET和POST,这样写:

@app.route('/register', methods=['GET', 'POST']) 

浏览器模拟工具client.py内容如下:

import requests

user_info = {'name': 'letian', 'password': '123'}
r = requests.post("http://127.0.0.1:5000/register", data=user_info)

print(r.text)

运行HelloWorld/server.py,然后运行client.py。client.py将输出:

welcome

而HelloWorld/server.py在终端中输出以下调试信息(通过print输出):

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
 
 
b'name=letian&password=123'

前6行是client.py生成的HTTP请求头,由print(request.headers)输出。

请求体的数据,我们通过print(request.stream.read())输出,结果是:

b'name=letian&password=123'

3 解析POST数据

上面,我们看到post的数据内容是:

b'name=letian&password=123'

我们要想办法把我们要的name、password提取出来,怎么做呢?自己写?不用,Flask已经内置了解析器request.form。

我们将服务代码改成:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/register', methods=['POST'])
def register():
    print(request.headers)
    # print(request.stream.read()) # 不要用,否则下面的form取不到数据
    print(request.form)
    print(request.form['name'])
    print(request.form.get('name'))
    print(request.form.getlist('name'))
    print(request.form.get('nickname', default='little apple'))
    return 'welcome'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

执行client.py请求数据,服务器代码会在终端输出:

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
 
 
ImmutableMultiDict([('name', 'letian'), ('password', '123')])
letian
letian
['letian']
little apple

request.form会自动解析数据。

request.form[‘name’]和request.form.get(‘name’)都可以获取name对应的值。对于request.form.get()可以为参数default指定值以作为默认值。所以:

print(request.form.get('nickname', default='little apple'))

输出的是默认值

little apple

如果name有多个值,可以使用request.form.getlist(‘name’),该方法将返回一个列表。我们将client.py改一下:

import requests
 
user_info = {'name': ['letian', 'letian2'], 'password': '123'}
r = requests.post("http://127.0.0.1:5000/register", data=user_info)
 
print(r.text)

此时运行client.py,print(request.form.getlist(‘name’))将输出:

[u'letian', u'letian2']

4 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-003

六. 处理和响应JSON数据

使用 HTTP POST 方法传到网站服务器的数据格式可以有很多种,比如「5. 获取POST方法传送的数据」讲到的name=letian&password=123这种用过&符号分割的key-value键值对格式。我们也可以用JSON格式、XML格式。相比XML的重量、规范繁琐,JSON显得非常小巧和易用。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 处理JSON格式的请求数据

如果POST的数据是JSON格式,request.json会自动将json数据转换成Python类型(字典或者列表)。

编写server.py:

from flask import Flask, request
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    print(request.headers)
    print(type(request.json))
    print(request.json)
    result = request.json['a'] + request.json['b']
    return str(result)
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

编写client.py模拟浏览器请求:

import requests
 
json_data = {'a': 1, 'b': 2}
 
r = requests.post("http://127.0.0.1:5000/add", json=json_data)
 
print(r.text)

运行server.py,然后运行client.py,client.py 会在终端输出:

3

server.py 会在终端输出:

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 16
Content-Type: application/json
 
 
<class 'dict'>
{'a': 1, 'b': 2}

注意,请求头中Content-Type的值是application/json。

3 响应JSON-方案1

响应JSON时,除了要把响应体改成JSON格式,响应头的Content-Type也要设置为application/json。
编写server2.py:

from flask import Flask, request, Response
import json
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    return Response(json.dumps(result),  mimetype='application/json')
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

修改后运行。

编写client2.py:

import requests
 
json_data = {'a': 1, 'b': 2}
 
r = requests.post("http://127.0.0.1:5000/add", json=json_data)
 
print(r.headers)
print(r.text)

运行client.py,将显示:

{'Content-Type': 'application/json', 'Content-Length': '10', 'Server': 'Werkzeug/0.14.1 Python/3.6.4', 'Date': 'Sat, 07 Jul 2018 05:23:00 GMT'}
{"sum": 3}

上面第一段内容是服务器的响应头,第二段内容是响应体,也就是服务器返回的JSON格式数据。

另外,如果需要服务器的HTTP响应头具有更好的可定制性,比如自定义Server,可以如下修改add()函数:

@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    resp = Response(json.dumps(result),  mimetype='application/json')
    resp.headers.add('Server', 'python flask')
    return resp

client2.py运行后会输出:

{'Content-Type': 'application/json', 'Content-Length': '10', 'Server': 'python flask', 'Date': 'Sat, 07 Jul 2018 05:26:40 GMT'}
{"sum": 3}

4 响应JSON-方案2

使用 jsonify 工具函数即可。

from flask import Flask, request, jsonify
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    return jsonify(result)
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-004

七. 上传文件

上传文件,一般也是用POST方法。

7.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 上传文件

这一部分的代码参考自How to upload a file to the server in Flask。

我们以上传图片为例:
假设将上传的图片只允许’png’、’jpg’、’jpeg’、’gif’这四种格式,通过url/upload使用POST上传,上传的图片存放在服务器端的static/uploads目录下。

首先在项目HelloWorld中创建目录static/uploads:

mkdir HelloWorld/static/uploads

werkzeug库可以判断文件名是否安全,例如防止文件名是../../../a.png,安装这个库:

$ sudo pip3 install werkzeug

server.py代码:

from flask import Flask, request
 
from werkzeug.utils import secure_filename
import os
 
app = Flask(__name__)
 
# 文件上传目录
app.config['UPLOAD_FOLDER'] = 'static/uploads/'
# 支持的文件格式
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}  # 集合类型
 
 
# 判断文件名是否是我们支持的格式
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in app.config['ALLOWED_EXTENSIONS']
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/upload', methods=['POST'])
def upload():
    upload_file = request.files['image']
    if upload_file and allowed_file(upload_file.filename):
        filename = secure_filename(upload_file.filename)
        # 将文件保存到 static/uploads 目录,文件名同上传时使用的文件名
        upload_file.save(os.path.join(app.root_path, app.config['UPLOAD_FOLDER'], filename))
        return 'info is '+request.form.get('info', '')+'. success'
    else:
        return 'failed'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

app.config中的config是字典的子类,可以用来设置自有的配置信息,也可以设置自己的配置信息。函数allowed_file(filename)用来判断filename是否有后缀以及后缀是否在app.config[‘ALLOWED_EXTENSIONS’]中。

客户端上传的图片必须以image01标识。upload_file是上传文件对应的对象。app.root_path获取server.py所在目录在文件系统中的绝对路径。upload_file.save(path)用来将upload_file保存在服务器的文件系统中,参数最好是绝对路径,否则会报错(网上很多代码都是使用相对路径,但是笔者在使用相对路径时总是报错,说找不到路径)。函数os.path.join()用来将使用合适的路径分隔符将路径组合起来。

好了,定制客户端client.py:

import requests
 
file_data = {'image': open('Lenna.jpg', 'rb')}
 
user_info = {'info': 'Lenna'}
 
r = requests.post("http://127.0.0.1:5000/upload", data=user_info, files=file_data)
 
print(r.text)

运行client.py,当前目录下的Lenna.jpg将上传到服务器。

然后,我们可以在static/uploads中看到文件Lenna.jpg。

要控制上产文件的大小,可以设置请求实体的大小,例如:

app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 #16MB

不过,在处理上传文件时候,需要使用try:…except:…。

如果要获取上传文件的内容可以:

file_content = request.files['image'].stream.read()

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-005

八. Restful URL

简单来说,Restful URL可以看做是对 URL 参数的替代。

8.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

编辑server.py:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user/')
def user(username):
    print(username)
    print(type(username))
    return 'hello ' + username
 
 
@app.route('/user//friends')
def user_friends(username):
    print(username)
    print(type(username))
    return 'hello ' + username
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

运行HelloWorld/server.py。使用浏览器访问http://127.0.0.1:5000/user/letian,HelloWorld/server.py将输出:

letian
<class 'str'>

而访问http://127.0.0.1:5000/user/letian/,响应为404 Not Found。

浏览器访问http://127.0.0.1:5000/user/letian/friends,可以看到:

Hello letian. They are your friends.

HelloWorld/server.py输出:

letian
<class 'str'>

3 转换类型

由上面的示例可以看出,使用 Restful URL 得到的变量默认为str对象。如果我们需要通过分页显示查询结果,那么需要在url中有数字来指定页数。按照上面方法,可以在获取str类型页数变量后,将其转换为int类型。不过,还有更方便的方法,就是用flask内置的转换机制,即在route中指定该如何转换。

新的服务器代码:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/')
def page(num):
    print(num)
    print(type(num))
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

`@app.route(‘/page/int:num‘)`会将num变量自动转换成int类型。

运行上面的程序,在浏览器中访问http://127.0.0.1:5000/page/1,HelloWorld/server.py将输出如下内容:

1
<class 'int'>

如果访问的是http://127.0.0.1:5000/page/asd,我们会得到404响应。

在官方资料中,说是有3个默认的转换器:

int     accepts integers
float     like int but for floating point values
path     like the default but also accepts slashes

看起来够用了。

4 一个有趣的用法

如下编写服务器代码:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/-')
def page(num1, num2):
    print(num1)
    print(num2)
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中访问http://127.0.0.1:5000/page/11-22,HelloWorld/server.py会输出:

11
22

5 编写转换器

自定义的转换器是一个继承werkzeug.routing.BaseConverter的类,修改to_python和to_url方法即可。to_python方法用于将url中的变量转换后供被`@app.route包装的函数使用,to_url方法用于flask.url_for`中的参数转换。

下面是一个示例,将HelloWorld/server.py修改如下:

from flask import Flask, url_for
 
from werkzeug.routing import BaseConverter
 
 
class MyIntConverter(BaseConverter):
 
    def __init__(self, url_map):
        super(MyIntConverter, self).__init__(url_map)
 
    def to_python(self, value):
        return int(value)
 
    def to_url(self, value):
        return value * 2
 
 
app = Flask(__name__)
app.url_map.converters['my_int'] = MyIntConverter
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/')
def page(num):
    print(num)
    print(url_for('page', num=123))   # page 对应的是 page函数 ,num 对应对应`/page/`中的num,必须是str
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器访问http://127.0.0.1:5000/page/123后,HelloWorld/server.py的输出信息是:

123
/page/123123

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-006

7 值得读

理解RESTful架构

9. 使用url_for生成链接

工具函数url_for可以让你以软编码的形式生成url,提供开发效率。

9.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

编辑HelloWorld/server.py:

from flask import Flask, url_for
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    pass
 
@app.route('/user/')
def user(name):
    pass
 
@app.route('/page/')
def page(num):
    pass
 
@app.route('/test')
def test():
    print(url_for('hello_world'))
    print(url_for('user', name='letian'))
    print(url_for('page', num=1, q='hadoop mapreduce 10%3'))
    print(url_for('static', filename='uploads/01.jpg'))
    return 'Hello'
 
if __name__ == '__main__':
    app.run(debug=True)

运行HelloWorld/server.py。然后在浏览器中访问http://127.0.0.1:5000/test,HelloWorld/server.py将输出以下信息:

/
/user/letian
/page/1?q=hadoop+mapreduce+10%253
/static/uploads/01.jpg

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-007

十. 使用redirect重定向网址

redirect函数用于重定向,实现机制很简单,就是向客户端(浏览器)发送一个重定向的HTTP报文,浏览器会去访问报文中指定的url。

10.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

使用redirect时,给它一个字符串类型的参数就行了。
编辑HelloWorld/server.py:

from flask import Flask, url_for, redirect
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'hello world'
 
@app.route('/test1')
def test1():
    print('this is test1')
    return redirect(url_for('test2'))
 
@app.route('/test2')
def test2():
    print('this is test2')
    return 'this is test2'
 
if __name__ == '__main__':
    app.run(debug=True)

运行HelloWorld/server.py,在浏览器中访问http://127.0.0.1:5000/test1,浏览器的url会变成http://127.0.0.1:5000/test2,并显示:

this is test2

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-008

十一. 使用Jinja2模板引擎

模板引擎负责MVC中的V(view,视图)这一部分。Flask默认使用Jinja2模板引擎。

Flask与模板相关的函数有:

  • flask.render_template(template_name_or_list, **context)
    Renders a template from the template folder with the given context.
  • flask.render_template_string(source, **context)
    Renders a template from the given template source string with the given context.
  • flask.get_template_attribute(template_name, attribute)
    Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code.

这其中常用的就是前两个函数。

这个实例中使用了模板继承、if判断、for循环。

11.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 创建并编辑HelloWorld/templates/default.html

内容如下:

    

 

    {% block body %}{% endblock %}
 
 
```
 
可以看到,在``标签中使用了if判断,如果给模板传递了`page_title`变量,显示之,否则,不显示。
 
``标签中定义了一个名为`body`的block,用来被其他模板文件继承。
 
### 11.3 创建并编辑HelloWorld/templates/user_info.html
内容如下:
 
```
{% extends "default.html" %}
 
{% block body %}
    {% for key in user_info %}
 
        {{ key }}: {{ user_info[key] }} 
 
 
    {% endfor %}
{% endblock %}

变量user_info应该是一个字典,for循环用来循环输出键值对。

4 编辑HelloWorld/server.py

内容如下:

from flask import Flask, render_template
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    user_info = {
        'name': 'letian',
        'email': '[email protected]',
        'age':0,
        'github': 'https://github.com/letiantian'
    }
    return render_template('user_info.html', page_title='letian\'s info', user_info=user_info)
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

render_template()函数的第一个参数指定模板文件,后面的参数是要传递的数据。

5 运行与测试

运行HelloWorld/server.py:

$ python3 HelloWorld/server.py

在浏览器中访问http://127.0.0.1:5000/user,效果图如下:

查看网页源码:

        name: letian 
        email: [email protected] 
        age: 0 
        github: https://github.com/letiantian 

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-009

十二. 自定义404等错误的响应

要处理HTTP错误,可以使用flask.abort函数。

1 示例1:简单入门

建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

代码

编辑HelloWorld/server.py:

from flask import Flask, render_template_string, abort
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    abort(401)  # Unauthorized 未授权
    print('Unauthorized, 请先登录')
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

效果

运行HelloWorld/server.py,浏览器访问http://127.0.0.1:5000/user,效果如下:

要注意的是,HelloWorld/server.py中abort(401)后的print并没有执行。

2 示例2:自定义错误页面

代码
将服务器代码改为:

from flask import Flask, render_template_string, abort
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    abort(401)  # Unauthorized


@app.errorhandler(401)
def page_unauthorized(error):
    return render_template_string('<h1> Unauthorized </h1><h2>{{ error_info }}</h2>', error_info=error), 401
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

page_unauthorized函数返回的是一个元组,401 代表HTTP 响应状态码。如果省略401,则响应状态码会变成默认的 200。

效果

运行HelloWorld/server.py,浏览器访问http://127.0.0.1:5000/user,效果如下:

 

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-010

十三. 用户会话

session用来记录用户的登录状态,一般基于cookie实现。

下面是一个简单的示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编辑HelloWorld/server.py

内容如下:

from flask import Flask, render_template_string, \
    session, request, redirect, url_for
 
app = Flask(__name__)
 
app.secret_key = 'F12Zr47j\3yX R~X@H!jLwf/T'
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/login')
def login():
    page = '''
        <form action="{{ url_for('do_login') }}" method="post">
        <p>name: <input type="text" name="user_name" /></p>
        <input type="submit" value="Submit" />
    </form>
    '''
return render_template_string(page)

@app.route('/do_login', methods=['POST'])
def do_login():
    name = request.form.get('user_name')
    session['user_name'] = name
    return 'success'
 
 
@app.route('/show')
def show():
    return session['user_name']
 
 
@app.route('/logout')
def logout():
    session.pop('user_name', None)
    return redirect(url_for('login'))
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

3 代码的含义

app.secret_key用于给session加密。

/login中将向用户展示一个表单,要求输入一个名字,submit后将数据以post的方式传递给/do_login/do_login将名字存放在session中。

如果用户成功登录,访问/show时会显示用户的名字。此时,打开firebug等调试工具,选择session面板,会看到有一个cookie的名称为session

/logout用于登出,通过将session中的user_name字段pop即可。Flask中的session基于字典类型实现,调用pop方法时会返回pop的键对应的值;如果要pop的键并不存在,那么返回值是pop()的第二个参数。

另外,使用redirect()重定向时,一定要在前面加上return

4 效果

进入http://127.0.0.1:5000/login,输入name,点击submit:

进入http://127.0.0.1:5000/show查看session中存储的name:

5 设置sessin的有效时间

下面这段代码来自Is there an easy way to make sessions timeout in flask?

from datetime import timedelta
from flask import session, app
 
session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=5)

这段代码将session的有效时间设置为5分钟。

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-011

十四. 使用Cookie

Cookie是存储在客户端的记录访问者状态的数据。具体原理,请见 http://zh.wikipedia.org/wiki/Cookie 。 常用的用于记录用户登录状态的session大多是基于cookie实现的。
cookie可以借助flask.Response来实现。下面是一个示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 代码

修改HelloWorld/server.py:

from flask import Flask, request, Response, make_response
import time
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/add')
def login():
    res = Response('add cookies')
    res.set_cookie(key='name', value='letian', expires=time.time()+6*60)
    return res
 
 
@app.route('/show')
def show():
    return request.cookies.__str__()
 
 
@app.route('/del')
def del_cookie():
    res = Response('delete cookies')
    res.set_cookie('name', '', expires=0)
    return res
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

由上可以看到,可以使用Response.set_cookie添加和删除cookie。expires参数用来设置cookie有效时间,它的值可以是datetime对象或者unix时间戳,笔者使用的是unix时间戳。

res.set_cookie(key='name', value='letian', expires=time.time()+6*60)

上面的expire参数的值表示cookie在从现在开始的6分钟内都是有效的。

要删除cookie,将expire参数的值设为0即可:

res.set_cookie('name', '', expires=0)

set_cookie()函数的原型如下:

set_cookie(key, value=’’, max_age=None, expires=None, path=’/‘, domain=None, secure=None, httponly=False)

Sets a cookie. The parameters are the same as in the cookie Morsel object in the Python standard library but it accepts unicode data, too.
Parameters:

     key – the key (name) of the cookie to be set.
value – the value of the cookie.
max_age – should be a number of seconds, or None (default) if the cookie should last only as long as the client’s browser session.

     expires – should be a datetime object or UNIX timestamp.

     domain – if you want to set a cross-domain cookie. For example, domain=”.example.com” will set a cookie that is readable by the domain www.example.com, foo.example.com etc. Otherwise, a cookie will only be readable by the domain that set it.

     path – limits the cookie to a given path, per default it will span the whole domain.

3 运行与测试

运行HelloWorld/server.py:

$ python3 HelloWorld/server.py

使用浏览器打开http://127.0.0.1:5000/add,浏览器界面会显示

add cookies

下面查看一下cookie,如果使用firefox浏览器,可以用firebug插件查看。打开firebug,选择Cookies选项,刷新页面,可以看到名为name的cookie,其值为letian。

在“网络”选项中,可以查看响应头中类似下面内容的设置cookie的HTTP「指令」:

Set-Cookie: name=letian; Expires=Sun, 29-Jun-2014 05:16:27 GMT; Path=/

在cookie有效期间,使用浏览器访问http://127.0.0.1:5000/show,可以看到:

{'name': 'letian'}

4 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-012

十五. 闪存系统 flashing system

Flask的闪存系统(flashing system)用于向用户提供反馈信息,这些反馈信息一般是对用户上一次操作的反馈。反馈信息是存储在服务器端的,当服务器向客户端返回反馈信息后,这些反馈信息会被服务器端删除。

下面是一个示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写HelloWorld/server.py

内容如下:

from flask import Flask, flash, get_flashed_messages
import time
 
app = Flask(__name__)
app.secret_key = 'some_secret'
 
 
@app.route('/')
def index():
    return 'hi'
 
 
@app.route('/gen')
def gen():
    info = 'access at '+ time.time().__str__()
    flash(info)
    return info
 
 
@app.route('/show1')
def show1():
    return get_flashed_messages().__str__()
 
 
@app.route('/show2')
def show2():
    return get_flashed_messages().__str__()
 
 
if __name__ == "__main__":
    app.run(port=5000, debug=True)

3 效果

运行服务器:

$ python3 HelloWorld/server.py

打开浏览器,访问http://127.0.0.1:5000/gen,浏览器界面显示(注意,时间戳是动态生成的,每次都会不一样,除非并行访问):

access at 1404020982.83

查看浏览器的cookie,可以看到session,其对应的内容是:

.eJyrVopPy0kszkgtVrKKrlZSKIFQSUpWSknhYVXJRm55UYG2tkq1OlDRyHC_rKgIvypPdzcDTxdXA1-XwHLfLEdTfxfPUn8XX6DKWCAEAJKBGq8.BpE6dg.F1VURZa7VqU9bvbC4XIBO9-3Y4Y

再一次访问http://127.0.0.1:5000/gen,浏览器界面显示:

access at 1404021130.32

cookie中session发生了变化,新的内容是:

.eJyrVopPy0kszkgtVrKKrlZSKIFQSUpWSknhYVXJRm55UYG2tkq1OlDRyHC_rKgIvypPdzcDTxdXA1-XwHLfLEdTfxfPUn8XX6DKWLBaMg1yrfCtciz1rfIEGxRbCwAhGjC5.BpE7Cg.Cb_B_k2otqczhknGnpNjQ5u4dqw

然后使用浏览器访问http://127.0.0.1:5000/show1,浏览器界面显示:

['access at 1404020982.83', 'access at 1404021130.32']

这个列表中的内容也就是上面的两次访问http://127.0.0.1:5000/gen得到的内容。此时,cookie中已经没有session了。

如果使用浏览器访问http://127.0.0.1:5000/show1或者http://127.0.0.1:5000/show2,只会得到:

[]

4 高级用法

flash系统也支持对flash的内容进行分类。修改HelloWorld/server.py内容:

from flask import Flask, flash, get_flashed_messages
import time
 
app = Flask(__name__)
app.secret_key = 'some_secret'
 
 
@app.route('/')
def index():
    return 'hi'
 
 
@app.route('/gen')
def gen():
    info = 'access at '+ time.time().__str__()
    flash('show1 '+info, category='show1')
    flash('show2 '+info, category='show2')
    return info
 
 
@app.route('/show1')
def show1():
    return get_flashed_messages(category_filter='show1').__str__()
 
@app.route('/show2')
def show2():
    return get_flashed_messages(category_filter='show2').__str__()
 
 
if __name__ == "__main__":
    app.run(port=5000, debug=True)

某一时刻,浏览器访问http://127.0.0.1:5000/gen,浏览器界面显示:

access at 1404022326.39

不过,由上面的代码可以知道,此时生成了两个flash信息,但分类(category)不同。

使用浏览器访问http://127.0.0.1:5000/show1,得到如下内容:

['1 access at 1404022326.39']

而继续访问http://127.0.0.1:5000/show2,得到的内容为空:

[]

5 在模板文件中获取flash的内容

在Flask中,get_flashed_messages()默认已经集成到Jinja2模板引擎中,易用性很强。下面是来自官方的一个示例:

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-013

gin+gorm+router 快速搭建 crud restful API 接口

gin mysql_gin+gorm+router 快速搭建 crud restful API 接口

下载扩展

go get github.com/go-sql-driver/mysql 
go get github.com/jinzhu/gorm
go get github.com/gin-gonic/gin

建表语句

CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`password` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

结构

├──api
│ ├── apis 
│ │ └── user.go 
│ ├── database 
│ │ └── mysql.go 
│ ├── models 
│ │ └── user.go 
│ └── router 
│ └── router.go
└──main.go

apis/apis/user.go

package apis

import (
    "github.com/gin-gonic/gin"
    model "api/models"
    "net/http"
    "strconv"
)

//列表数据
func Users(c *gin.Context) {
    var user model.User
    user.Username = c.Request.FormValue("username")
    user.Password = c.Request.FormValue("password")
    result, err := user.Users()

    if err != nil {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "抱歉未找到相关信息",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data":   result,
    })
}

//添加数据
func Store(c *gin.Context) {
    var user model.User
    user.Username = c.Request.FormValue("username")
    user.Password = c.Request.FormValue("password")
    id, err := user.Insert()

    if err != nil {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "添加失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "添加成功",
        "data":    id,
    })
}

//修改数据
func Update(c *gin.Context) {
    var user model.User
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    user.Password = c.Request.FormValue("password")
    result, err := user.Update(id)
    if err != nil || result.ID == 0 {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "修改失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "修改成功",
    })
}

//删除数据
func Destroy(c *gin.Context) {
    var user model.User
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    result, err := user.Destroy(id)
    if err != nil || result.ID == 0 {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "删除失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "删除成功",
    })
}

database/mysql.go

package database

import (
    _ "github.com/go-sql-driver/mysql" //加载mysql
    "github.com/jinzhu/gorm"
    "fmt"
)

var Eloquent *gorm.DB

func init() {
    var err error
    Eloquent, err = gorm.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local&timeout=10ms")

    if err != nil {
        fmt.Printf("mysql connect error %v", err)
    }

    if Eloquent.Error != nil {
        fmt.Printf("database error %v", Eloquent.Error)
    }
}

models/user.go

    package models

    import (
        orm "api/database"
    )

    type User struct {
        ID       int64  `json:"id"`       // 列名为 `id`
        Username string `json:"username"` // 列名为 `username`
        Password string `json:"password"` // 列名为 `password`
    }

    var Users []User

    //添加
    func (user User) Insert() (id int64, err error) {

        //添加数据
        result := orm.Eloquent.Create(&user)
        id =user.ID
        if result.Error != nil {
            err = result.Error
            return
        }
        return
    }

    //列表
    func (user *User) Users() (users []User, err error) {
        if err = orm.Eloquent.Find(&users).Error; err != nil {
            return
        }
        return
    }

    //修改
    func (user *User) Update(id int64) (updateUser User, err error) {

        if err = orm.Eloquent.Select([]string{"id", "username"}).First(&updateUser, id).Error; err != nil {
            return
        }

        //参数1:是要修改的数据
        //参数2:是修改的数据
        if err = orm.Eloquent.Model(&updateUser).Updates(&user).Error; err != nil {
            return
        }
        return
    }

    //删除数据
    func (user *User) Destroy(id int64) (Result User, err error) {

        if err = orm.Eloquent.Select([]string{"id"}).First(&user, id).Error; err != nil {
            return
        }

        if err = orm.Eloquent.Delete(&user).Error; err != nil {
            return
        }
        Result = *user
        return
    }

router/router.go

package router

import (
    "github.com/gin-gonic/gin"
    . "api/apis"
)

func InitRouter() *gin.Engine {
    router := gin.Default()

    router.GET("/users", Users)

    router.POST("/user", Store)

    router.PUT("/user/:id", Update)

    router.DELETE("/user/:id", Destroy)

    return router
}

main.go

    package main

    import (
        _ "api/database"
        "api/router"
        orm "api/database"
    )

    func main() {
        defer orm.Eloquent.Close()
        router := router.InitRouter()
        router.Run(":8000")
    }

执行 go run main.go

访问地址

    • POST localhost:8006/user 添加
    • GET localhost:8006/users 列表
    • DELETE localhost:8006/user/id 删除
    • PUT localhost:8006/user/id 修改

将Vue项目打包为Windows应用(.exe)

背景

朋友是做商品零售,每月都需要将销售数据汇总至年度销售表格中,在这个过程中存在很多重复性的工作,无奈中。在一次聊天中,我了解到他的需求,就用 Vue 做了一个页面,可以实现 Excel 转成 JSON 进行操作,最后再将 JSON 转成 Excel ,虽然后来了解到用 Python应该会更高效,待日后来研究!

不过咱好歹有个图形界面,用户体验好!(自我安慰一波~)

接下来问题便来了,朋友完全不懂编程,每次都准备开发环境也挺麻烦,便想着能不能做成可执行文件.exe,直接双击安装,生成快捷方式,直接就能用,人性化点赞!

1.首先从electron官网克隆一个demo

选择一个你想存放项目的盘。(可以不用新建文件夹,看个人)直接运行cmd;

注意这里的最好是npm的依赖包

npm与cnpm的区别

  • 说到npm与cnpm的区别,可能大家都知道,但大家容易忽视的一点,是cnpm装的各种node_module,这种方式下所有的包都是扁平化的安装。
  • 一下子node_modules展开后有非常多的文件。导致了在打包的过程中非常慢。但是如果改用npm来安装node_modules的话,所有的包都是树状结构的,层级变深。
  • 由于这个不同,对一些项目比较大的应用,很容易出现打包过程慢且node内存溢出的问题
  • 所以建议大家在打包前,讲使用cnpm安装的依赖包删除,替换成npm安装的依赖包。
git clone https://github.com/electron/electron-quick-start
cd electron-quick-start
cnpm install //npm,cnpm 都可以,cnpm速度较快.
npm start

项目跑起来以后, 就会出现electron的桌面页面,找到clone下来项目的入口文件main.js 和package.json.接下来修改路径和配置。

//----main.js----
function createWindow () {

// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/../dist/index.html`) //修改这里

2. 接下来,在已创建好的vue-cli项目中

安装electron依赖,运行如下命令:

npm install electron --save-dev
npm install electron-packager --save-dev

现在将clone项目中的main.js拷到刚刚新建的项目中的build文件夹下,并重命名为electron.js  , 并更改config/index.js中生产模式下(build)的assetsPublicPth

 build: {
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',

assetsPublicPath: './', //这里改为./

3. 在新建的项目的package.json文件中增加一条指令

如下:

 "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"build": "node build/build.js",
"electron_dev": "npm run build && electron build/electron.js" //增加的指令

接着执行:

npm run build //生成dist目录(包含静态资源文件) 
npm run electron_dev //启动electron

现在,生成桌面应用基本成功实现了,还剩下最后一步:打包!

首先,复制build目录下的electron.js到dist目录中,注意很关键的一步是复制过来之后,要调整一下loadURL路径的格式,

像这样:

function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({

})

// and load the index.html of the app.
mainWindow.loadURL(`${__dirname}/index.html`) //--修改的--

接着,复制clone例子的package.json到新建项目的dist目录中。在项目的package.json中(注意不是dist下的package.json)为之前下载好的electron-packager,增加一条启动命令。

 "build": "node build/build.js",
"electron_dev": "npm run build && electron build/electron.js",
"electron_build": "electron-packager ./dist/ --platform=win32 --arch=ia32 --icon=./src/assets/yizhu.ico --overwrite" //--新增的命令--

接着,如果你要替换应用图标的话,就在项目中的scr文件夹下的assets目录下,放入你要设置的exe文件的图标,为.ico格式。

这里指的注意的是,你的ico图标是什么名称,上一条的electron_build里面的路径最后就要改成你图标的名称,像这里的yizhu.ico一样,yizhu.ico就是我自己图标的名称。(这点很重要!)

这里我要强调一点, 有同学到这里运行报错, 很有可能是你的图片路径没改过来. 还有一点就是你把自己的图片强行修改为ico格式了,这点是不允许的. 一定要是原生的ico格式的图标. 且看我最下面截图的ico的图标是怎样的. 这里我附上一个转为ico格式的链接. 操作简单.

最后,运行

npm run build //刷新静态资源文件
npm run electron_build //启动

这个时候已经生成了aps-win32-ia32文件夹,找到里面的helloworld.exe文件即可运行。当然,我这里没有给文件重命名,你们可以自行命名。

到这里,exe文件已经最终完成。