Gin与Mysql实现简单Restful风格API实战示例详解

目录
  • It works
    • main.go
    • 编译运行
  • 数据库
  • CURD 增删改查
      • 查询列表 Query
      • 查询单条记录 QueryRow
  • 组织代码
    • 封装模型方法
    • Handler函数
  • 组织项目
    • 数据库处理
    • 数据model封装
    • handler
    • 路由
    • 分组路由
    • app入口
  • 总结

我们已经了解了Golang的Gin框架。对于Webservice服务,restful风格几乎一统天下。Gin也天然的支持restful。下面就使用gin写一个简单的服务,麻雀虽小,五脏俱全。我们先以一个单文件开始,然后再逐步分解模块成包,组织代码。

It works

使用Gin的前提是安装,我们需要安装gin和mysql的驱动,具体的安装方式就不在赘述。

参考Golang 微框架Gin简介和Golang持久化。

创建一个文件夹用来为项目,新建一个文件main.go:

☁  newgin  tree
.
└── main.go

main.go

package main
import (
 "gopkg.in/gin-gonic/gin.v1"
 "net/http"
)
func main() {
 router := gin.Default()
 router.GET("/", func(c *gin.Context) {
  c.String(http.StatusOK, "It works")
 })
 router.Run(":8000")
}

编译运行

☁  newgin  go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env: export GIN_MODE=release
 - using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8000
 

访问 /即可看见我们返回的字串It works

数据库

安装完毕框架,完成一次请求响应之后。接下来就是安装数据库驱动和初始化数据相关的操作了。首先,我们需要新建数据表。一个及其简单的数据表:

CREATE TABLE `person` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(40) NOT NULL DEFAULT '',
  `last_name` varchar(40) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建数据表之后,初始化数据库连接池:

func main() {
 db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
 if err != nil{
  log.Fatalln(err)
 }
defer db.Close()
 db.SetMaxIdleConns(20)
 db.SetMaxOpenConns(20)
 if err := db.Ping(); err != nil{
  log.Fatalln(err)
 }
 router := gin.Default()
 router.GET("/", func(c *gin.Context) {
  c.String(http.StatusOK, "It works")
 })
 router.Run(":8000")
}

使用sql.Open方法会创建一个数据库连接池db。这个db不是数据库连接,它是一个连接池,只有当真正数据库通信的时候才创建连接。例如这里的db.Ping的操作。db.SetMaxIdleConns(20)db.SetMaxOpenConns(20)分别设置数据库的空闲连接和最大打开连接,即向Mysql服务端发出的所有连接的最大数目。

如果不设置,默认都是0,表示打开的连接没有限制。我在压测的时候,发现会存在大量的TIME_WAIT状态的连接,虽然mysql的连接数没有上升。设置了这两个参数之后,不在存在大量TIME_WAIT状态的连接了。而且qps也没有明显的变化,出于对数据库的保护,最好设置这连个参数。

CURD 增删改查

Restful的基本就是对资源的curd操作。下面开启我们的第一个api接口,增加一个资源。

func main() {
 ...
 router.POST("/person", func(c *gin.Context) {
  firstName := c.Request.FormValue("first_name")
  lastName := c.Request.FormValue("last_name")
  rs, err := db.Exec("INSERT INTO person(first_name, last_name) VALUES (?, ?)", firstName, lastName)
  if err != nil {
   log.Fatalln(err)
  }
  id, err := rs.LastInsertId()
  if err != nil {
   log.Fatalln(err)
  }
  fmt.Println("insert person Id {}", id)
  msg := fmt.Sprintf("insert successful %d", id)
  c.JSON(http.StatusOK, gin.H{
   "msg": msg,
  })
 })
 ...
}
 

执行非query操作,使用db的Exec方法,在mysql中使用?做占位符。最后我们把插入后的id返回给客户端。请求得到的结果如下:

☁  ~  curl -X POST http://127.0.0.1:8000/person -d "first_name=hello&last_name=world" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    62  100    30  100    32   5054   5391 --:--:-- --:--:-- --:--:--  6400
{
    "msg": "insert successful 1"
}

