【Go语言】Gin框架的简单基本文档

思维导图

一、go 原生的http服务

在go中写一个web服务非常方便和快速:

package main

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

type Response struct {
	Code int    `json:"code"`
	Data any    `json:"data"`
	Msg  string `json:"msg"`
}

func GET(w http.ResponseWriter, r *http.Request) {
	// 获取参数
	fmt.Println(r.URL.String())

	byteData, _ := json.Marshal(Response{
		Code: 0,
		Data: map[string]any{},
		Msg:  "成功",
	})
	w.Write(byteData)
}

func POST(res http.ResponseWriter, req *http.Request) {
	// 获取参数
	byteData, _ := io.ReadAll(req.Body)
	fmt.Println(string(byteData))
	byteData, _ = json.Marshal(Response{
		Code: 0,
		Data: map[string]any{},
		Msg:  "成功",
	})
	res.Write(byteData)
}

func main() {
	// 创建出一个路由
	http.HandleFunc("/get", GET)
	http.HandleFunc("/post", POST)
	// 监听套接字
	fmt.Println("http server running: http://127.0.0.1:8080")
	http.ListenAndServe(":8080", nil)
}

但是在实际项目中使用原生go http 库会很不方便,主要体现在以下几点:

  • 参数解析与验证
  • 路由不太明了
  • 响应处理比较原始 

二、gin响应

       gin提供了非常多的响应方法,比如字符串,json,html等,下面,我们来一一查看,这些响应,我们都进行了重新封装。

2.1 json响应

       现在大部分的前后端交互都是以json为主,所以gin中最常用的就是json响应,他的用法非常简单,代码如下所示:

c.JSON(200, gin.H(
    "code": 0,
    "msg": "ok",
})

       但是,我们需要对其进行一定的封装,例如,标准响应格式中的 code,data,msg,前端可以判断code的值来确定操作是否成功,不过code的定义就是每家公司都有其自己的定义,我们定义 code = 0 为操作成功的状态码,非0值就是具体的错误码,这样可以方便定位错误,例如,code = 1001 是权限错误,code = 1002 是资源不存在。 

// 先定义出响应的结构体
type Response struct {
	Code int         `json:"code"`
	Data interface{} `json:"data"`
	Msg  string      `json:"msg"`
}

type Code int

const (
	RoleErrCode    Code = 1001
	NetworkErrCode      = 1002
)

var codeMap = map[Code]string{
	RoleErrCode:    "权限错误",
	NetworkErrCode: "网络错误",
}

func response(c *gin.Context, r Response) {
	c.JSON(200, r)
}

func Ok(c *gin.Context, data interface{}, msg string) {
	response(c, Response{
		Code: 0,
		Data: data,
		Msg:  msg,
	})
}

func OkWithData(c *gin.Context, data interface{}) {
	Ok(c, data, "成功")
}

func OkWithCode(c *gin.Context, msg string) {
	Ok(c, map[string]any{}, msg)
}

func Fail(c *gin.Context, code int, data interface{}, msg string) {
	response(c, Response{
		Code: code,
		Data: data,
		Msg:  msg,
	})
}

func FailWithMsg(c *gin.Context, msg string) {
	response(c, Response{
		Code: 7,
		Data: nil,
		Msg:  msg,
	})
}

func FailWithCode(c *gin.Context, code Code) {
	msg, ok := codeMap[code]
	if !ok {
		msg = "未知错误"
	}
	response(c, Response{
		Code: int(code),
		Data: nil,
		Msg:  msg,
	})
}

封装之后使用就比较简单了, 代码如下:

res.OkWithMsg(c, "登录成功")
res.OkWithData(c, map[string]any{
    "name": "加油旭杏",
})
res.FailWithMsg(c, "参数错误")

2.2 html 响应

       我们需要先使用 LoadHTMLGlob 加载一个目录下的所有html文件,也可以使用 LoadHTMLFiles 加载单个html文件。我们在load之后,我们在下面才可以使用这个文件名。代码如下:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    // 加载模版,只有这里加载了模版,下面才可以使用
    r.LoadHTMLGlob("template/*")
    // r.LoadHTMLFiles("template/index.html")
    r.GET("", func(c *gin.Context) {
        c.HTML(200, "index.html", nil)
    })
    r.Run(":8080")
}

       HTML的第三个参数是可以向HTML中传递数据的,也就是可以通过渲染,将后端的数据传递到前端,但是现在是前后端分离的时代,也很少使用后端返回模版了。下面是一个简单的例子:

// 后端代码
c.HTML(200, "index.html", map[string]any{
    "title": "这是网页标题",
})

// 在HTML文件中使用
<title>{{.title}}</title>

2.3 响应文件

用于浏览器直接请求这个接口唤起下载:

// 表示是文件流,唤起浏览器下载,一般设置为这个,就要设置文件名
c.Header("Contect-Type", "application/octet-stream")
// 用来执行下载下来的文件名
c.Header("Contect-Disposition", "attachment; filename=3.sldfjlkds.go") 
  • 需要设置Content-Type,唤起浏览器下载
  • 只能是get请求

2.4 静态文件 

静态文件的路径不能在被使用,响应静态文件的代码如下:

r.Static("st", "static") // 第一个参数是别名,第二个参数才是实际的路径
r.StaticFile("abcd", "stsatic/abc.txt")

三、gin请求

3.1 查询参数

       ?key=xxx&name=xxxx&name=yyyy 这种就被称为查询参数,但是这里要记住,查询参数不是GET请求专属的。

name := c.Query("name") // 查询单个参数
age := c.DefaultQuery("are", "25") // 查询单个参数,如果没有查到这个参数,有一个默认值
keyList := c.QueryArray("key")  // 查询一个字段的多个介绍
fmt.Println(name, are, keyList)

3.2 动态参数

动态参数也是查询url中的信息,就是查询模式不一样,下面是动态参数和静态参数的对比:

/user?id=123  // 查询参数的模式
/user/123  // 动态参数的模式

我们可以使用如下代码来进行动态参数的获取:

r.GET("users/:id", func(c *gin.Context) {
    userID := c.Param(id)
    fmt.Println(userID)
})

3.3 表单参数

一般就是专指的是form表单,就是你的http请求中的正文格式是form表单,代码如下所示:

name := c.PostForm("name")
age, ok := c.GetPostForm("age")
fmt.Println(name)
fmt.Println(age, ok)

3.4 文件上传

3.4.1 单个文件上传

       文件上传,我们需要将文件进行上传,就需要使用post请求,将文件数据放在http请求中的请求正文,然后将正文中的数据读取出来,再写入到新创建的文件中,代码如下:

r.POST("users", func(c *gin.Context) {
    fileHeader, err := c.FormFile("file")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(fileHeader.Filename) // 文件名
    fmt.Println(fileHeader.Size) // 文件大小,单位是字节,需要对文件大小进行限制
    
    file, _ := fileHeader.Open()
    byteData, _ := io.ReadAll(file)
    
    err = os.WriteFile("xxx.jpg", byteData, 0666)
    fmt.Println(err)
}

还有一种简单的方式,代码如下:

err = c.SaveUploadedFile(fileHeader, "upload/xxx/yyy/" + fileHeader.Filename)
fmt.Println(err)

3.4.2 多个文件上传

       我们在进行多个文件的上传时,我们需要使用循环来逐一获取文件的资源,然后将文件一一保存到新创建的文件中,代码如下:

r.POST("users", func(c *gin.Context) {
    form, err := c.MultipartForm()
    if err != nil {
        fmt.Println(err)
        return
    }  
    for _, headers := range form.File {
        for _, header := range headers {
            c.SaveUploadedFile(header, "uploads/" + header.Filename)
        }
    }
})

3.5 原始内容

       我们可以查看不同请求类型中的内容是什么,但是这个请求体中的body如果一旦被阅读,就会被销毁,但是我们有一个办法可以解决,代码如下:

byteData,_ := io.ReadAdd(c.Request.Body)
fmt.Println(string(byteData))
// 如果将请求中的正文读取之后,就直接进行销毁
c.Request.Body = io.NopCloser(bytes.NewReader(byteData))

四、参数绑定

       我们可以使用binding可以很好地完成参数的绑定,在C++语言中,我们也使用 std::bind 函数进行参数的绑定。

       ShouleBind这一类函数通常用于在处理请求时,将请求数据(比如表单或者JSON)绑定到相应的结构体中。他可以根据请求内容自动匹配字段,并验证数据的有效性。这在构建API时很重要,因为他能确保接收到的数据符合预期的格式,从而提升代码的安全性和可维护性。

4.1 绑定不同类型的参数

4.1.1 查询参数

type User struct{
    Name string `form:"name"`
    Age  int    `form:"Name"`
}

var user User

err := c.ShouldBindQuery(&user)
fmt.Println(user, err)

4.1.2 路径参数(uri)

r.GET("users/:id/:name", func(C *gin.Context) {
    type User struct {
        Name string `uri:"name"`
        ID   int    `uri:"id"`
    }    

    var user User

    err := c.ShouldBindUri(&user)
    fmt.Println(user, err)
}

4.1.3 表单参数

type User struct {
    Name string `form:"name"`
    Age  int    `form:"age"`
}

var user User

err := c.ShouldBind(&user)
fmt.Println(user, err)

注意:不能解析 x-www-form-urlencoded 的格式

4.1.4 json参数

type User struct {
  Name string `json:"name"`
  Age  int    `json:"age"`
}

var user User

err := c.ShouldBindJSON(&user)
fmt.Println(user, err)

4.1.5 header参数

type User struct {
  Name        string `header:"Name"`
  Age         int    `header:"Age"`
  UserAgent   string `header:"User-Agent"`
  ContentType string `header:"Content-Type"`
}

var user User

err := c.ShouldBindHeader(&user)
fmt.Println(user, err)

4.2 binding 内置规则

如果有多个规则,我们需要使用逗号进行分割,下面是每一个字段的意思解释:

// 不能为空,并且不能没有这个字段
required: 必填字段,比如:binding:"required"

// 针对字符串的长度
min 最小长度,比如:binding:"min=5"
max 最大长度,比如:binding:"max=10"
len 长度,比如:binding:"len=6"

// 针对数字的大小
eq 等于,比如:binding:"eq=3"
ne 不等于,比如:binding:"ne=12"
gt 大于,比如:binding:"gt=10"
gte 大于等于,比如:binding:"gte=19"
lt 小于,比如:binding:"lt=10"
lte 小于等于,比如:binding:"lte=10"

// 针对同级字段的值
eqfield 等于其他字段的值  比如:PassWord string `binding:"eqfield=Password"`
nefield 不等于其他字段的值
// 这个字段我们在进行密码的检验上是需要使用的,我们在RePasswor字段上进行使用

// 
-  忽略字段,比如:binding:"-" 或者不写

// 枚举类型,只能是red 或者 green
oneof=red green

// 字符串
contains=fengfeng  // 包含fengfeng的字符串
excludes  // 不包含
startswitch  // 字符串前缀
endswitch  // 字符串后缀

// 数组
dive  // dive 后面的验证就是针对数组中的每一个元素

// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标识符,可以唯一标识一个资源
// url 在于L(Locater)是统一资源定位符,提供找到该资源的确切路径
 
// 日期验证 
datatime=2006-01-02

4.3 自己编写binding规则

我们可以自己编写一个将错误信息显示中文的代码,代码如下:

package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "github.com/gin-gonic/gin/binding"
  "github.com/go-playground/locales/zh"
  "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  zh_translations "github.com/go-playground/validator/v10/translations/zh"
  "net/http"
  "strings"
)

var trans ut.Translator

// 这个init函数已经自动调用了
func init() {
  // 创建翻译器
  uni := ut.New(zh.New())
  trans, _ = uni.GetTranslator("zh")

  // 注册翻译器
  v, ok := binding.Validator.Engine().(*validator.Validate)
  if ok {
    _ = zh_translations.RegisterDefaultTranslations(v, trans)
  }
}

func ValidateErr(err error) string {
  errs, ok := err.(validator.ValidationErrors)
  if !ok {
    return err.Error()
  }
  var list []string
  for _, e := range errs {
    list = append(list, e.Translate(trans))
  }
  return strings.Join(list, ";")
}

type User struct {
  Name  string `json:"name" binding:"required"`
  Email string `json:"email" binding:"required,email"`
}

func main() {
  r := gin.Default()
  // 注册路由
  r.POST("/user", func(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
      // 参数验证失败
      c.String(200, ValidateErr(err))
      return
    }

    // 参数验证成功
    c.JSON(http.StatusOK, gin.H{
      "message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
    })
  })

  // 启动HTTP服务器
  r.Run()
}

