19-错误处理(下):如何设计错误包?

 

 

业界有很多优秀的、开源的错误包可供选择,例如Go标准库自带的errors包、github.com/pkg/errors包。 

错误包需要具有哪些功能?

 在我看来,至少需要有下面这六个功能:

首先,应该能支持错误堆栈。 假设保存在bad.go文件中:

package mainimport ("fmt""log"
)func main() {if err := funcA(); err != nil {log.Fatalf("call func got failed: %v", err)return}log.Println("call func success")
}func funcA() error {if err := funcB(); err != nil {return err}return fmt.Errorf("func called error")
}func funcB() error {return fmt.Errorf("func called error")
}

执行上面的代码:

$ go run bad.go
2021/07/02 08:06:55 call func got failed: func called error
exit status 1

 为了解决这个问题,我们可以加一些Debug信息,来协助我们定位问题。这样做在测试环境是没问题的,但是在线上环境,一方面修改、发布都比较麻烦,另一方面问题可能比较难重现。这时候我们会想,要是能打印错误的堆栈就好了。例如:

2021/07/02 14:17:03 call func got failed: func called error
main.funcB/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/good.go:27
main.funcA/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/good.go:19
main.main/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/good.go:10
runtime.main/home/colin/go/go1.16.2/src/runtime/proc.go:225
runtime.goexit/home/colin/go/go1.16.2/src/runtime/asm_amd64.s:1371
exit status 1

 

其次,能够支持不同的打印格式。例如%+v%v%s等格式,可以根据需要打印不同丰富度的错误信息。

再次,能支持Wrap/Unwrap功能,也就是在已有的错误上,追加一些新的信息。例如errors.Wrap(err, "open file failed") 。Wrap通常用在调用函数中,调用函数可以基于被调函数报错时的错误Wrap一些自己的信息,丰富报错信息,方便后期的错误定位,例如:

func funcA() error {if err := funcB(); err != nil {return errors.Wrap(err, "call funcB failed")}return errors.New("func called error")
}func funcB() error {return errors.New("func called error")
}

 

还有,错误包应该有Is方法。在实际开发中,我们经常需要判断某个error是否是指定的error。在Go 1.13之前,也就是没有wrapping error的时候,我们要判断error是不是同一个,可以使用如下方法:

if err == os.ErrNotExist {// normal code
}

 这种情况下,我们的错误包就需要提供Is函数:

func Is(err, target error) bool

当err和target是同一个,或者err是一个wrapping error的时候,如果target也包含在这个嵌套error链中,返回true,否则返回fasle。

另外,错误包应该支持 As 函数。

在Go 1.13之前,没有wrapping error的时候,我们要把error转为另外一个error,一般都是使用type assertion或者type switch,也就是类型断言。例如:

if perr, ok := err.(*os.PathError); ok {fmt.Println(perr.Path)
}

但是现在,返回的err可能是嵌套的error,甚至好几层嵌套,这种方式就不能用了。所以,我们可以通过实现 As 函数来完成这种功能。现在我们把上面的例子,用 As 函数实现一下:

var perr *os.PathError
if errors.As(err, &perr) {fmt.Println(perr.Path)
}

这样就可以完全实现类型断言的功能,而且还更强大,因为它可以处理wrapping error。

最后,能够支持两种错误创建方式:非格式化创建和格式化创建。例如:

errors.New("file not found")
errors.Errorf("file %s not found", "iam-apiserver")

 所以,我基于github.com/pkg/errors包进行了二次封装,用来支持上一讲所介绍的错误码。

错误包实现

明确优秀的错误包应该具备的功能后,我们来看下错误包的实现。实现的源码存放在github.com/marmotedu/errors。

我通过在文件github.com/pkg/errors/errors.go中增加新的withCode结构体,该错误类型可以记录错误码、stack、cause和具体的错误信息。

type withCode struct {err   error // error 错误code  int // 业务错误码cause error // cause error*stack // 错误堆栈
}

下面,我们通过一个示例,来了解下github.com/marmotedu/errors所提供的功能。假设下述代码保存在errors.go文件中:

package mainimport ("fmt""github.com/marmotedu/errors"code "github.com/marmotedu/sample-code"
)func main() {if err := bindUser(); err != nil {// %s: Returns the user-safe error string mapped to the error code or the error message if none is specified.fmt.Println("====================> %s <====================")fmt.Printf("%s\n\n", err)// %v: Alias for %s.fmt.Println("====================> %v <====================")fmt.Printf("%v\n\n", err)// %-v: Output caller details, useful for troubleshooting.fmt.Println("====================> %-v <====================")fmt.Printf("%-v\n\n", err)// %+v: Output full error stack details, useful for debugging.fmt.Println("====================> %+v <====================")fmt.Printf("%+v\n\n", err)// %#-v: Output caller details, useful for troubleshooting with JSON formatted output.fmt.Println("====================> %#-v <====================")fmt.Printf("%#-v\n\n", err)// %#+v: Output full error stack details, useful for debugging with JSON formatted output.fmt.Println("====================> %#+v <====================")fmt.Printf("%#+v\n\n", err)// do some business process based on the error typeif errors.IsCode(err, code.ErrEncodingFailed) {fmt.Println("this is a ErrEncodingFailed error")}if errors.IsCode(err, code.ErrDatabase) {fmt.Println("this is a ErrDatabase error")}// we can also find the cause errorfmt.Println(errors.Cause(err))}
}func bindUser() error {if err := getUser(); err != nil {// Step3: Wrap the error with a new error message and a new error code if needed.return errors.WrapC(err, code.ErrEncodingFailed, "encoding user 'Lingfei Kong' failed.")}return nil
}func getUser() error {if err := queryDatabase(); err != nil {// Step2: Wrap the error with a new error message.return errors.Wrap(err, "get user failed.")}return nil
}func queryDatabase() error {// Step1. Create error with specified error code.return errors.WithCode(code.ErrDatabase, "user 'Lingfei Kong' not found.")
}

上述代码中,通过WithCode函数来创建新的withCode类型的错误;通过WrapC来将一个error封装成一个withCode类型的错误;通过IsCode来判断一个error链中是否包含指定的code。

withCode错误实现了一个func (w *withCode) Format(state fmt.State, verb rune)方法,该方法用来打印不同格式的错误信息,见下表:

例如,%+v会打印以下错误信息:

get user failed. - #1 [/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/errortrack_errors.go:19 (main.getUser)] (100101) Database error; user 'Lingfei Kong' not found. - #0 [/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/errortrack_errors.go:26 (main.queryDatabase)] (100101) Database error

 这里我简单解释一下。

首先, withCode 中包含了int类型的错误码,例如100101

其次,当使用github.com/marmotedu/errors包的时候,需要调用Register或者MustRegister,将一个Coder注册到github.com/marmotedu/errors开辟的内存中,数据结构为:

var codes = map[int]Coder{}

Coder是一个接口,定义为:

type Coder interface {// HTTP status that should be used for the associated error code.HTTPStatus() int// External (user) facing error text.String() string// Reference returns the detail documents for user.Reference() string// Code returns the code of the coderCode() int
}

这样 withCode 的Format方法,就能够通过 withCode 中的code字段获取到对应的Coder,并通过Coder提供的HTTPStatus、String、Reference、Code函数,来获取 withCode 中code的详细信息,最后格式化打印。

这里要注意,我们实现了两个注册函数:RegisterMustRegister,二者唯一区别是:当重复定义同一个错误Code时,MustRegister会panic 

XXX()MustXXX()的函数命名方式,是一种Go代码设计技巧

最后,我还有一个建议:在实际的生产环境中,我们可以使用JSON格式打印日志,JSON格式的日志可以非常方便的供日志系统解析。我们可以根据需要,选择%#-v%#+v两种格式。

错误包在代码中,经常被调用,所以我们要保证错误包一定要是高性能的,否则很可能会影响接口的性能。这里,我们再来看下github.com/marmotedu/errors包的性能。

在这里,我们把这个错误包跟go标准库的 errors 包,以及 github.com/pkg/errors 包进行对比,来看看它们的性能:

$  go test -test.bench=BenchmarkErrors -benchtime="3s"
goos: linux
goarch: amd64
pkg: github.com/marmotedu/errors
BenchmarkErrors/errors-stack-10-8         	57658672	        61.8 ns/op	      16 B/op	       1 allocs/op
BenchmarkErrors/pkg/errors-stack-10-8     	 2265558	      1547 ns/op	     320 B/op	       3 allocs/op
BenchmarkErrors/marmot/errors-stack-10-8  	 1903532	      1772 ns/op	     360 B/op	       5 allocs/op
BenchmarkErrors/errors-stack-100-8        	 4883659	       734 ns/op	      16 B/op	       1 allocs/op
BenchmarkErrors/pkg/errors-stack-100-8    	 1202797	      2881 ns/op	     320 B/op	       3 allocs/op
BenchmarkErrors/marmot/errors-stack-100-8 	 1000000	      3116 ns/op	     360 B/op	       5 allocs/op
BenchmarkErrors/errors-stack-1000-8       	  505636	      7159 ns/op	      16 B/op	       1 allocs/op
BenchmarkErrors/pkg/errors-stack-1000-8   	  327681	     10646 ns/op	     320 B/op	       3 allocs/op
BenchmarkErrors/marmot/errors-stack-1000-8         	  304160	     11896 ns/op	     360 B/op	       5 allocs/op
PASS
ok  	github.com/marmotedu/errors	39.200s

可以看到github.com/marmotedu/errorsgithub.com/pkg/errors包的性能基本持平。在对比性能时,重点关注ns/op,也即每次error操作耗费的纳秒数。另外,我们还需要测试不同error嵌套深度下的error操作性能,嵌套越深,性能越差。例如:在嵌套深度为10的时候, github.com/pkg/errors 包ns/op值为1547, github.com/marmotedu/errors 包ns/op值为1772。可以看到,二者性能基本保持一致。

具体性能数据对比见下表:

我们是通过BenchmarkErrors测试函数来测试error包性能的,你感兴趣可以打开链接看看。

如何记录错误?

上面,我们一起看了怎么设计一个优秀的错误包,那如何用我们设计的错误包来记录错误呢?

根据我的开发经验,我推荐两种记录错误的方式,可以帮你快速定位问题。

方式一:通过github.com/marmotedu/errors包提供的错误堆栈能力,来跟踪错误。

具体你可以看看下面的代码示例。以下代码保存在errortrack_errors.go中。

package mainimport ("fmt""github.com/marmotedu/errors"code "github.com/marmotedu/sample-code"
)func main() {if err := getUser(); err != nil {fmt.Printf("%+v\n", err)}
}func getUser() error {if err := queryDatabase(); err != nil {return errors.Wrap(err, "get user failed.")}return nil
}func queryDatabase() error {return errors.WithCode(code.ErrDatabase, "user 'Lingfei Kong' not found.")
}

执行上述的代码:

$ go run errortrack_errors.go
get user failed. - #1 [/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/errortrack_errors.go:19 (main.getUser)] (100101) Database error; user 'Lingfei Kong' not found. - #0 [/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/errors/errortrack_errors.go:26 (main.queryDatabase)] (100101) Database error

可以看到,打印的日志中打印出了详细的错误堆栈,包括错误发生的函数、文件名、行号和错误信息,通过这些错误堆栈,我们可以很方便地定位问题。

你使用这种方法时,我推荐的用法是,在错误最开始处使用 errors.WithCode() 创建一个 withCode类型的错误。上层在处理底层返回的错误时,可以根据需要,使用Wrap函数基于该错误封装新的错误信息。如果要包装的error不是用github.com/marmotedu/errors包创建的,建议用 errors.WithCode() 新建一个error。

方式二:在错误产生的最原始位置调用日志包记录函数,打印错误信息,其他位置直接返回(当然,也可以选择性的追加一些错误信息,方便故障定位)。示例代码(保存在errortrack_log.go)如下:

package mainimport ("fmt""github.com/marmotedu/errors""github.com/marmotedu/log"code "github.com/marmotedu/sample-code"
)func main() {if err := getUser(); err != nil {fmt.Printf("%v\n", err)}
}func getUser() error {if err := queryDatabase(); err != nil {return err}return nil
}func queryDatabase() error {opts := &log.Options{Level:            "info",Format:           "console",EnableColor:      true,EnableCaller:     true,OutputPaths:      []string{"test.log", "stdout"},ErrorOutputPaths: []string{},}log.Init(opts)defer log.Flush()err := errors.WithCode(code.ErrDatabase, "user 'Lingfei Kong' not found.")if err != nil {log.Errorf("%v", err)}return err
}

执行以上代码:

$ go run errortrack_log.go
2021-07-03 14:37:31.597	ERROR	errors/errortrack_log.go:41	Database error
Database error

当错误发生时,调用log包打印错误。通过log包的caller功能,可以定位到log语句的位置,也就是定位到错误发生的位置。你使用这种方式来打印日志时,我有两个建议。

  • 只在错误产生的最初位置打印日志,其他地方直接返回错误,一般不需要再对错误进行封装。
  • 当代码调用第三方包的函数时,第三方包函数出错时打印错误信息。比如:
if err := os.Chdir("/root"); err != nil {log.Errorf("change dir failed: %v", err)
}

一个错误码的具体实现

接下来,我们看一个依据上一讲介绍的错误码规范的具体错误码实现github.com/marmotedu/sample-code

sample-code实现了两类错误码,分别是通用错误码(sample-code/base.go)和业务模块相关的错误码(sample-code/apiserver.go)。

首先,我们来看通用错误码的定义:

// 通用: 基本错误
// Code must start with 1xxxxx
const (// ErrSuccess - 200: OK.ErrSuccess int = iota + 100001// ErrUnknown - 500: Internal server error.ErrUnknown// ErrBind - 400: Error occurred while binding the request body to the struct.ErrBind// ErrValidation - 400: Validation failed.ErrValidation// ErrTokenInvalid - 401: Token invalid.ErrTokenInvalid
)

在代码中,我们通常使用整型常量(ErrSuccess)来代替整型错误码(100001),因为使用ErrSuccess时,一看就知道它代表的错误类型,可以方便开发者使用。

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

// ErrCode implements `github.com/marmotedu/errors`.Coder interface.
type ErrCode struct {// C refers to the code of the ErrCode.C int// HTTP status that should be used for the associated error code.HTTP int// External (user) facing error text.Ext string// Ref specify the reference document.Ref string
}

可以看到ErrCode结构体包含了以下信息:

  • int类型的业务码。
  • 对应的HTTP Status Code。
  • 暴露给外部用户的消息。
  • 错误的参考文档。

下面是一个具体的Coder示例:

coder := &ErrCode{C:    100001,HTTP: 200,Ext:  "OK",Ref:  "https://github.com/marmotedu/sample-code/blob/master/README.md",
}

接下来,我们就可以调用github.com/marmotedu/errors包提供的Register或者MustRegister函数,将Coder注册到github.com/marmotedu/errors包维护的内存中。

一个项目有很多个错误码,如果每个错误码都手动调用MustRegister函数会很麻烦,这里我们通过代码自动生成的方法,来生成register函数调用:

//go:generate codegen -type=int
//go:generate codegen -type=int -doc -output ./error_code_generated.md

//go:generate codegen -type=int 会调用codegen工具,生成sample_code_generated.go源码文件:

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")register(ErrValidation, 400, "Validation failed")// other register function call
}

这些register调用放在init函数中,在加载程序的时候被初始化。

这里要注意,在注册的时候,我们会检查HTTP Status Code,只允许定义200、400、401、403、404、500这6个HTTP错误码。这里通过程序保证了错误码是符合HTTP Status Code使用要求的。

//go:generate codegen -type=int -doc -output ./error_code_generated.md会生成错误码描述文档 error_code_generated.md。当我们提供API文档时,也需要记着提供一份错误码描述文档,这样客户端才可以根据错误码,知道请求是否成功,以及具体发生哪类错误,好针对性地做一些逻辑处理。

codegen工具会根据错误码注释生成sample_code_generated.goerror_code_generated.md文件:

// ErrSuccess - 200: OK.ErrSuccess int = iota + 100001

codegen工具之所以能够生成sample_code_generated.goerror_code_generated.md,是因为我们的错误码注释是有规定格式的:// <错误码整型常量> - <对应的HTTP Status Code>: <External Message>.

codegen工具可以在IAM项目根目录下,执行以下命令来安装:

$ make tools.install.codegen

安装完 codegen 工具后,可以在 github.com/marmotedu/sample-code 包根目录下执行 go generate 命令,来生成sample_code_generated.goerror_code_generated.md。这里有个技巧需要你注意:生成的文件建议统一用 xxxx_generated.xx 来命名,这样通过 generated ,我们就知道这个文件是代码自动生成的,有助于我们理解和使用。

在实际的开发中,我们可以将错误码独立成一个包,放在 internal/pkg/code/目录下,这样可以方便整个应用调用。例如IAM的错误码就放在IAM项目根目录下的internal/pkg/code/目录下。

我们的错误码是分服务和模块的,所以这里建议你把相同的服务放在同一个Go源文件中,例如IAM的错误码存放文件:

$ ls base.go apiserver.go authzserver.go 
apiserver.go  authzserver.go  base.go

一个应用中会有多个服务,例如IAM应用中,就包含了iam-apiserver、iam-authz-server、iam-pump三个服务。 

另外,同一个服务中不同模块的错误码,可以按以下格式来组织:相同模块的错误码放在同一个const代码块中,不同模块的错误码放在不同的const代码块中。每个const代码块的开头注释就是该模块的错误码定义。例如:

// iam-apiserver: user errors.
const (// ErrUserNotFound - 404: User not found.ErrUserNotFound int = iota + 110001// ErrUserAlreadyExist - 400: User already exist.ErrUserAlreadyExist
)// iam-apiserver: secret errors.
const (// ErrEncrypt - 400: Secret reach the max count.ErrReachMaxCount int = iota + 110101//  ErrSecretNotFound - 404: Secret not found.ErrSecretNotFound
)

最后,我们还需要将错误码定义记录在项目的文件中,供开发者查阅、遵守和使用,例如IAM项目的错误码定义记录文档为code_specification.md。这个文档中记录了错误码说明、错误描述规范和错误记录规范等。

错误码实际使用方法示例

 我就举一个在 gin web 框架中使用该错误码的例子:

// Response defines project response format which in marmotedu organization.
type Response struct {Code      errors.Code `json:"code,omitempty"`Message   string      `json:"message,omitempty"`Reference string      `json:"reference,omitempty"`Data      interface{} `json:"data,omitempty"`
}// WriteResponse used to write an error and JSON data into response.
func WriteResponse(c *gin.Context, err error, data interface{}) {if err != nil {coder := errors.ParseCoder(err)c.JSON(coder.HTTPStatus(), Response{Code:      coder.Code(),Message:   coder.String(),Reference: coder.Reference(),Data:      data,})}c.JSON(http.StatusOK, Response{Data: data})
}func GetUser(c *gin.Context) {log.Info("get user function called.", "X-Request-Id", requestid.Get(c))// Get the user by the `username` from the database.user, err := store.Client().Users().Get(c.Param("username"), metav1.GetOptions{})if err != nil {core.WriteResponse(c, code.ErrUserNotFound.Error(), nil)return}core.WriteResponse(c, nil, user)
}

上述代码中,通过WriteResponse统一处理错误。在 WriteResponse 函数中,如果err != nil,则从error中解析出Coder,并调用Coder提供的方法,获取错误相关的Http Status Code、int类型的业务码、暴露给用户的信息、错误的参考文档链接,并返回JSON格式的信息。如果 err == nil 则返回200和数据。

 

课后练习

  1. 在这门课里,我们定义了base、iam-apiserver服务的错误码,请试着定义iam-authz-server服务的错误码,并生成错误码文档。
  2. 思考下,这门课的错误包和错误码设计能否满足你当前的项目需求,如果觉得不能满足,可以在留言区分享你的看法。

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

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

相关文章

【MySQL笔记】SELECT COUNT(*) 的时候,加不加where条件有差别吗?

文章目录 前言实验结论 前言 这部分很多帖子都只在问题里罗列下&#xff0c;好像也没详细解答 其实就是跟InnoDB优先走二级索引的优化有关&#xff0c;前面也提到了”优化的前提是查询语句中不包含where条件和group by条件“ 还不太了解这个优化的朋友可以看上一篇帖子 实验 …

如何使用剪映专业版剪辑视频

1.操作界面功能介绍 2.时间线的使用 拖动前端后端缩减时长&#xff0c;有多个素材可以拖动调节前后顺序拼接。 分割视频 删除

CentOs7.9中修改Mysql8.0.28默认的3306端口防止被端口扫描入侵

若你的服务器被入侵&#xff0c;可以从这些地方找到证据&#xff1a; 若有上述信息&#xff0c;300%是被入侵了&#xff0c;重装服务器系统以后再重装Mysql数据库&#xff0c;除了设置一个复杂的密码以外&#xff0c;还需要修改默认的Mysql访问端口&#xff0c;逃避常规端口扫描…

目标检测——工业安全生产环境违规使用手机的识别

一、重要性及意义 首先&#xff0c;工业安全生产环境涉及到许多复杂的工艺和设备&#xff0c;这些设备和工艺往往需要高精度的操作和严格的监管。如果员工在生产过程中违规使用手机&#xff0c;不仅可能分散其注意力&#xff0c;降低工作效率&#xff0c;更可能因操作失误导致…

1688采集商品信息 马帮 店小秘 芒果采集API接口 java php

1688详情API接口是一种基于开放平台的应用程序接口&#xff0c;它能够实现与1688平台的数据交互&#xff0c;让商家能够更加方便地获取商品详情、库存信息、价格变动等重要数据。通过这一接口&#xff0c;商家可以实时掌握市场动态&#xff0c;快速响应消费者需求&#xff0c;从…

Leetcode 617. 合并二叉树

心路历程&#xff1a; 看到两颗二叉树的问题&#xff0c;第一反应想到了同频遍历&#xff0c;然后每一步创建新的结点&#xff0c;虽然也写出来了但是代码比较长&#xff0c;而且空间复杂度比较高&#xff0c;好处是没有修改原始的两个二叉树的结果。 后来看了网上的解答&…

探索父进程和子进程

文章目录 通过系统调用查看进程PID父进程、子进程 通过系统调用创建进程-fork初识为什么fork给父进程返回子进程的PID&#xff0c;给子进程返回0fork函数如何做到返回两个值一个变量为什么同时会有两个返回值&#xff1f;bash总结 通过系统调用查看进程PID getpid()函数可以获…

SpringBoot 集成分布式任务调度 XXL-JOB【保姆级上手】

文章目录 XXL-JOB 介绍分布式任务调度XXL-JOB 概述 快速入门下载源码初始化调度数据库编译源码调度中心调度中心介绍配置调度中心部署调度中心集群部署调度中心&#xff08;可选&#xff09;Docker 镜像方式搭建调度中心&#xff08;可选&#xff09; 执行器执行器介绍添加依赖…

动态系统的稳定性【3】-庞加莱截面

引言&#xff1a; 非线性动力学在众多领域中都得到了较广泛的应用&#xff0c;但各领域研究方向有所不同。在论文中如果涉及到方程的非线性&#xff0c;总会需要定性的分析一下&#xff0c;由于方法比较类似&#xff0c;所以常常也可以参阅别的领域的非线性分析相关的方法或者好…

抖音小店没达人带货?学会这几个话术!精准拿捏!全是干货快收藏

哈喽~我是电商月月 一说抖音小店如何寻找达人&#xff0c;了解过相关知识的人都会背了&#xff01; 精选联盟的选品广场&#xff0c;达人广场&#xff0c;招商团长&#xff01; 但操作一学就会&#xff0c;动手就废&#xff0c;商品也挂佣金了&#xff0c;达人也打招呼了&am…

7、Qwen-7B 部署实践

1、Qwen&#xff08;通义千问&#xff09;介绍 Qwen&#xff08;通义千问&#xff09;由阿里云团队研发&#xff0c;其训练数据覆盖多语言&#xff0c;但主要以中文和英文为主。Qwen的系列模型主要有两类&#xff0c;一类是基座模型Qwen&#xff0c;所谓基座模型&#xff0c;指…

JumpServer 堡垒主机

JumpServer 堡垒机帮助企业以更安全的方式管控和登陆各种类型的资产 SSH&#xff1a;Linux/Unix/网络设备等Windows&#xff1a;Web方式连接/原生RDP连接数据库&#xff1a;MySQL、Oracle、SQLServer、PostgreSQL等Kubernetes&#xff1a;连接到K8s集群中的PodsWeb站点&#x…