1.项目初始化

目录

1.相关联的数据库表

2.使用gorm操作数据库

使用gen生成model和对数据库的操作

3.使用viper进行配置管理

读取配置文件

进行热更新

4.使用Pflag来进行命令行参数解析

5.使用日志slog

日志轮转与切割功能

6.错误码和http返回格式标准化

提供错误码

提供错误类型

提供统一的http返回格式

7.使用gin构建web服务器

路由管理

实现优雅关闭 


项目地址: https://github.com/liwook/PublicReview

1.相关联的数据库表

创建的sql语句保存在dianping.sql文件中。其中已经添加了一些用户信息和商户的信息。

该章节对应的文件目录:

 

2.使用gorm操作数据库

数据库是使用MySql。我们使用第三方的开源库 gorm来操作数据库。gorm是目前 Go 语言中最流行的 ORM 库,同时也是一个功能齐全且对开发人员友好的 ORM 库。

创建internal/db目录,在这创建mysql.go文件。

var DBEngine *gorm.DBfunc NewMySQL() (*gorm.DB, error) {dsn := "root:123456@tcp(127.0.0.1:3306)/mydbname?charset=utf8mb4&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {return nil, err}sqlDB, err := db.DB()if err != nil {return nil, err}sqlDB.SetMaxOpenConns(100) //设置数据库连接池最大连接数sqlDB.SetMaxIdleConns(30)  //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于MaxIdleConns,超过的连接会被连接池关闭return db, nil
}

提示:要是出现错误gorm.io/plugin/dbresolver@v1.2.1/dbresolver.go:139:18: cannot use map[string]gorm.Stmt{} (value of type map[string]gorm.Stmt) as type map[string]*gorm.Stmt in struct literal。

解决方案是:执行 go get gorm.io/plugin/dbresolver@latest 把 gorm.io/plugin/dbresolver 升级到最新版本 。

之后需要写数据库表的表结构和表的一些操作。

使用gen生成model和对数据库的操作

以前需要手动写每个数据库表的model和自己写增删改查。而GEN 是一个基于 GORM 的安全 ORM 框架,其主要通过代码生成方式实现 GORM 代码封装。旨在安全上避免业务代码出现 SQL 注入,同时给研发带来最佳用户体验。

官方文档:Gen Guides | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

而现有库gen,写极少代码就可以生成表结构和表操作的代码,很方便。

创建cmd目录,在该目录下创建generate目录,在generate目录添加generate.go文件。添加如下代码。这样就可以生成表的结构体和对表操作的一些函数。