下面可以随意增加几条记录。

查询列表 Query

上面我们增加了一条记录,下面就获取这个记录,查一般有两个操作,一个是查询列表,其次就是查询具体的某一条记录。两种大同小异。

为了给查询结果绑定到golang的变量或对象,我们需要先定义一个结构来绑定对象。在main函数的上方定义Person结构:

type Person struct {
 Id        int    `json:"id" form:"id"`
 FirstName string `json:"first_name" form:"first_name"`
 LastName  string `json:"last_name" form:"last_name"`
}

然后查询我们的数据列表

 router.GET("/persons", func(c *gin.Context) {
  rows, err := db.Query("SELECT id, first_name, last_name FROM person")
  if err != nil {
   log.Fatalln(err)
  }
 defer rows.Close()
   persons := make([]Person, 0)
  for rows.Next() {
   var person Person
   rows.Scan(&person.Id, &person.FirstName, &person.LastName)
   persons = append(persons, person)
  }
  if err = rows.Err(); err != nil {
   log.Fatalln(err)
  }
  c.JSON(http.StatusOK, gin.H{
   "persons": persons,
  })
 })

读取mysql的数据需要有一个绑定的过程,db.Query方法返回一个rows对象,这个数据库连接随即也转移到这个对象,因此我们需要定义row.Close操作。然后创建一个[]Person的切片。

使用make,而不是直接使用var persons []Person的声明方式。还是有所差别的,使用make的方式,当数组切片没有元素的时候,Json会返回[]。如果直接声明,json会返回null

接下来就是使用rows对象的Next方法,遍历所查询的数据,一个个绑定到person对象上,最后append到persons切片。

☁  ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   113  100   113    0     0   101k      0 --:--:-- --:--:-- --:--:--  110k
{
    "persons": [
        {
            "first_name": "hello",
            "id": 1,
            "last_name": "world"
        },
        {
            "first_name": "vanyar",
            "id": 2,
            "last_name": "elves"
        }
    ]
}

查询单条记录 QueryRow

查询列表需要使用迭代rows对象,查询单个记录,就没这么麻烦了。虽然也可以迭代一条记录的结果集。因为查询单个记录的操作实在太常用了,因此golang的database/sql也专门提供了查询方法

 router.GET("/person/:id", func(c *gin.Context) {
  id := c.Param("id")
  var person Person
  err := db.QueryRow("SELECT id, first_name, last_name FROM person WHERE id=?", id).Scan(
   &person.Id, &person.FirstName, &person.LastName,
  )
  if err != nil {
   log.Println(err)
   c.JSON(http.StatusOK, gin.H{
    "person": nil,
   })
   return
  }
   c.JSON(http.StatusOK, gin.H{
   "person": person,
  })

 })

查询结果为:

☁  ~  curl  http://127.0.0.1:8000/person/1 | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    60  100    60    0     0  20826      0 --:--:-- --:--:-- --:--:-- 30000
{
    "person": {
        "first_name": "hello",
        "id": 1,
        "first_name": "world"
    }
}

查询单个记录有一个小问题,当数据不存在的时候,同样也会抛出一个错误。粗暴的使用log退出有点不妥。返回一个nil的时候,万一真的是因为错误,比如sql错误。这种情况如何解决。还需要具体场景设计程序。

增删改查,下面进行更新的操作。前面增加记录我们使用了urlencode的方式提交,更新的api我们自动匹配绑定content-type

 router.PUT("/person/:id", func(c *gin.Context) {
  cid := c.Param("id")
  id, err := strconv.Atoi(cid)
  person := Person{Id: id}
  err = c.Bind(&person)
  if err != nil {
   log.Fatalln(err)
  }
  stmt, err := db.Prepare("UPDATE person SET first_name=?, last_name=? WHERE id=?")
  defer stmt.Close()
  if err != nil {
   log.Fatalln(err)
  }
  rs, err := stmt.Exec(person.FirstName, person.LastName, person.Id)
  if err != nil {
   log.Fatalln(err)
  }
  ra, err := rs.RowsAffected()
  if err != nil {
   log.Fatalln(err)
  }
  msg := fmt.Sprintf("Update person %d successful %d", person.Id, ra)
  c.JSON(http.StatusOK, gin.H{
   "msg": msg,
  })
 })
 

