Go-Gin-Example 第八部分 优化配置接口+图片上传功能

文章目录

    • 前情提要
    • 本节目标
  • 优化配置结构
    • 讲解
    • 落实
      • 修改配置文件
      • 优化配置读取及设置初始化顺序
        • 第一步
      • 验证
    • 抽离file
  • 实现上传图片接口
    • 图片名加密
    • 封装image的处理逻辑
    • 编写上传图片的业务逻辑
    • 增加图片上传的路由
  • 验证
  • 实现前端访问 http.FileServer
    • r.StaticFS
    • 修改文章接口
      • 新增、更新文章接口

前情提要

学习项目github地址

上一部分学习笔记

本节目标

  • 优化配置结构(因为配置项越来越多)
  • 抽离 原 loggingFile 便于公用(logging、upload 各保有一份并不合适)
  • 实现上传图片接口(需限制文件格式、大小)
  • 修改文章接口(需支持封面地址参数)
  • 增加 blog_article (文章)的数据库字段
  • 实现 http.FileServer

优化配置结构

讲解

在先前章节中,我们通过读取KEY的方式读取配置项(建立setting模块)
本次需求中,需要增加图片的配置项,总体就有些冗余了

我们采用以下解决方法:

  • 映射结构体:使用 MapTo 来设置配置参数
  • 配置统管:所有的配置项统管到 setting

落实

修改配置文件

修改 conf/app.ini

增加了 5 个配置项用于上传图片的功能,4文件日志方面的配置项

[app]
PageSize = 10
JwtSecret = 233

RuntimeRootPath = runtime/

ImagePrefixUrl = http://127.0.0.1:8000
ImageSavePath = upload/images/
# MB
ImageMaxSize = 5
ImageAllowExts = .jpg,.jpeg,.png

LogSavePath = logs/
LogSaveName = log
LogFileExt = log
TimeFormat = 20060102

[server]
#debug or release
RunMode = debug
HttpPort = 8000
ReadTimeout = 60
WriteTimeout = 60

[database]
Type = mysql
User = root
Password = rootroot
Host = 127.0.0.1:3306
Name = blog
TablePrefix = blog_

优化配置读取及设置初始化顺序

第一步

将散落在其他文件里的配置都删掉,统一在 setting 中处理以及修改 init 函数为 Setup 方法

  1. 打开 pkg/setting/setting.go 文件,修改如下:
package models

import (
	"fmt"
	"log"
	"time"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/kingsill/gin-example/pkg/setting"
)

// 定义一个全局的数据库连接变量
var db *gorm.DB

// Model 设定常用结构体,可以作为匿名结构体嵌入到别的表格对应的结构体
type Model struct {
	ID         int `gorm:"primary_key" json:"id"`
	CreatedOn  int `json:"created_on"`
	ModifiedOn int `json:"modified_on"`
	DeletedOn  int `json:"deleted_on"`
}

func Setup() {
   //配置文件加载
   Cfg, err := ini.Load("conf/app.ini")
   if err != nil {
      log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
   }

   //将app section 部分映射到AppSetting结构体上
   err = Cfg.Section("app").MapTo(AppSetting)
   if err != nil {
      log.Fatalf("Cfg.MapTo AppSetting err: %v", err)
   }
   //将图片最大大小设置从5字节Byte转换为5兆字节MB
   AppSetting.ImageMaxSize = AppSetting.ImageMaxSize * 1024 * 1024

   err = Cfg.Section("server").MapTo(ServerSetting)
   if err != nil {
      log.Fatalf("Cfg.MapTo ServerSetting err: %v", err)
   }

   //将读取时自动转换的类型转换为时间间隔了,只不过是最小单位纳秒
   ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second
   ServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second

   err = Cfg.Section("database").MapTo(DatabaseSetting)
   if err != nil {
      log.Fatalf("Cfg.MapTo DatabaseSetting err: %v", err)
   }
}