需要注意的是:

       在Go语言中,init函数具有特殊的用途和规则,他会在包被导入时自动被调用,具体原因如下:

  • 初始化顺序:init函数确保包在被使用之前进行必要的初始化。Go会在运行程序时自动调用init函数,这样可以确保包中的全局变量、状态或者其他资源在主逻辑执行前已经准备好了
  • 无需显式调用:开发者不需要在main函数或者其他地方显式调用init,这减少了代码的复杂性,因为初始化逻辑是自动处理的
  • 包级别:每一个包可以有多个init函数,这些函数可以在不同的文件中定义。Go运行时会按文件顺序(或者编译顺序)调用他们,确保所有初始化都完成
  • 代码组织:使用init函数可以帮助组织初始化逻辑,使得代码更加清晰和模块化

       我们也可以将字段名显示为中文,但是我们需要在结构体中添加一些字段:label字段,代码如下: 

func init() {
  // 创建翻译器
  uni := ut.New(zh.New())
  trans, _ = uni.GetTranslator("zh")

  // 注册翻译器
  v, ok := binding.Validator.Engine().(*validator.Validate)
  if ok {
    _ = zh_translations.RegisterDefaultTranslations(v, trans)
  }

  v.RegisterTagNameFunc(func(field reflect.StructField) string {
    label := field.Tag.Get("label")
    if label == "" {
      return field.Name
    }
    return label
  })
}

我们还可以将错误信息和错误字段一起返回,代码如下:

package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "github.com/gin-gonic/gin/binding"
  "github.com/go-playground/locales/zh"
  "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  zh_translations "github.com/go-playground/validator/v10/translations/zh"
  "net/http"
  "reflect"
  "strings"
)

var trans ut.Translator

func init() {
  // 创建翻译器
  uni := ut.New(zh.New())
  trans, _ = uni.GetTranslator("zh")

  // 注册翻译器
  v, ok := binding.Validator.Engine().(*validator.Validate)
  if ok {
    _ = zh_translations.RegisterDefaultTranslations(v, trans)
  }

  v.RegisterTagNameFunc(func(field reflect.StructField) string {
    label := field.Tag.Get("label")
    if label == "" {
      label = field.Name
    }
    name := field.Tag.Get("json")
    return fmt.Sprintf("%s---%s", name, label)
  })
}

/*
{
  "name": "name参数必填",
}
*/

func ValidateErr(err error) any {
  errs, ok := err.(validator.ValidationErrors)
  if !ok {
    return err.Error()
  }
  var m = map[string]any{}
  for _, e := range errs {
    msg := e.Translate(trans)
    _list := strings.Split(msg, "---")
    m[_list[0]] = _list[1]
  }
  return m
}

type User struct {
  Name  string `json:"name" binding:"required" label:"用户名"`
  Email string `json:"email" binding:"required,email"`
}

func main() {
  r := gin.Default()
  // 注册路由
  r.POST("/user", func(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
      // 参数验证失败
      c.JSON(200, map[string]any{
        "code": 7,
        "msg":  "验证错误",
        "data": ValidateErr(err),
      })
      return
    }

    // 参数验证成功
    c.JSON(http.StatusOK, gin.H{
      "message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
    })
  })

  // 启动HTTP服务器
  r.Run()
}

4.4 自定义校验

我们要定义一个检验器:如果传入的IP字段中有值,一定是正确的;如果不传,就不传。