使用 urlencode的方式更新:

☁  ~  curl -X PUT http://127.0.0.1:8000/person/2 -d "first_name=noldor&last_name=elves" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    72  100    39  100    33   3921   3317 --:--:-- --:--:-- --:--:--  4333
{
    "msg": "Update person 2 successful 1"
}

使用json的方式更新:

☁  ~  curl -X PUT http://127.0.0.1:8000/person/2 -H "Content-Type: application/json"  -d '{"first_name": "vanyar", "last_name": "elves"}' | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    85  100    39  100    46   4306   5079 --:--:-- --:--:-- --:--:--  5750
{
    "msg": "Update person 2 successful 1"
}

最后一个操作就是删除了,删除所需要的功能特性,上面的例子都覆盖了。实现删除也就特别简单了:

 router.DELETE("/person/:id", func(c *gin.Context) {
  cid := c.Param("id")
  id, err := strconv.Atoi(cid)
  if err != nil {
   log.Fatalln(err)
  }
  rs, err := db.Exec("DELETE FROM person WHERE id=?", id)
  if err != nil {
   log.Fatalln(err)
  }
  ra, err := rs.RowsAffected()
  if err != nil {
   log.Fatalln(err)
  }
  msg := fmt.Sprintf("Delete person %d successful %d", id, ra)
  c.JSON(http.StatusOK, gin.H{
   "msg": msg,
  })
 })

我们可以使用删除接口,把数据都删除了,再来验证上面post接口获取列表的时候,当记录没有的时候,切片被json序列化[]还是null

☁  ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    15  100    15    0     0  11363      0 --:--:-- --:--:-- --:--:-- 15000
{
    "persons": []
}

persons := make([]Person, 0)改成persons []Person。编译运行:

☁  ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    17  100    17    0     0  13086      0 --:--:-- --:--:-- --:--:-- 17000
{
    "persons": null
}

至此,基本的CURD操作的restful风格的API已经完成。内容其实不复杂,甚至相当简单。完整的代码可以通过GIST获取。

组织代码

实现了一个基本点restful服务,可惜我们的代码都在一个文件中。对于一个库,单文件或许很好,对于稍微大一点的项目,单文件总是有点非主流。当然,更多原因是为了程序的可读和维护,我们也需要重新组织代码,拆分模块和包。

封装模型方法

我们的handler出来函数中,对请求的出来和数据库的交互,都糅合在一起。首先我们基于创建的Person结构创建数据模型,以及模型的方法。把数据库交互拆分出来。

创建一个单例的数据库连接池对象:

var db *sql.DB
func main() {
 var err error
 db, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
 if err != nil {
  log.Fatalln(err)
 }
 defer db.Close()
 if err := db.Ping(); err != nil {
  log.Fatalln(err)
 }
 ...
}

这样在main包中,db就能随意使用了。

接下来,再把增加记录的的函数封装成Person结构的方法:

func (p *Person) AddPerson() (id int64, err error) {
 rs, err := db.Exec("INSERTs INTO person(first_name, last_name) VALUES (?, ?)", p.FirstName, p.LastName)
 if err != nil {
  return
 }
 id, err = rs.LastInsertId()
 return
}

然后handler函数也跟着修改,先创建一个Person结构的实例,然后调用其方法即可:

 router.POST("/person", func(c *gin.Context) {
  firstName := c.Request.FormValue("first_name")
  lastName := c.Request.FormValue("last_name")
  person := Person{FirstName: firstName, LastName: lastName}
  ra_rows, err := person.AddPerson()
  if err != nil {
   log.Fatalln(err)
  }
  msg := fmt.Sprintf("insert successful %d", ra_rows)
  c.JSON(http.StatusOK, gin.H{
   "msg": msg,
  })
 })