在这里,我们做了如下几件事:

  • 编写与配置项保持一致的结构体(App、Server、Database
  • 使用 MapTo 将配置项映射到结构体上
  • 对一些需特殊设置的配置项进行再赋值
  1. 修改models.go
    init函数改为Setup方法,将独立读取的DB配置项删除,改为统一读取setting
package models

import (
...
)

// 定义一个全局的数据库连接变量
var db *gorm.DB

// Model 设定常用结构体,可以作为匿名结构体嵌入到别的表格对应的结构体
type Model struct {
	ID         int `gorm:"primary_key" json:"id"`
	CreatedOn  int `json:"created_on"`
	ModifiedOn int `json:"modified_on"`
	DeletedOn  int `json:"deleted_on"`
}

func Setup() {
	var err error

	//使用gorm框架初始化数据库连接
	db, err = gorm.Open(setting.DatabaseSetting.Type, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
		setting.DatabaseSetting.User,
		setting.DatabaseSetting.Password,
		setting.DatabaseSetting.Host,
		setting.DatabaseSetting.Name))

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

	//自定义默认表的表名,使用匿名函数,在原默认表名的前面加上配置文件中定义的前缀
	gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
		return setting.DatabaseSetting.TablePrefix + defaultTableName
	}

	//gorm默认使用复数映射,当前设置后即进行严格匹配
	db.SingularTable(true)
	//log记录打开
	db.LogMode(true)

	//进行连接池设置
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(100)

	//替换Create和Update回调函数
	db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
	db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)

	//添加删除的回调CallBacks
	db.Callback().Delete().Replace("gorm:delete", deleteCallback)
}


// CloseDB 与数据库断开连接函数
func CloseDB() {
	defer db.Close()
}

// updateTimeStampForCreateCallback 在创建记录时设置 `CreatedOn`, `ModifiedOn`
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
	...
}

// updateTimeStampForUpdateCallback 在更新记录时设置 `ModifyOn`
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
...
}

// 设定delete操作的callback逻辑
func deleteCallback(scope *gorm.Scope) {
	...
}

// 判断是否为空来进行空格插入,防止sql注入,保证安全性
func addExtraSpaceIfExist(str string) string {
	...
}

  1. 修改log.go
    init函数改为Setup方法