func init() {
  // 创建翻译器
  uni := ut.New(zh.New())
  trans, _ = uni.GetTranslator("zh")

  // 注册翻译器
  v, ok := binding.Validator.Engine().(*validator.Validate)
  if ok {
    _ = zh_translations.RegisterDefaultTranslations(v, trans)
  }

  v.RegisterTagNameFunc(func(field reflect.StructField) string {
    label := field.Tag.Get("label")
    if label == "" {
      label = field.Name
    }
    name := field.Tag.Get("json")
    return fmt.Sprintf("%s---%s", name, label)
  })
    
  // 可以允许开发者将用于自定义的验证函数注册到验证器中
  // 可以自定义和使用自己的验证逻辑,从而扩展男默认的验证功能
  v.RegisterValidation("fip", func(fl validator.FieldLevel) bool { // 这种类型是检验上下文信息
    // 下面将每一个字段打印出来
    fmt.Println("fl.Field(): ", fl.Field())
    fmt.Println("fl.FieldName(): ", fl.FieldName())
    fmt.Println("fl.StructFieldName(): ", fl.StructFieldName())
    fmt.Println("fl.Parent(): ", fl.Parent())
    fmt.Println("fl.Top(): ", fl.Top())
    fmt.Println("fl.Param(): ", fl.Param())

    ip, ok := fl.Field().Interface().(string)
    if ok && ip != "" {
      // 传了值就去校验是不是IP地址
      ipObj := net.ParseIP(ip)
      return ipObj != nil
    }
    return true
  })
}

       validator.FieldLevel 是Go 的 go-playground/validator 库中的一个类型,他代表了在验证过程中字段的上下文信息,具体来说,他提供了关于正在验证的字段的详细信息,例如字段的值,标签以及其他相关数据。

主要特性和用途

  1. 字段值:可以通过 Field() 方法获取正在验证的字段的值。这对于自定义验证逻辑非常重要。

  2. 标签:可以使用 Tag() 方法获取字段的验证标签,这有助于根据不同的标签定义不同的验证逻辑。

  3. 上下文信息FieldLevel 还可以提供关于验证的上下文信息,例如字段所在的结构体,这使得可以进行更复杂的验证。

五、gin中间件和路由

5.1 路由

r.GET()
r.POST()
r.PUT()
r.PATCH()
r.DELETE()

       我们需要将路由进行分组,然后将一类api划分到一个组中,我们可以使用 r.Group()这个函数将一组路由划分到同一个组中,我们可以写出以下代码:

func main() {
    // 创建出默认路由器
    r := gin.Dafault()

    // 进行分组
    r.Group("api")
    
    userGroup(r)
    
}

func userGroup(r *gin.RouterGroup) {
    r.GET()
    r.POST()
}

我们在分完组之后,可以使用一个统一的中间件加到这个组中。 

       在 Go 的 go-playground/validator 库中,Use 函数通常是指用于注册一个新的验证器,或者是将现有的验证器用于特定的结构体或类型。这是一个比较常见的设计模式,允许开发者为特定的类型提供定制化的验证逻辑。

主要功能

  1. 注册验证器:通过 Use 函数,开发者可以将一个新的验证规则或验证器注册到现有的验证器实例中。

  2. 组合验证器Use 允许将多个验证器组合在一起,以实现更复杂的验证需求。

  3. 灵活性:开发者可以根据具体需要灵活地定义和使用验证逻辑,增强代码的可维护性。

5.2 RESETFul Api 规范

尽量使用名称的复数来定义路由:

// 在没有resetful规范正确,表示创建用户,删除用户
/api/user_create
/api/users/create
/api/users/add
/api/add_user
/api/user/delete
/api/user_remove

// 使用resetful规范
GET /api/users  用户列表
POST /api/users  创建用户
PUT /api/users/:id 更新用户信息
DELETE /api/users 批量删除用户
DELETE /api/users/:id 删除单个用户

有一些公司里面的项目,基本上都是POST请求:

  1. 很早之前,那个时候还没有RESETFul 规范这个说法
  2. 很多公司的防火墙会拦截GET和POST之外的请求

5.3 中间件

5.3.1 局部中间件

       直接作用单个路由,我们可以使用Next函数将中间件跳转到下一个中间件,也可以使用Abort函数进行拦截,使用Abort函数拦截之后,就会原路返回。

// /3.中间件.go
package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
)

func Home(c *gin.Context) {
  fmt.Println("Home")
  c.String(200, "Home")
}
func M1(c *gin.Context) {
  fmt.Println("M1 请求部分")
  c.Next()
  fmt.Println("M1 响应部分")
}
func M2(c *gin.Context) {
  fmt.Println("M2 请求部分")
  c.Next()
  fmt.Println("M2 响应部分")
}

func main() {
  r := gin.Default()
  r.GET("", M1, M2, Home)
  r.Run(":8080")
}

5.3.2 全局中间件

全局也就是路由组,这也就是给路由分组的意义:

// /3.中间件.go
package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
)