对于获取列表的模型方法和handler函数也很好改:

func (p *Person) GetPersons() (persons []Person, err error) {
 persons = make([]Person, 0)
 rows, err := db.Query("SELECT id, first_name, last_name FROM person")
 defer rows.Close()
 if err != nil {
  return
 }
 for rows.Next() {
  var person Person
  rows.Scan(&person.Id, &person.FirstName, &person.LastName)
  persons = append(persons, person)
 }
 if err = rows.Err(); err != nil {
  return
 }
 return
}

 router.POST("/person", func(c *gin.Context) {
  firstName := c.Request.FormValue("first_name")
  lastName := c.Request.FormValue("last_name")
  person := Person{FirstName: firstName, LastName: lastName}
  ra_rows, err := person.AddPerson()
  if err != nil {
   log.Fatalln(err)
  }
  msg := fmt.Sprintf("insert successful %d", ra_rows)
  c.JSON(http.StatusOK, gin.H{
   "msg": msg,
  })
 })

剩下的函数和方法就不再一一举例了。

增加记录的接口中,我们使用了客户端参数和Person创建实例,然后再调用其方法。而获取列表的接口中,我们直接声明了Person对象。两种方式都可以。

Handler函数

gin提供了router.Get(url, handler func)的格式。首先我们可以把所有的handler函数从router中提取出来。

例如把增加记录和获取列表的handle提取出来

func AddPersonApi(c *gin.Context) {
 firstName := c.Request.FormValue("first_name")
 lastName := c.Request.FormValue("last_name")
 person := Person{FirstName: firstName, LastName: lastName}
 ra_rows, err := person.AddPerson()
 if err != nil {
  log.Fatalln(err)
 }
 msg := fmt.Sprintf("insert successful %d", ra_rows)
 c.JSON(http.StatusOK, gin.H{
  "msg": msg,
 })
}
func main(){
 ...
 router.POST("/person", AddPersonApi)
 ...
}

把modle和handler抽出来之后,我们的代码结构变得更加清晰,具体可以参考这个GIST

组织项目

经过上面的model和handler的分离,代码结构变得更加清晰,可是我们还是单文件。下一步将进行封装不同的包。

数据库处理

在项目根目录创建下面三个文件夹,apisdatabasesmodels,并在文件夹内创建文件。此时我们的目录结果如下:

apis文件夹存放我们的handler函数,models文件夹用来存放我们的数据模型。

myql.go的包代码如下:

package database
import (
 "database/sql"
 _ "github.com/go-sql-driver/mysql"
 "log"
)
var SqlDB *sql.DB
func init() {
 var err error
 SqlDB, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
 if err != nil {
  log.Fatal(err.Error())
 }
 err = SqlDB.Ping()
 if err != nil {
  log.Fatal(err.Error())
 }
}

因为我们需要在别的地方使用SqlDB这个变量,因此依照golang的习惯,变量名必须大写开头。

数据model封装

修改models文件夹下的person.go,把对应的Person结构及其方法移到这里:

package models
import (
 "log"
 db "newgin/database"
)
type Person struct {
 Id        int    `json:"id" form:"id"`
 FirstName string `json:"first_name" form:"first_name"`
 LastName  string `json:"last_name" form:"last_name"`
}
func (p *Person) AddPerson() (id int64, err error) {
 rs, err := db.SqlDB.Exec("INSERT INTO person(first_name, last_name) VALUES (?, ?)", p.FirstName, p.LastName)
 if err != nil {
  return
 }
 id, err = rs.LastInsertId()
 return
}
func (p *Person) GetPersons() (persons []Person, err error) {
 persons = make([]Person, 0)
 rows, err := db.SqlDB.Query("SELECT id, first_name, last_name FROM person")
 defer rows.Close()
 if err != nil {
  return
 }
 for rows.Next() {
  var person Person
  rows.Scan(&person.Id, &person.FirstName, &person.LastName)
  persons = append(persons, person)
 }
 if err = rows.Err(); err != nil {
  return
 }
 return
}
....
 