func Setup() {
	//获取log文件目录
	filePath := getLogFileFullPath()

	//得到log文件句柄
	F = openLogFile(filePath)

	//创建一个新的日志记录器
	logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
  1. 修改pkg/logging/file.go

    独立的 LOG 配置项删除,改为统一读取 setting,修改这两个函数即可

// 返回log文件的前缀路径,算是一个具有仪式感的函数
func getLogFilePath() string {
	return fmt.Sprintf("%s", setting.AppSetting.LogSavePath)
}

// 获得log文件的整体路径,以当前日期作为.log文件的名字
func getLogFileFullPath() string {
	prefixPath := getLogFilePath()
	suffixPath := fmt.Sprintf("%s%s.%s", setting.AppSetting.LogSaveName, time.Now().Format(setting.AppSetting.TimeFormat), setting.AppSetting.LogFileExt)

	return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}

  1. 其他漏下的未改为统一读取setting的根据报错进行修改即可

验证

在这里为止,针对本需求的配置优化就完毕了,你需要执行 go run main.go 验证一下你的功能是否正常哦

抽离file

  1. pkg目录下新建file/file.go
package file

import (
	"io"
	"mime/multipart"
	"os"
	"path"
)

// GetSize multipart.file用于处理HTTP请求中文件上传到类型   os.file则主要是本地文件的操作
func GetSize(f multipart.File) (int, error) {
	content, err := io.ReadAll(f)
	return len(content), err
}

// GetExt 获取文件扩展名
func GetExt(filename string) string {
	return path.Ext(filename)
}

// CheckExist 检查文件是否存在
func CheckExist(src string) bool {
	//os.stat用于获取文件的相关信息
	_, err := os.Stat(src)

	return os.IsNotExist(err)
}

// CheckPermission 检查访问文件的权限
func CheckPermission(src string) bool {
	_, err := os.Stat(src)

	//检查是否有访问文件的权限
	return os.IsPermission(err)
}

// IsNotExistMkDir 检查是否存在目录,不存在则创建目录
func IsNotExistMkDir(src string) error {

	if notExist := CheckExist(src); notExist == true {
		if err := MkDir(src); err != nil {
			return err
		}
	}

	return nil
}

// MkDir 创建目录
func MkDir(src string) error {
	err := os.MkdirAll(src, os.ModePerm) //权限0777,权限拉满
	if err != nil {
		return err
	}

	return nil
}

// Open 算是简单包装os.openfile
func Open(name string, flag int, perm os.FileMode) (*os.File, error) {
	f, err := os.OpenFile(name, flag, perm)
	if err != nil {
		return nil, err
	}

	return f, nil
}

在这里我们用到了 mime/multipart 包,它主要实现了 MIMEmultipart解析,主要适用于 HTTP 和常见浏览器生成的 multipart 主体

  1. 修改原logging包的方法
  • 修改pkg/logging/file.go
package logging

import (
   "fmt"
   "github.com/kingsill/gin-example/pkg/file"
   "github.com/kingsill/gin-example/pkg/setting"
   "os"
   "time"
)

// 返回log文件的前缀路径,算是一个具有仪式感的函数
func getLogFilePath() string {
   return fmt.Sprintf("%s", setting.AppSetting.LogSavePath)
}

// 获得log文件的整体路径,以当前日期作为.log文件的名字 runtime/log20010212.log
func getLogFileFullPath() string {
   prefixPath := getLogFilePath()
   suffixPath := fmt.Sprintf("%s%s.%s",
      setting.AppSetting.LogSaveName,
      time.Now().Format(setting.AppSetting.TimeFormat),
      setting.AppSetting.LogFileExt,
   )

   return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}

// 打开日志文件,返回写入的句柄handle
func openLogFile() (*os.File, error) {

   //获取文件整体路径
   fileName := getLogFileFullPath()

   //创建目录
   mkDir()

   //如果.log文件不存在,这里会创建一个
   handle, err := file.Open(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   if err != nil {
      return nil, fmt.Errorf("fail to open:%s\n", fileName)
   }

   return handle, nil
}

// 创建log目录
func mkDir() {
   //获得当前目录 dir: /home/wang2/gin-example
   dir, _ := os.Getwd()

   //检查目录访问权限
   perm := file.CheckPermission(getLogFilePath())
   if perm == true {
      panic("Permission denied")
   }

   //如果目录不存在,创建目录
   err := file.IsNotExistMkDir(dir + "/" + getLogFilePath())
   if err != nil {
      panic(err)
   }
}

  • 修改pkg/logging/log.go
    由于原方法传参有变,这里也进行相关调整
...

// Setup 自定义logger的初始化
func Setup() {
	var err error

	//得到log文件句柄
	F, err = openLogFile()
	if err != nil {
		log.Fatalln(err)
	}

	//创建一个新的日志记录器
	logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
...

实现上传图片接口

首先需要在 blog_article 中增加字段 cover_image_url,格式为 varchar(255) DEFAULT '' COMMENT '封面图片地址'

alter table blog_article add cover_image_url varchar(255) DEFAULT '' COMMENT '封面图片地址';

图片名加密

我们通过 MD5 对图片进行加密,防止图片名暴露
util目录下新建md5.go,写入文件内容

package util

import (
	"crypto/md5"
	"encoding/hex"
)

// EncodeMD5 计算给定字符的MD5哈希值,返回其十六进制表示
func EncodeMD5(value string) string {
	//创建一个新的MD5计算器实例
	m := md5.New()

	//将value写入到MD5计算器中
	m.Write([]byte(value))

	//nil表示计算完哈希值后不添加后缀
	return hex.EncodeToString(m.Sum(nil))
}

封装image的处理逻辑

pkg 目录下新建upload/image.go文件,写入文件内容

这里基本是对底层代码的二次封装,为了更灵活的处理一些图片特有的逻辑,并且方便修改,不直接对外暴露下层

package upload

import (
...
)

func GetImageFullUrl(name string) string {
	return setting.AppSetting.ImagePrefixUrl + "/" + GetImagePath() + name
}

// GetImageName 计算MD5加密之后的图片名
func GetImageName(name string) string {
	//将图片的名字剥离扩展名
	ext := path.Ext(name)
	fileName := strings.TrimSuffix(name, ext)

	//对单纯的图片名进行MD5加密
	fileName = util.EncodeMD5(fileName)

	//将MD5加密后的图片名和后缀返回
	return fileName + ext
}

// GetImagePath 包装文件路径 upload/images/
func GetImagePath() string {
	return setting.AppSetting.ImageSavePath
}

// GetImageFullPath 拼凑完整路径 runtime/+upload/images/
func GetImageFullPath() string {
	return setting.AppSetting.RuntimeRootPath + GetImagePath()
}

// CheckImageExt 检查图片格式是否正确
func CheckImageExt(fileName string) bool {
	ext := file.GetExt(fileName)
	for _, allowExt := range setting.AppSetting.ImageAllowExts {
		//都大写进行对比
		if strings.ToUpper(allowExt) == strings.ToUpper(ext) {
			return true
		}
	}

	return false
}

// CheckImageSize 检查图片的大小是否小于规定的最大值 5M
func CheckImageSize(f multipart.File) bool {
	size, err := file.GetSize(f)

	if err != nil {
		log.Println(err)
		logging.Warn(err)
		return false
	}

	return size <= setting.AppSetting.ImageMaxSize
}

func CheckImage(src string) error {
	dir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("os.Getwd err: %v", err)
	}

	//检查图片目录
	err = file.IsNotExistMkDir(dir + "/" + src)
	if err != nil {
		return fmt.Errorf("file.IsNotExistMkDir err: %v", err)
	}

	//检查访问权限
	perm := file.CheckPermission(src)
	if perm == true {
		return fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
	}

	return nil
}

编写上传图片的业务逻辑

routers/api 目录下新建 upload.go 文件,写入内容

package api

import (
...
)

func UploadImage(c *gin.Context) {
	code := e.SUCCESS
	data := make(map[string]string)

	file, image, err := c.Request.FormFile("image")
	if err != nil {
		logging.Warn(err)
		code = e.ERROR
		c.JSON(http.StatusOK, gin.H{
			"code": code,
			"msg":  e.GetMsg(code),
			"data": data,
		})
	}

	if image == nil {
		code = e.INVALID_PARAMS
	} else {
		imageName := upload.GetImageName(image.Filename) //获取图片名
		fullPath := upload.GetImageFullPath()            //图片完整路径
		savePath := upload.GetImagePath()                //仓库内保存路径

		//图片路径+名字
		src := fullPath + imageName

		//检查图片格式和大小
		if !upload.CheckImageExt(imageName) || !upload.CheckImageSize(file) {
			code = e.ERROR_UPLOAD_CHECK_IMAGE_FORMAT
		} else {
			//检查图片目录、访问权限
			err := upload.CheckImage(fullPath)
			if err != nil {
				logging.Warn(err)
				code = e.ERROR_UPLOAD_CHECK_IMAGE_FAIL
			} else if err := c.SaveUploadedFile(image, src); err != nil { //图片保存到指定位置
				logging.Warn(err)
				code = e.ERROR_UPLOAD_SAVE_IMAGE_FAIL
			} else {
				//
				data["image_url"] = upload.GetImageFullUrl(imageName)
				data["image_save_url"] = savePath + imageName
			}
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": data,
	})
}

在这一大段的业务逻辑中,我们做了如下事情:

  • c.Request.FormFile:获取上传的图片(返回提供的表单键的第一个文件)
  • CheckImageExt、CheckImageSize 检查图片大小,检查图片后缀
  • CheckImage:检查上传图片所需(权限、文件夹)
  • SaveUploadedFile:保存图片
    总的来说,就是 入参 -> 检查 -》 保存 的应用流程

增加图片上传的路由

打开 routers/router.go 文件,增加路由 r.POST("/upload", api.UploadImage)

func InitRouter() *gin.Engine {
	r := gin.New()
    ...
	r.GET("/auth", api.GetAuth)
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	r.POST("/upload", api.UploadImage)

	apiv1 := r.Group("/api/v1")
	apiv1.Use(jwt.JWT())
	{
		...
	}

	return r
}

验证

使用 postman,测试图片上传功能
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
看到runtime/upload/images下存在我们上传的文件
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现前端访问 http.FileServer

在完成了上一小节后,我们还需要让前端能够访问到图片,一般是如下:

  • CDN
  • http.FileSystem

在公司的话,CDN 或自建分布式文件系统居多,也不需要过多关注。而在实践里的话肯定是本地搭建了,Go 本身对此就有很好的支持,而 Gin 更是再封装了一层,只需要在路由增加一行代码即可

r.StaticFS

打开 routers/router.go 文件,增加路由 r.StaticFS("/upload/images", http.Dir(upload.GetImageFullPath()))

func InitRouter() *gin.Engine {
    ...
    //网页 请求我们指定目录内的内容
    r.StaticFS("/upload/images", http.Dir(upload.GetImageFullPath()))
	
	r.GET("/auth", api.GetAuth)
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	r.POST("/upload", api.UploadImage)
    ...
}

http.dir 创建了文件系统,将 /upload/image 路径映射到我们指定的文件目录中,这里为 runtime/upload/images/ ,即我们放置图片的文件夹下

更多内容可以查看源码进行学习,到这里可以自行进行验证,访问 127.0.0.1:8000/upload/images/图片名

修改文章接口

新增、更新文章接口

支持入参 cover_image_url、增加对cover_image_url的非空、最长长度的检验

  1. 修改 models/article.go
...

// Article 建立对应article表的struct结构体,方便进行信息读写
type Article struct {
...

	CoverImageUrl string `json:"cover_image_url"`
}


// AddArticle 添加文章
func AddArticle(data map[string]interface{}) bool {
	db.Create(&Article{
...
		CoverImageUrl: data["cover_image_url"].(string),
	})

	return true
}
...

  1. 修改 routers/api/v1/article.go
    AddArticleEditArticle 方法在原来的基础上进行修改,首先将之前为了方便验证写的使用 查询参数 ,改为 表单参数, 更安全
// @Summary	新增文章
// @Produce	json
// @Param		tagId		body		int		true	"tagId"
// @Param		title		body        string	true	"title"
// @Param		desc		body		string	true	"desc"
// @Param		content		body		string	true	"content"
// @Param		createdBy	body		string	true	"createdBy"
// @Param		state		body		int		true	"state"
// @Success	200			{string}	json	"{"code":200,"data":{},"msg":"ok"}"
// @Router		/api/v1/tags [post]
func AddArticle(c *gin.Context) {
	tagId := com.StrTo(c.PostForm("tag_id")).MustInt()
	title := c.PostForm("title")
	desc := c.PostForm("desc")
	content := c.PostForm("content")
	createdBy := c.PostForm("created_by")
	coverImageUrl := c.PostForm("cover_image_url")//**********
	state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()

	valid := validation.Validation{}
	valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
	valid.Required(title, "title").Message("标题不能为空")
	valid.Required(desc, "desc").Message("简述不能为空")
	valid.Required(content, "content").Message("内容不能为空")
	valid.Required(createdBy, "created_by").Message("创建人不能为空")
	valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
	valid.Required(coverImageUrl, "cover_image_url").Message("封面地址不能为空")//***********

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		if models.ExistTagByID(tagId) {
			data := make(map[string]interface{})
			data["tag_id"] = tagId
			data["title"] = title
			data["desc"] = desc
			data["content"] = content
			data["created_by"] = createdBy
			data["state"] = state
			data["cover_image_url"] = coverImageUrl//****************

			models.AddArticle(data)
			code = e.SUCCESS
		} else {
			code = e.ERROR_NOT_EXIST_TAG
		}
	} else {
		for _, err := range valid.Errors {
			logging.Info("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]interface{}),
	})
}

// @Summary	修改文章
// @Produce	json
// @Param		id			path		int		true	"id"
// @Param		tagId		body		int		true	"tagId"
// @Param		title		body		string	true	"title"
// @Param		desc		body		string	true	"desc"
// @Param		content		body		string	true	"content"
// @Param		modifiedBy	body		string	true	"modifiedBy"
// @Param		state		body		int		false	"state"
// @Success	200			{string}	json	"{"code":200,"data":{},"msg":"ok"}"
// @Router		/api/v1/tags [post]
func EditArticle(c *gin.Context) {
	valid := validation.Validation{}

	id := com.StrTo(c.Param("id")).MustInt()
	tagId := com.StrTo(c.PostForm("tag_id")).MustInt()
	title := c.PostForm("title")
	desc := c.PostForm("desc")
	content := c.PostForm("content")
	coverImageUrl := c.PostForm("cover_image_url")//******
	modifiedBy := c.PostForm("modified_by")

	var state int = -1
	if arg := c.Query("state"); arg != "" {
		state = com.StrTo(arg).MustInt()
		valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
	}

	valid.Min(id, 1, "id").Message("ID必须大于0")
	valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
	valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
	valid.Required(title, "title").Message("标题不能为空")
	valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
	valid.Required(desc, "desc").Message("简述不能为空")
	valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
	valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
	valid.Required(coverImageUrl, "cover_image_url").Message("封面地址不能为空")//****************
	valid.MaxSize(coverImageUrl, 255, "cover_image_url").Message("封面地址最长为255字符")//*************
	valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		if models.ExistArticleByID(id) {
			if models.ExistTagByID(tagId) {
				data := make(map[string]interface{})
				data["tag_id"] = tagId

				data["title"] = title

				data["desc"] = desc

				data["content"] = content

				data["modified_by"] = modifiedBy

				models.EditArticle(id, data)
				code = e.SUCCESS
			} else {
				code = e.ERROR_NOT_EXIST_TAG
			}
		} else {
			code = e.ERROR_NOT_EXIST_ARTICLE
		}
	} else {
		for _, err := range valid.Errors {
			logging.Info("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}


接下来进行验证即可

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

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

相关文章

基于单片机智能数字温度采集报警器系统设计

**单片机设计介绍&#xff0c;基于单片机智能数字温度采集报警器系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机智能数字温度采集报警器系统设计的核心目标是通过单片机实现温度的实时采集、显示以及超温报警…

琴童投稿发表论文

《琴童》是由国家新闻出版总署批准&#xff0c;中文天地出版传媒集团股份有限公司主管、百花洲文艺出版社有限责任公司主办的一本音乐素质教育期刊。本刊的办刊宗旨为&#xff1a;为中小学生普及音乐知识、提高音乐教育水平、促进素质教育服务。2008年、2010年、2014年、2015年…

镭速如何解决UDP传输不通的问题

我们之前有谈到过企业如果遇到UDP传输不通的情况&#xff0c;常见的一些解决方式&#xff0c;同时也介绍了一站式企业文件传输方式-镭速相关优势&#xff0c;如果在实际应用中&#xff0c;若镭速UDP传输出现不通的情况&#xff0c;需要按照网络通信的一般性排查方法以及针对镭速…

ESP32学习---ESP-NOW

ESP32学习---ESP-NOW 基于Arduino IDE环境获取mac地址单播通讯一对多通讯多对一通讯多对多通讯模块1代码模块2模块3 广播通讯 基于ESP-IDF框架 乐鑫编程指南中关于ESP-NOW的介绍&#xff1a;https://docs.espressif.com/projects/esp-idf/zh_CN/v5.2.1/esp32/api-reference/net…

探秘开发公司内部,开发小程序只要几百块?

做一个微信小程序大概需要多少钱&#xff1f; 在考虑开发微信小程序之前&#xff0c;许多商家和企业都会关心开发费用这个问题&#xff0c;并且可能会对比多家公司的报价。那么&#xff0c;开发一个微信小程序大概需要多少费用呢&#xff1f;下面我们简单介绍一下小程序开发的…

思考:开启MMU瞬间可能出现的多种问题以及多种解决方案

快速链接: 【精选】ARMv8/ARMv9架构入门到精通-[目录] &#x1f448;&#x1f448;&#x1f448; (说明本文的介绍都是基于armv8-aarch64或armv9硬件架构) 在mmu未开启阶段&#xff0c;PC操作的都是物理地址执行程序&#xff0c;这样看起来一切正常&#xff0c;没啥问题。 例如…

【Leetcode】top 100 图论

基础知识补充 1.图分为有向图和无向图&#xff0c;有权图和无权图&#xff1b; 2.图的表示方法&#xff1a;邻接矩阵适合表示稠密图&#xff0c;邻接表适合表示稀疏图&#xff1b; 邻接矩阵&#xff1a; 邻接表&#xff1a; 基础操作补充 1.邻接矩阵&#xff1a; class GraphAd…

蓝桥杯第1593题——二进制问题

题目描述 小蓝最近在学习二进制。他想知道 1 到 N 中有多少个数满足其二进制表示中恰好有 K 个 1。你能帮助他吗&#xff1f; 输入描述 输入一行包含两个整数 N 和 K。 输出描述 输出一个整数表示答案。 输入输出样例 示例 输入 7 2输出 3评测用例规模与约定 对于 30% …

软件测试工作中需要的Linux知识,一篇文章就够了

01、Linux基础 1、Linux系统简单介绍 Linux是一套免费使用, 支持多用户、多任务、支持多线程和多个核心CPU的操作系统&#xff1b;很多中型, 大型甚至是巨型项目都在使用Linux。 Linux的发行版说简单点就是将Linux与应用软件做一个打包, 目前市面上比较知名的发行版有: Ubun…

Free RTOS day2

1.思维导图 2.使用PWMADC光敏电阻完成光控灯的实验 int adc_val0;//用于保存ADC采样得到的数值 float volt0;//用于保存电压值 int main(void) {MX_GPIO_Init();MX_DMA_Init();MX_TIM1_Init();MX_USART1_UART_Init();MX_ADC_Init();MX_TIM3_Init();HAL_TIM_PWM_Start(&hti…

代码随想录算法训练营第二十七天|131.分割回文串、93.复原IP地址

文档链接&#xff1a;https://programmercarl.com/ LeetCode131.分割回文串 题目链接&#xff1a;https://leetcode.cn/problems/palindrome-partitioning/ 思路&#xff1a;把回溯的树画出来就好很多。startIndex用来控制切割的位置 例如对于字符串abcdef&#xff1a; 组…

实现offsetof宏以及交换一个整数二进制奇偶位的宏

目录 1. offsetof宏2. 交换奇偶位 1. offsetof宏 我们想用宏来实现offsetof函数,首先要了解这个函数的用法。 1.1 offsetof函数的介绍及用法 &#xff08;1&#xff09;功能&#xff1a;用来计算结构体中一个成员在该结构体中的相对起始位置的偏移量&#xff0c;单位是字节。 …

Golang goroutine 同步原语:sync 包让你对并发控制得心应手

在 Go 语言中&#xff0c;不仅有 channel 这类比较易用且高级的同步机制&#xff0c;还有 sync.Mutex、sync.WaitGroup 等比较原始的同步机制。通过它们&#xff0c;我们可以更加灵活地控制数据的同步和多协程的并发。 资源竞争 在一个 goroutine 中&#xff0c;如果分配的内存…

Python多任务处理---多线程

引入 生活中&#xff0c;我们在电脑上打开了一个word, 这个word对操作系统来说就是一个进程。我们在进行word操作的时候&#xff0c;比如在你打字的时候&#xff0c;该word同时可以进行文字检查。发现了没&#xff0c;在同一个进程中&#xff0c;我们也可以进行同时操作。…

【Pytorch学习笔记(二)】张量的创建(补充)

一、知识回顾 我们在博客《张量的创建与访问》中已经讨论了一些张量的创建方法如torch.CharTensor()、torch.FloatTensor()以及torch.zeros()等张量创建方法&#xff0c;但由于其仅仅介绍了cpu版本torch下张量的创建方法和只有具体数据类型张量&#xff0c;本节内容旨在补充gp…

论文速览 | IEEE TCI, 2022 | 单光子级非视距成像:估计强度与优化重建

注1:本文系"计算成像最新论文速览"系列之一,致力于简洁清晰地介绍、解读非视距成像领域最新的顶会/顶刊论文(包括但不限于 Nature/Science及其子刊; CVPR, ICCV, ECCV, SIGGRAPH, TPAMI; Light‑Science & Applications, Optica 等)。 本次介绍的论文是:<2…

【Git】命令行使用体验大大优化的方法

Git的优化使用 相信很多人&#xff0c;在使用git作为版本管理工具时都会感受到它的方便&#xff0c;但是也会有一些问题困扰着我们&#xff0c;让我们觉得使用体验不是很好。我在使用git的过程中就发现了几个问题&#xff1a;写commit费时、怎么做多人开发的代码审查等等。今天…

代码随想录算法训练营第二十五天| 216.组合总和III,17.电话号码的字母组合

题目与题解 216.组合总和III 题目链接&#xff1a;216.组合总和III 代码随想录题解&#xff1a;216.组合总和III 视频讲解&#xff1a;和组合问题有啥区别&#xff1f;回溯算法如何剪枝&#xff1f;| LeetCode&#xff1a;216.组合总和III_哔哩哔哩_bilibili 解题思路&#xf…

Linux之用户账号、用户组和与账号有关的系统文件

目录 一、基本介绍 1.用户和用户组 2.UID和GID 二、 账户管理 1.查看用户的UID和GID 2.添加账户 3.删除账号 4.修改账号 5.账户口令 三、分组管理 1.新增用户组 2.删除用户组 3.修改用户组 4.用户组切换 四、与账号有关的系统文件 1./etc/passwd 2./etc/shado…

组蛋白脱乙酰酶介导的胃癌肿瘤微环境特征及协同免疫治疗(多组学文献学习)

目录 ①HDAC转录组多数据NMF一次聚类 ②ACRG队列中HDAC单独NMF聚类 ③HDS评分在胃癌中的临床特征和基因组特征 ④高 HDS 可能提示胃癌的“热”肿瘤状态 ⑤HDS是胃癌免疫治疗效果的有力预测指标 ⑥单细胞转录组测序揭示了高HDS和低HDS患者的TME ⑦内皮细胞和成纤维细胞可…