// 这里使用的是gorm.io/gen@v0.3.16
func main() {db, err := db.NewMySQL()if err != nil {panic(err)}g := gen.NewGenerator(gen.Config{// OutPath是相对执行`go run`时的路径OutPath: "./dal/query",    //代码生成的路径// ModelPkgPath:      "./dal/model",不写是最好,不然就出现目录:dal/dal/modelMode:              gen.WithDefaultQuery | gen.WithoutContext | gen.WithQueryInterface,FieldNullable:     false,FieldCoverable:    false,FieldSignable:     true,FieldWithIndexTag: false,FieldWithTypeTag:  true,})g.UseDB(db)dataMap := map[string]func(detailType string) (dataType string){"tinyint":  func(detailType string) (dataType string) { return "int8" },"smallint": func(detailType string) (dataType string) { return "int16" },"bigint":   func(detailType string) (dataType string) { return "int64" },"int":      func(detailType string) (dataType string) { return "int64" },}g.WithDataTypeMap(dataMap)autoUpdateTimeField := gen.FieldGORMTag("modified_on", "column:modified_on;type:int unsigned;autoUpdateTime")autoCreateTimeField := gen.FieldGORMTag("created_on", "column:created_on;type:int unsigned;autoCreateTime")// softDeleteField := gen.FieldType("deleted_on", "soft_delete.DeletedAt")// 模型自定义选项组fieldOpts := []gen.ModelOpt{autoCreateTimeField, autoUpdateTimeField}allModel := g.GenerateAllTable(fieldOpts...)g.ApplyBasic(allModel...)g.Execute()
}

3.使用viper进行配置管理

我们一般不会在代码中把一些可能会改变的参数写成常量,比如监听的端口。我们一般是配置在配置文件中,然后程序读取配置文件。

创建configs目录,该目录是用来存放配置文件,html等文件的。在该目录中添加文件config.yaml。

Server:RunMode: debugHttpPort: 10000ReadTimeout: 60WriteTimeout: 60
mysql:Username: root   # 填写你的数据库账号Password: 123456 # 填写你的数据库密码Host: 127.0.0.1:3306DBName: dianpingMaxIdleConns: 30MaxOpenConns: 100

然后在internal目录下创建config目录,在该目录中新建 config.go 文件,用于声明配置属性的结构体并编写读取段配置的配置方法。

var (ServerOption *ServerSettingMysqlOption  *MysqlSetting
)type ServerSetting struct {RunMode      stringHttpPort     stringReadTimeout  time.DurationWriteTimeout time.Duration
}type MysqlSetting struct {UserName     stringPassword     stringHost         stringDbName       stringMaxIdleConns intMaxOpenConns int
}

读取配置文件

编写读取配置文件的方法,按照每个结构体来读取,即是分段读取。

//config.go//打开配置文件进行读取
func ReadConfigFile() error {//viper是可以开箱即用的,这样写法就类似单例模式//也可以创建viper 比如 vp:=viper.New()viper.SetConfigFile("../configs/config.yaml") // 指定配置文件名和位置return viper.ReadInConfig()
}//分段读取
func ReadSection(key string, v any) error {return viper.UnmarshalKey(key, v)
}

进行读取。

func init() {InitConfig()
}func InitConfig() {if err := ReadConfigFile(); err != nil {panic(err)}err := ReadSection("server", &ServerOption)if err != nil {panic(err)}err = ReadSection("mysql", &MysqlOption)if err != nil {panic(err)}
}

在cmd目录中添加main.go文件,添加代码进行读取配置文件并打印。因为config.go文件使用了init函数,所以在main.go文件中无需手动进行初始化,会自动使用init函数。

package mainimport ("dianping/internal/config""fmt"
)func main() {fmt.Println("Hello, World!")fmt.Println(config.ServerOption)fmt.Println(config.MysqlOption)
}

接着修改下mysql.go文件中的NewMySQL函数,现在是使用了参数设置。

//mysql.go
func NewMySQL(config *config.MysqlSetting) (*gorm.DB, error) {dsn := fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=true&loc=Local`,config.UserName,config.Password,config.Host,config.DbName)..............sqlDB.SetMaxOpenConns(config.MaxOpenConns) //设置数据库连接池最大连接数sqlDB.SetMaxIdleConns(config.MaxIdleConns) //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于MaxIdleConns,超过的连接会被连接池关闭//添加这句query.SetDefault(db) //设置了才能使用query包,这样方便
}//generate.go的也要修改
var options config.MysqlSettingfunc init() {err := config.ReadConfigFile()..........err = config.ReadSection("mysql", &options)
}func main() {db, err := db.NewMySQL(&options)if err != nil {panic(err)}................
}

进行热更新

我们也会有些要求,想要在程序运行中,更改了配置文件后,程序不用重启也可以生效。viper也可以实现该效果。使用如下代码可以实现其效果,就是设置回调函数。注意:WatchConfig要使用在前面。

func ReadConfigFile() error {//viper是可以开箱即用的,这样写法就类似单例模式//也可以创建viper 比如 vp:=viper.New()viper.SetConfigFile("./configs/config.yaml")if err := viper.ReadInConfig(); err != nil {return err}viper.WatchConfig() //该函数内部是开启了一个新协程去监听配置文件是否更新//设置回调函数viper.OnConfigChange(func(in fsnotify.Event) {reloadAllSection()fmt.Println("更新了 :", in.Name)})return nil
}

在配置文件有更新后,就需要使用函数reloadAllSection再次读取配置并重新赋值给之前的变量。

把要解析的配置存储在key,而ServerOption等存储在value。之后有更新时候,使用变量sections的key来更新其value,那么该ServerOption也就更新了。

ReadSection函数也有改动,每次读取时候,往sections添加key,value。

var sections = make(map[string]any)func ReadSection(key string, v any) error {err := viper.UnmarshalKey(key, v)if err != nil {return nil}//增加读取section的存储记录,以便在重新加载配置的方法中进行处理if _, ok := sections[key]; !ok {sections[key] = v}return nil
}

reloadAllSection函数就是在配置文件更新时候会执行的函数,其内容也就是重新读取文件嘛。

// 用于重新读取配置
func reloadAllSection() error {for k, v := range sections {if err := ReadSection(k, v); err != nil {return nil}}return nil
}

 在main.go中进行测试,程序运行后修改配置文件,之后查看打印出来的是否有改动即可。

注意:这个热更新尽量少用。因为有些在程序启动后一些参数就不能改动的。比如http的监听端口,在程序启动后,你再更新配置文件也不会起作用的了。

这个热更新可以用于更新日志等级。这样不重启程序,新的日志等级也会在程序中生效。

func main() {go func() {for {fmt.Println(*global.ServerOption)fmt.Println(*global.DatabaseOption)fmt.Println()time.Sleep(5 * time.Second)}}()time.Sleep(20 * time.Second)
}

4.使用Pflag来进行命令行参数解析

Go服务开发中,经常需要给开发的组件加上各种启动参数来配置服务进程,影响服务的行为。一些大型服务就有多达上百个启动参数,而且这些参数的类型各不相同(例如:string、int、ip类型等),使用方式也不相同(例如:需要支持--长选项,-短选项等),所以我们需要一个强大的命令行参数解析工具。

比如我们的配置文件位置改变了,那我们就读取不了配置,所以需要可以在命令行来获取配置文件位置。比如执行 ./main --config=./config.yaml。

Go源码中提供了一个标准库Flag包,用来对命令行参数进行解析,但在大型项目中应用更广泛的是另外一个包:Pflag

Pflag提供了很多强大的特性,非常适合用来构建大型项目。Pflag有很多功能,这里就只介绍和本项目相关的部分。

想要手动指定配置文件的位置。可以使用pflag.StringP函数。参数config是对应的长选项,c是短选项。"../configs/config.yaml"是默认值。

//main.go
func init() {configPath := pflag.StringP("config", "c", "../configs/config.yaml", "config file path")pflag.Parse()fmt.Println("path:", *configPath)config.InitConfig(*configPath)..................
}func main() {............................
}//这样,就需要修改config.go文件,不在该文件提供init函数。
//config.go//不再使用
// func init() {
// 	InitConfig()
// }func ReadConfigFile(path string) error {viper.SetConfigFile(path) // 指定配置文件名和位置............
}func InitConfig(path string) {if err := ReadConfigFile(path); err != nil {panic(err)}err := ReadSection("server", &ServerOption)if err != nil {panic(err)}err = ReadSection("mysql", &MysqlOption)if err != nil {panic(err)}err = ReadSection("log", &LogOption)if err != nil {panic(err)}
}

查看效果

5.使用日志slog

在程序出错的时候,我们会想记录一些出错的位置和原因。这时我们就需要使用日志来记录了。

Go语言中有自己的官方日志库slog,也支持结构化。

创建pkg目录,在该目录下创建logger目录,创建log.go文件。

// 初始化后,日志使用直接 slog.Info("dfsf")就行
func InitLogger(level string) error {file, err := os.OpenFile("dianping.log", os.O_CREATE|os.O_APPEND, 0666)if err != nil {return err}//使用json格式logger := slog.New(slog.NewJSONHandler(file, &slog.HandlerOptions{AddSource: true,Level:     LogLevel(level),}))slog.SetDefault(logger)return nil
}// 获得日志等级
func LogLevel(level string) slog.Level {switch strings.ToLower(level) {case "debug":return slog.LevelDebugcase "info":return slog.LevelInfocase "warn":return slog.LevelWarncase "error":return slog.LevelError}return slog.LevelInfo
}

日志轮转与切割功能

要是不断记录日志,日志文件会越来越大。单个文件过大会影响写入效率,那我们就会希望在日志文件达到一个大小上限后,自动开启一个新文件再记录日志。还有  最多保留可以保留多少个日志文件,最多保留多少天,要不要做压缩处理?而这些使用库lumberjack都可以方便做到

lumberjack 是一个专门设计用于日志轮转和切割的库,其作用可以类比于一个可插拔的组件。我们可以通过配置该组件,并将其 集成 到所选的日志库中,从而实现日志文件的轮转与切割功能。

有v2.0版本,需要go get gopkg.in/natefinch/lumberjack.v2。

初始化 lumberjack 组件的代码如下所示:

log := &lumberjack.Logger{Filename:   "/path/file.log", // 日志文件的位置MaxSize:    10, // 文件最大尺寸(以MB为单位)MaxBackups: 3, // 保留的最大旧文件数量MaxAge:     28, // 保留旧文件的最大天数Compress:   true, // 是否压缩/归档旧文件LocalTime:  true, // 使用本地时间创建时间戳
}

需要注意的是, lumberjackLogger 结构体实现了 io.Writer 接口。这意味着所有关于日志文件的轮转与切割的核心逻辑都封装在 Write 方法中。这一实现也方便 Logger 结构体被集成到任何支持 io.Writer 参数的日志库中。

在log.go中添加日志参数变量,并进行解析。之后在config.yaml添加log的参数。

//config
var LogOption    *logger.LogSetting//并进行解析.............//config.yaml
log :Filename  : ../dianping.loglevel : debugMaxSize : 10 #mbMaxBackups :  10 #保留的最大文件个数MaxAge    : 30 #保留的最大天数

修改InitLogger函数。

// 日志选项结构体
type LogSetting struct {Filename   stringLevel      slog.LevelMaxSize    intMaxBackups intMaxAge     int
}var LogLevel = new(slog.LevelVar)// 使用lumberjack库将日志轮转与切割
func InitLogger(config *LogSetting) error {log := lumberjack.Logger{Filename:   config.Filename,   //日志文件的位置MaxSize:    config.MaxSize,    //文件最大尺寸(以mb为单位)MaxBackups: config.MaxBackups, //保留的最大文件个数MaxAge:     config.MaxAge,     //保留旧文件的最大天数LocalTime:  true,              //使用本地时间创建时间戳}LogLevel.Set(GetLogLevel(config.Level)) //这样就可以在运行时更新日志等级//使用json格式logger := slog.New(slog.NewJSONHandler(&log, &slog.HandlerOptions{AddSource: true,Level:     LogLevel,}))slog.SetDefault(logger)return nil
}

要可以在热更新时候使日志等级生效,需要修改热更新函数。在设置回调函数时候,更新日志等级。

func ReadConfigFile() error {.................viper.WatchConfig() //该函数内部是开启了一个新协程去监听配置文件是否更新//设置回调函数viper.OnConfigChange(func(in fsnotify.Event) {reloadAllSection()//查看是否有更新了日志等级level := viper.GetString("log.level")// fmt.Println("new_level:", level)logger.LogLevel.Set(logger.GetLogLevel(level))})
}

在mian.go中进行测试。config.yaml中level是info,之后修改为debug,这样不用重启程序,debug等级的也可打印出来。

func init() {err := logger.InitLogger(config.LogOption)if err != nil {panic(err)}
}func main() {fmt.Println(config.LogOption)go func() {for {slog.Info("Error")slog.Error("Error")slog.Debug("debug")time.Sleep(5 * time.Second)}}()time.Sleep(30 * time.Second)
}

6.错误码和http返回格式标准化

首先,我们要对错误码有统一的处理,哪个错误码对应哪个错误。接着,服务端返回错误给客户端时候,我们也要统一错误信息的格式,这样客户端解析的时候就可以统一解析。

提供错误码

创建pkg/code文件夹,添加commoncode.go文件,添加如下代码:

使用的httpcode就使用200、400、401、403、404、500这6个HTTP错误码,不需要过多的HTTP错误码展示给用户。

var OnlyUseHTTPStatus = map[int]bool{200: true, 400: true, 401: true, 403: true, 404: true, 500: true}//http状态码 5开头表示服务器端错误。4开头表示客户端错误// 基础错误
// code must start with 1xxxxx
const (ErrSuccess int = iota + 100001ErrUnknownErrBindErrValidation   //validation failedErrTokenInvalid //token invalid
)// 数据库类错误
const (ErrDatabase int = iota + 100101
)// 认证授权类错误
const (ErrEncrypt int = iota + 100201ErrSignatureInvalidErrExpiredErrInvalidAuthHeaderErrMissingHeader //The `Authorization` header was empty.ErrPasswordIncorrectErrPermissionDenied //Permission denied.
)// 编解码类错误
const (// ErrEncodingFailed - 500: Encoding failed due to an error with the data.ErrEncodingFailed int = iota + 100301ErrDecodingFailedErrInvalidJSONErrEncodingJSONErrDecodingJSON// ErrInvalidYaml - 500: Data is not valid Yaml.ErrInvalidYamlErrEncodingYamlErrDecodingYaml
)

提供错误类型

添加code.go。错误码用来指代一个错误类型,该错误类型需要包含一些有用的信息,例如对应的HTTP Status Code、对外展示的Message,以及跟该错误匹配的帮助文档。所以,我们还需要实现一个Coder来承载这些信息。我们定义了ErrCode结构体:

type Errcode struct {code    intHTTP    intmessage string
}func (coder Errcode) Error() string {return coder.message
}func (coder Errcode) Code() int {return coder.code
}func (coder Errcode) String() string {return coder.message
}func (coder Errcode) HTTPStatus() int {if coder.HTTP == 0 {return 500}return coder.HTTP
}

之后再用前面的错误码和错误码结构体进行注册,方便后续的使用。

var codes = map[int]*Errcode{}
var codeMux = &sync.Mutex{}func register(code int, httpStaus int, message string) {if code == 0 {panic("code 0 is reserved")}if _, ok := OnlyUseHTTPStatus[httpStaus]; !ok {panic("httstatuscode and code  are not good")}codeMux.Lock()defer codeMux.Unlock()errcode := &Errcode{code:    code,HTTP:    httpStaus,message: message,}codes[code] = errcode
}func ParseCoder(code int) *Errcode {if coder, ok := codes[code]; ok {return coder}return &Errcode{code: 1, HTTP: http.StatusInternalServerError, message: "unknown error"}
}

 添加code_generated.go文件,进行注册Coder。

func init() {register(ErrSuccess, 200, "OK")register(ErrUnknown, 500, "Internal server error")register(ErrBind, 400, "Error occurred while binding the request body to the struct")............................
}

提供统一的http返回格式

添加response.go文件,在该文件添加response结构体:

// Response defines project response format
// 使用json标签中的omitempty选项来实现当字段为空值时不返回该字段
type Response struct {Code    int    `json:"code,omitempty"`Message string `json:"message,omitempty"`Data    any    `json:"data,omitempty"`
}// WriteResponse used to write an error and JSON data into response.
func WriteResponse(c *gin.Context, code int, data interface{}) {coder := ParseCoder(code)if coder.HTTPStatus() != http.StatusOK {c.JSON(coder.HTTPStatus(), Response{Code:    coder.Code(),Message: coder.String(),Data:    data,})return}c.JSON(http.StatusOK, Response{Data: data})
}

7.使用gin构建web服务器

路由管理

创建目录internal/routeer,在该目录添加router.go文件。该文件是对所有的路由进行管理的。

func NewRouter() *gin.Engine {r := gin.Default()r.GET("/ping", func(c *gin.Context) {code.WriteResponse(c, code.ErrSuccess, "pong")})r.GET("/test", func(c *gin.Context) {code.WriteResponse(c, code.ErrDatabase, "not find this data")})r.GET("/test2", func(c *gin.Context) {code.WriteResponse(c, code.ErrDatabase, nil)})return r
}

 在main.go文件中测试

func main() {r := router.NewRouter()err := r.Run(":" + config.ServerOption.HttpPort)if err != nil {panic(err)}
}

实现优雅关闭 

对于一个web服务器,客户端正在使用,而这时服务器却关闭了,用户可能会收到错误消息,并且不知道发生了什么。所以要是服务器可以等待已连接的所有数据处理完所有用户,并且这个时段也不再接收客户端的新请求。

  1. 使用信号监听。通过监听操作系统信号来触发服务器的停止。当接收到特定信号(如 os.Interrupt 或 syscall.SIGTERM)时,开始执行停止流程。
  2. 使用 http.Server 内置的 Shutdown() 方法优雅地关机。
func main() {r := router.NewRouter()// err := r.Run(":" + config.ServerOption.HttpPort)// if err != nil {// 	panic(err)// }//创建HTTP服务器server := http.Server{Addr:    ":" + config.ServerOption.HttpPort,Handler: r,}go func() {err := server.ListenAndServe()if err != nil {panic(err)}}()quit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // syscall.SIGKILL是无法捕捉的<-quitfmt.Println("shutdown server...")//创建超时上下文,Shutdown可以让未处理的连接在这个时间内关闭ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := server.Shutdown(ctx); err != nil {panic(err)}fmt.Println("server shutdown success")
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode—1115. 交替打印 FooBar【中等】(多线程)

2024每日刷题&#xff08;180&#xff09; Leetcode—1115. 交替打印 FooBar C实现代码 class FooBar { private:int n;sem_t fooSem;sem_t barSem;public:FooBar(int n) {this->n n;sem_init(&fooSem, 0, 1);sem_init(&barSem, 0, 0);}~FooBar() {sem_destroy(&…

免杀对抗—内存加载UUID标识IPV4地址MAC地址

前言 试想我们开辟一块内存,然后直接将shellcode写入到对应的内存中并且该内存是可读可写可执行的状态,那么这种方式太容易被AV所查杀,因此当我们如果是利用Windows自身提供的API来将加密或者封装好的shellcode写入到内存执行的话,将会大大增加查杀的难度。 参考文章&#xf…

Linux的pinctrl和gpio子系统

上一章我们编写了基于设备树的 LED 驱动&#xff0c;但是驱动的本质还是没变&#xff0c;都是配置 LED 灯所使用的 GPIO 寄存器&#xff0c;驱动开发方式中硬件初始化这一部分和裸机基本没啥区别。Linux 是一个庞大而完善的系统&#xff0c;尤其是驱动框架&#xff0c;像 GPIO …

Stm32+Esp8266连接阿里云程序移植教程(MQTT协议)

Stm32Esp8266连接阿里云程序移植教程&#xff08;MQTT协议&#xff09; 一、前期准备二、移植过程三、程序的使用3.1 连接上阿里云3.2 传输用户数据到阿里云3.3 解析从阿里云下发给用户的数据3.4 关于调试接口 一、前期准备 自己要的工程文件移植所需的文件&#xff08;如下图&…

新手必看!手把手教你打造10W+爆款文章

自定义 GPTs 的引入彻底改变了博主、营销人员和内容创作者在 ChatGPT 高级版本中的写作方式。这些自定义 GPTs 提供个性化的 AI 工具&#xff0c;旨在执行特定任务&#xff0c;使写作过程更顺畅、迅速且高效。从主题头脑风暴到撰写 SEO 友好的内容&#xff0c;自定义 GPTs 满足…

ES6扩展运算符

1.介绍&#xff1a; ... 扩展运算符能将数组转换为逗号分隔的参数序列&#xff1b; 扩展运算符&#xff08;spread&#xff09;也是三个点&#xff08;...&#xff09;。它好比 rest 参数的逆运算&#xff0c;将一个数组转为用逗号分隔的 参数序列&#xff0c;对数组进…

方波信号发生器(完整SCL源代码)

正弦和余弦信号发生器请参考下面文章链接: 1、博途PLC平台 PLC信号发生器(博途SCL)_博图软件波形发生器怎么用-CSDN博客文章浏览阅读1.1k次。本文介绍了如何使用博途SCL编程实现不同周期和幅值的信号发生器,包括余弦和正弦信号。通过信号发生器,可以用于验证PLC的滤波器效…

Android Framework AMS(06)startActivity分析-3(补充:onPause和onStop相关流程解读)

该系列文章总纲链接&#xff1a;专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明&#xff1a; 说明&#xff1a;本章节主要解读AMS通过startActivity启动Activity的整个流程的补充&#xff0c;更新了startActivity流程分析部分。 一般来说&#xff0c;有Activ…

ZBrush入门使用介绍——17、FiberMesh

大家好&#xff0c;我是阿赵。   继续介绍ZBrush的使用。这次来看看FiberMesh功能。这是一个可以模仿毛发的功能。 一、 使用FiberMesh的预览功能 先准备一个模型&#xff0c;并生成多边形网格 然后按着Ctrl&#xff0c;在模型的表面画一个遮罩。 找到FiberMesh功能&#…

JSP 的 response 和 session 内置对象

文章目录 前言一、response 内置对象 1.重定向网页2.处理 HTTP 文件头3.设置输出缓存二、session 内置对象 1.创建及获取客户会话2.会话中移动指定的绑定对象3.销毁 session 内置对象4.会话超时的管理5. session 对象的应用总结 前言 JSP 的 response 和 session 内置对像&…

HCIP——以太网交换安全(四)DHCP Snooping

目录 一、DHCP Snooping的知识点 二、DHCP Snooping实验拓扑 三、总结 一、DHCP Snooping的知识点 1.1、DHCP snooping 概述&#xff1a; ①DHCP Snooping使能DHCP的一种安全特性&#xff0c;用于保证DHCP客户端从合法的DHCP服务端获取IP地址。DHCP服务器记录DHCP客户端IP…

uniapp 省、市、区、乡镇 数据层级选择插件 Ba-DataPicker

Ba-DataPicker 是一款uniapp数据层级选择弹窗插件。支持省市区乡四级&#xff1b;支持自定义数据。 支持省、市、区、乡镇四级支持自定义数据支持字母检索 截图展示 支持定制、本地包、源码等&#xff0c;有建议和需要&#xff0c;请点击文章结尾“Uniapp插件开发”联系我&am…

Flink有状态计算

前言 状态是什么&#xff1f;状态就是数据&#xff0c;准确点说&#xff0c;状态是指 Flink 作业计算时依赖的历史数据或中间数据。如果一个 Flink 作业计算依赖状态&#xff0c;那它就是有状态计算的作业&#xff0c;反之就是无状态计算的作业。 举个例子&#xff0c;服务端…

AnaTraf | 提升网络稳定性与效率:深入解析网络流量采集分析与故障定位

目录 网络流量采集分析的核心价值 什么是网络流量采集分析&#xff1f; 网络流量分析的应用场景 利用流量分析优化企业网络 网络故障定位的关键步骤 故障定位的基本流程 常用故障定位方法 实用技巧 网络流量采集分析与故障定位的协同作用 整合流量分析提升故障响应速…

人脸识别-特征算法

文章目录 一、LBPH算法1.基本原理2.实现步骤3.代码实现 二、Eigenfaces算法1.特点2.代码实习 三、FisherFaces算法1.算法原理2.算法特点3.代码实现 四、总结 人脸识别特征识别器是数字信息发展中的一种生物特征识别技术&#xff0c;其核心在于通过特定的算法和技术手段&#xf…

开源 | Science子刊 | GCS轨迹优化方法

论文: https://arxiv.org/pdf/2205.04422 Github: https://github.com/RobotLocomotion/gcs-science-robotics 本文介绍了一种基于凸优化的高效运动规划方法&#xff0c;该方法能够在高维空间中可靠地规划出障碍物周围的轨迹。研究者们通过结合贝塞尔曲线和凸集图&#xff08…

大厂面试一上来就手撕 Transformer,心凉半截

在这两年&#xff0c;尤其是大模型问世之后&#xff0c;有关 Transformer 的面试题不仅数量众多&#xff0c;而且颇具新意。 今日&#xff0c;我将分享 18 道 Transformer 高频面试题&#xff08;如需获取更多专业面试题&#xff0c;扫描文末二维码即可&#xff09;&#xff0…

【超详细】TCP协议

TCP(Transmission Control Protocol 传输控制协议) 传输层协议有连接可靠传输面向字节流 为什么TCP是传输控制协议呢&#xff1f; 我们以前所看到的write接口&#xff0c;都是把用户级缓冲区的数据拷贝到发送缓冲区中&#xff0c;然后数据就由TCP自主决定了&#xff0c;所以…

Postman 如何测试入参是文件类型(File)参数接口

Postman 如何测试 File 类型参数 前提背景测试步骤1、打开 Postman 选择 POST 方法输入调用地址2、参数选择 Body 下的 form-data3、KEY 选择 File 选项&#xff0c;并填写 file 前提背景 springboot 项目&#xff0c;接口的参数是 File 类型&#xff0c;需要通过 Postman 测试…

js中map,filter,find,foreach的用法介绍

js中map&#xff0c;filter&#xff0c;find&#xff0c;foreach的用法介绍 在 JavaScript 中&#xff0c;数组提供了一些常用的迭代方法&#xff0c;如 map、filter、find 和 forEach&#xff0c;这些方法允许你对数组中的每个元素进行操作&#xff0c;下面是它们的用法和区别…