handler

然后把具体的handler函数封装到api包中,因为handler函数要操作数据库,所以会引用model包

package apis
import (
 "net/http"
 "log"
 "fmt"
 "strconv"
 "gopkg.in/gin-gonic/gin.v1"
 . "newgin/models"
)
func IndexApi(c *gin.Context) {
 c.String(http.StatusOK, "It works")
}
func AddPersonApi(c *gin.Context) {
 firstName := c.Request.FormValue("first_name")
 lastName := c.Request.FormValue("last_name")
 p := Person{FirstName: firstName, LastName: lastName}
 ra, err := p.AddPerson()
 if err != nil {
  log.Fatalln(err)
 }
 msg := fmt.Sprintf("insert successful %d", ra)
 c.JSON(http.StatusOK, gin.H{
  "msg": msg,
 })
}
...

路由

最后就是把路由抽离出来,修改router.go,我们在路由文件中封装路由函数

package main
import (
 "gopkg.in/gin-gonic/gin.v1"
 . "newgin/apis"
)
func initRouter() *gin.Engine {
 router := gin.Default()
 router.GET("/", IndexApi)
 router.POST("/person", AddPersonApi)
 router.GET("/persons", GetPersonsApi)
 router.GET("/person/:id", GetPersonApi)
 router.PUT("/person/:id", ModPersonApi)
 router.DELETE("/person/:id", DelPersonApi)
 return router
}
 

分组路由

	v1 := router.Group("/v1").Use(middleware.AuthRequired())
	{
		v1.GET("/", IndexApi)

		v1.GET("/person", AddPersonApi)

		v1.GET("/persons", GetPersonsApi)

		v1.POST("/person/:id", GetPersonApi)
		//
		v1.PUT("/person/:id", EditPersonApi)
		//
		v1.DELETE("/person/:id", DelPersonApi)
	}

app入口

最后就是main函数的app入口,将路由导入,同时我们要在main函数结束的时候,关闭全局的数据库连接池:

main.go

package main
import (
 db "newgin/database"
)
func main() {
 defer db.SqlDB.Close()
 router := initRouter()
 router.Run(":8000")
}
 

至此,我们就把简单程序进行了更好的组织。当然,golang的程序组织依包为基础,不拘泥,根据具体的应用场景可以组织。

此时运行项目,不能像之前简单的使用go run main.go,因为包main包含main.go和router.go的文件,因此需要运行go run *.go命令编译运行。如果是最终编译二进制项目,则运行go build -o app

总结

通过上述的实践,我们了解了Gin框架创建基本的的restful服务。并且了解了如何组织golang的代码包。我们讨论了很多内容,但是唯独缺少测试。测试很重要,考察一个框架或者三方包的时候,是否有测试文件以及测试覆盖率是一个重要的参考。因为测试的内容很多,我们这里就不做单独的测试介绍。后面会结合gofight给gin的api增加测试代码。

此外,更多的内容,可以阅读别人优秀的开源项目,学习并实践,以提升自己的编码能力。