func Home(c *gin.Context) {
  fmt.Println("Home")
  c.String(200, "Home")
}

func GM1(c *gin.Context) {
  fmt.Println("GM1 请求部分")
  c.Next()
  fmt.Println("GM1 响应部分")
}

func GM2(c *gin.Context) {
  fmt.Println("GM2 请求部分")
  c.Next()
  fmt.Println("GM2 响应部分")
}

func AuthMiddleware(c *gin.Context) {

}

func main() {
  r := gin.Default()
  g := r.Group("api")
  g.Use(GM1, GM2)
  g.GET("users", Home)
  r.Run(":8080")
}

       gin.Default() 中有两个中间件,一个是logger,一个recover,一个是日志系统,一个防止panic导致系统崩溃。 

5.3.3 中间件传递参数

c.Set("GM1", "GM1")
fmt.Println(c.Get("GM1"))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901903.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring中配置文件方式来配置实现数据源

我的后端学习大纲 我Spring学习大纲 1.1.数据源&#xff08;连接池&#xff09;的作用&#xff1a; 1.数据源&#xff08;连接池&#xff09;是提高程序性能而出现的2.数据源的使用步骤 &#xff1a; 创建数据源对象&#xff0c;在对象创建的时候会初始化部分连接资源使用连接…

【jvm】堆的内部结构

目录 1. 说明2. 年轻代&#xff08;Young Generation&#xff09;2.1 说明2.2 Eden区2.3 Survivor区 3. 老年代&#xff08;Old Generation&#xff09;3.1 说明3.2 对象存放3.3 垃圾回收 4. jdk7及之前5. jdk8及之后 1. 说明 1.JVM堆的内部结构主要包括年轻代&#xff08;You…

录屏软件推荐,4个工具助你高效录屏。

不同的录屏软件具有不同的特点和优势&#xff0c;如果只是偶尔需要录制&#xff0c;Win10 自带的录制功能就很方便&#xff1b;如果需要更加专业的录制和编辑功能&#xff0c;我可以推荐几款功能更加多样也效果较好的第三方软件。 1、福昕高清录屏 直达&#xff1a;www.foxits…

SVM(支持向量机)

SVM&#xff08;支持向量机&#xff09; 引言 支持向量机(Support Vector Machine,SVM)&#xff0c;可以用来解答二分类问题。支持向量(Support Vector)&#xff1a;把划分数据的决策边界叫做超平面&#xff0c;点到超平面的距离叫做间隔。在SVM中&#xff0c;距离超平面最近…

基于neo4j的新冠治疗和新冠患者轨迹的知识图谱问答系统

毕业设计还在苦恼选题&#xff1f;想做一个兼具前沿性和实用性的技术项目&#xff1f;了解下这款基于Neo4j的新冠治疗和患者轨迹的知识图谱问答系统吧&#xff01; 系统可以实现两大功能模块&#xff1a;新冠医疗信息和患者活动轨迹的展示与问答。通过图谱技术&#xff0c;你可…

VBA技术资料MF219:创建一个新的类型模块

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

【方波转正弦波谐波二阶】2022-6-10

缘由怎么用555时基电路将方波转换为正弦波&#xff1f;-其他-CSDN问答 可参带通滤波器电路图大全&#xff08;三款带通滤波器电路设计原理图详解&#xff09; - 全文 - 应用电子电路 - 电子发烧友网

《关于构图问题》

这是一本讲绘画技巧的书&#xff0c;但仔细琢磨体现出不易察觉的东方哲学思想。中国画讲究意境与留白&#xff0c;留白不代表“空”&#xff0c;而是代表对“实”的延伸&#xff0c;留下瞎想空间&#xff0c;实现对“有限&#xff08;实&#xff09;”的超越。 总论 文艺是人们…

演员王丹妮化身岛屿姐姐 开启少年们的欢乐挑战之旅

全民海岛真人秀《岛屿少年》正在持续热播中&#xff0c;少年们迎来了“茶嵛饭后”⻩⻥馆的开业日&#xff0c;知名演员王丹妮以岛屿姐姐的身份&#xff0c;悄然降临此地&#xff0c;为岛屿少年们带来了一场别开生面的考验。 在餐厅正式开业前夕&#xff0c;王丹妮巧妙地伪装成普…