以上就是Gin与Mysql实现简单Restful风格API示例详解的详细内容,更多关于Gin与Mysql实现Restful风格API的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go语言基础单元测试与性能测试示例详解

    目录 概述 单元测试 代码说明如下 问题 注意 性能测试 基本使用 自定义测试时间 概述 测试不是Go语言独有的,其实在很多语言都有测试,例如:Go.Java.Python- 要想成为一名合格的大牛,这是程序员必须具备的一项技能,特别是一些大公司,这是加分的一项,主要有如下优点: 代码可以随时测试,保证代码不会产生错误 写出更加高效的代码 testing文档 Testing_flags文档 单元测试 格式:func TestXXX(t *testing.T) //add.go package c

  • 使用Go进行单元测试的实现

    简介 日常开发中, 测试是不能缺少的. Go 标准库中有一个叫做 testing 的测试框架, 可以用于单元测试和性能测试. 它是和命令 go test 集成使用的. 测试文件是以后缀 _test.go 命名的, 通常和被测试的文件放在同一个包中. 单元测试 单元测试的格式形如: func TestAbs(t *testing.T) { got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %d; want 1", got) } } 在 ut

  • Golang单元测试与覆盖率的实例讲解

    1 概述 C/C++和Java(以及大多数的主流编程语言)都有自己成熟的单元测试框架,前者如Check,后者如JUnit,但这些编程框架本质上仍是第三方产品,为了执行单元测试,我们不得不从头开始搭建测试工程,并且需要依赖于第三方工具才能生成单元测试的覆盖率. 相比之下,Go语言官方则提供了语言级的单元测试支持,即testing包,而且仅通过go工具本身就可以方便地生成覆盖率数据,也就是说,单元测试是Go语言的自带属性,除了好好设计自己的单元测试用例外,开发者不需要操心工程搭建的任何细节.没错,G

  • Go语言net包RPC远程调用三种方式http与json-rpc及tcp

    目录 一.服务端 二.http客户端 三.TCP客户端 四.json客户端 五.运行结果 rpc有多种调用方式,http.json-rpc.tcp 一.服务端 在代码中,启动了三个服务 package main import ( "log" "net" "net/http" "net/rpc" "net/rpc/jsonrpc" "sync" ) //go对RPC的支持,支持三个级别:T

  • 深入理解Golang的单元测试和性能测试

    前言 大家做开发的应该都知道,在开发程序中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定.本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试. go语言中自带有一个轻量级的测试框架testing和自带的go test命令来

  • Golang高性能持久化解决方案BoltDB数据库介绍

    目录 1. 介绍Bolt 2. 示例 3. 示例分析 4. 总结 1. 介绍Bolt BoltDB是纯Go语言实现的持久化解决方案,保存数据至内存映射文件.称之为持久化解决方案不是数据库,因为数据库这个词有很多额外功能是bolt所不具备的.正是因为缺乏这些功能使得bolt如此优雅.好用. Bolt就是一个Go包.无需在系统中安装,开始编码前也无需配置,什么都不需要,仅需要go get github.com/boltdb/bolt,然后import "github.com/boltdb/bolt&

  • Gin与Mysql实现简单Restful风格API实战示例详解

    目录 It works main.go 编译运行 数据库 CURD 增删改查 增 查 查询列表 Query 查询单条记录 QueryRow 改 删 组织代码 封装模型方法 Handler函数 组织项目 数据库处理 数据model封装 handler 路由 分组路由 app入口 总结 我们已经了解了Golang的Gin框架.对于Webservice服务,restful风格几乎一统天下.Gin也天然的支持restful.下面就使用gin写一个简单的服务,麻雀虽小,五脏俱全.我们先以一个单文件开始,然

  • Java实现超简单抖音去水印的示例详解

    目录 一.前言 二.原理与步骤 三.代码实现 四.总结 一.前言 抖音去水印方法很简单,以前一直没有去研究,以为搞个去水印还要用到算法去除,直到动手的时候才发现这么简单,不用编程基础都能做. 二.原理与步骤 其实抖音它是有一个隐藏无水印地址的,只要我们找到那个地址就可以了 1.我们在抖音找一个想要去水印的视频链接 注意:这里一定要是https开头的,不是口令 打开浏览器访问: 访问之后会重定向到这个地址,后面有一串数字,这个就是视频的id,他是根据这个唯一id来找到视频播放的 按F12查看网络请

  • mysql主从同步原理及应用场景示例详解

    目录 基础知识 MySQL 主从同步的主要应用场景有: 原理 设置主从同步,还有以下几个前提: 实验环境模拟实现主从同步 首先在 docker 中拉取 mysql 5.7 版本的镜像: 通过以下命令进入到 Master 容器内部: 接下来,进入数据库: 接下来执行以下命令: 基础知识 随着业务复杂度的增加,单台 MySQL 数据库服务器已不能满足实际的需求,取而代之的是数据库服务器集群.MySQL 具有支持分布式的特性,能轻松搭建一个支持高并发的 MySQL 数据库服务器集群.在集群中我们必须保

  • MySQL中使用去重distinct方法的示例详解

    一 distinct 含义:distinct用来查询不重复记录的条数,即distinct来返回不重复字段的条数(count(distinct id)),其原因是distinct只能返回他的目标字段,而无法返回其他字段 用法注意: 1.distinct[查询字段],必须放在要查询字段的开头,即放在第一个参数: 2.只能在SELECT 语句中使用,不能在 INSERT, DELETE, UPDATE 中使用: 3.DISTINCT 表示对后面的所有参数的拼接取 不重复的记录,即查出的参数拼接每行记录

  • vue最简单的前后端交互示例详解

    一.学习 vue 面临的问题 最近想学一门前端技术防身,看到博客园中写 vue 的多,那就 vue 吧.都说 vue 的官方教程写得好,所以我就从官方教程开始学习.官方教程说"Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用."但是这个概念是个什么鬼?还是让人一头雾水嘛.而且我一开始也没有搞清楚 vue 的定位,只知道它是一个前端库,但是确实不知道它的优势是什么,还以为它是一个学会就能一劳

  • Java8新特性时间日期库DateTime API及示例详解

    Java8新特性的功能已经更新了不少篇幅了,今天重点讲解时间日期库中DateTime相关处理.同样的,如果你现在依旧在项目中使用传统Date.Calendar和SimpleDateFormat等API来处理日期相关操作,这篇文章你一定不要错过.来刷新你的知识库吧! 背景 Java对日期.日历及时间的处理一直以来都饱受诟病,比如java.util.Date和java.util.Calendar类易用性差,不支持时区,非线程安全:还有用于格式化日期的类DateFormat也是非线程安全的等问题. J

  • ResizeObserver API使用示例详解

    目录 API介绍 浏览器兼容性 用法 总结 API介绍 当我们需要知道一个元素的大小变化或者屏幕横竖屏时,我们需要监听window.resize事件或者window.orientationchange方法.由于reize事件会在一秒内触发将近60次,所以很容易在改变窗口大小时导致性能问题.换句话说,window.resize事件通常是浪费的,因为它会监听每个元素的大小变化(只有window对象才有resize事件),而不是具体到某个元素的变化.如果我们只想监听某个元素的变化的话,这种操作就很浪费

  • MySQL中sleep函数的特殊现象示例详解

    前言 MySQL中的系统函数sleep,实际应用的场景不多,一般用来做实验测试,昨天在测试的时候,意外发现sleep函数的一个特殊现象.如果在查询语句中使用sleep函数,那么休眠的时间跟返回的记录有关. 如下测试所示: mysql> create table test(id int); Query OK, 0 rows affected (0.03 sec) mysql> select *, sleep(6) from test; Empty set (0.00 sec) mysql>

  • C++中简单的文本文件输入/输出示例详解

    为了便于理解,我们把cout 用于控制台输出时的一些情况和写入到文本文件的情况进行类比: cout 控制台输出 包含头文件 iostream 头文件 iostream 定义了一个 ostream 类用于处理输出 头文件 iostream 声明了一个名为 cout 的 ostream 对象 指明名称空间 std 可以结合使用cout和运算符<<来显示各种类型的数据 文本文件输出(写入到文本文件) 包含文件头 fstream 头文件 fstream 定义了一个ofstream 类用于处理输出 声明

  • 在Django下测试与调试REST API的方法详解

    对于大多数研发人员来说,都期望能找到一个良好的测试/调试方法,来提高工作效率和快速解决问题.所谓调试,偏重于对某个bug的查找.定位.修复:所谓测试,是检验某个功能是否达到预期效果.测试发现问题后进行调试,从而解决问题. 对于后台研发来说,往往没有客户端研发(Windows/Android等等)那样简单有效的DEBUG方法,比如Step by Step.虽然目前有很多IDE可以实现本地调试,但是因为后台研发的环境复杂,你很难在一台机器上模拟所有的环境,比如线上的数据库只能在内网访问等等,所以很多

随机推荐