【Spark+Hive大数据】基于spark抖音数据分析预测舆情系统(完整系统源码+数据库+开发笔记+详细部署教程)✅

目录 【SparkHive大数据】基于spark抖音数据分析预测舆情系统&#xff08;完整系统源码数据库开发笔记详细部署教程&#xff09;✅ 一、项目背景 二、研究目的 三、项目意义 四、项目功能 五、项目创新点​​​​​​​ 六、算法介绍 七、项目展示 八、启动文档 九、…

Android Kotlin中协程详解

博主前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住也分享一下给大家&#xff0c; &#x1f449;点击跳转到教程 前言 Kotlin协程介绍&#xff1a; Kotlin 协程是 Kotlin 语言中的一种用于处理异步编程的机制。它提供了一…

Chromium127调试指南 Windows篇 - 安装C++扩展与配置(五)

前言 在前面的文章中&#xff0c;我们已经安装了Visual Studio Code&#xff08;VS Code&#xff09;并配置了基本的扩展。现在&#xff0c;我们将进一步优化我们的开发环境&#xff0c;重点关注C相关的依赖扩展。这些扩展对于在VS Code中高效开发和调试Chromium项目至关重要。…

如何在 Linux 中对 USB 驱动器进行分区

如何在 Linux 中对 USB 驱动器进行分区 一、说明 为了在 Linux 上访问 USB 驱动器&#xff0c;它需要有一个或多个分区。由于 USB 驱动器通常相对较小&#xff0c;仅用于临时存储或轻松传输文件&#xff0c;因此绝大多数用户会选择只配置一个跨越整个 USB 磁盘的分区。但是&a…

基于Django+python的车牌识别系统设计与实现(带文档)

项目运行 需要先安装Python的相关依赖&#xff1a;pymysql&#xff0c;Django3.2.8&#xff0c;pillow 使用pip install 安装 第一步&#xff1a;创建数据库 第二步&#xff1a;执行SQL语句&#xff0c;.sql文件&#xff0c;运行该文件中的SQL语句 第三步&#xff1a;修改源…

Unity C#脚本的热更新

以下内容是根据Unity 2020.1.0f1版本进行编写的   目前游戏开发厂商主流还是使用lua框架来进行热更&#xff0c;如xlua&#xff0c;tolua等&#xff0c;也有的小游戏是直接整包更新&#xff0c;这种小游戏的包体很小&#xff0c;代码是用C#写的&#xff1b;还有的游戏就是通过…

【mysql进阶】4-5. InnoDB 内存结构

InnoDB 内存结构 1 InnoDB存储引擎中内存结构的主要组成部分有哪些&#xff1f; &#x1f50d; 分析过程 从官⽹给出的InnoDB架构图中可以找到答案 InnoDB存储引擎架构链接&#xff1a;https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html ✅ 解答问题 InnoD…

ECharts饼图-富文本标签,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…

虚拟光驱软件 PowerISO v8.7.0 中文激活版

PowerISO 是一款虚拟光驱工具及强大的光盘映像文件制作工具。支持创建、编辑、提取、压缩、加密和转换ISO/BIN图像文件。同时自带DISM工具&#xff0c;支持ESD/ISO/WIM/ESD格式转换&#xff0c;制作镜像文件制作U盘启动&#xff0c;支持ISO/BIN/IMG/DAA/WIM等各种常见文件类型。…

【Nas】X-Doc:jellyfin“该客户端与媒体不兼容,服务器未发送兼容的媒体格式”问题解决方案

【Nas】X-Doc&#xff1a;jellyfin“该客户端与媒体不兼容&#xff0c;服务器未发送兼容的媒体格式”问题解决方案 当使用Jellyfin播放视频时出现“该客户端与媒体不兼容&#xff0c;服务器未发送兼容的媒体格式”&#xff0c;这是与硬件解码和ffmpeg设置有关系&#xff0c;具体…

机器学习新领域:联邦学习方法——分布式机器学习

联邦学习是一种分布式机器学习方法&#xff0c;旨在保护数据隐私并提高模型的训练效率。以下是对联邦学习的详细介绍&#xff0c;包括其基本概念、应用场景以及面临的挑战。 一、介绍 1. 基本概念 联邦学习的核心思想是将模型训练过程分散到多个数据源上&#xff0c;而不需要…