您好,登錄后才能下訂單哦!
這篇“go語言規范RESTful API業務錯誤處理的方法是什么”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“go語言規范RESTful API業務錯誤處理的方法是什么”文章吧。
現如今,主流的 Web API 都采用 RESTful 設計風格,對于接口返回的 HTTP 狀態碼和響應內容都有統一的規范。針對接口錯誤響應,一般都會返回一個 Code(錯誤碼)和 Message(錯誤消息內容),通常錯誤碼 Code 用來定位一個唯一的錯誤,錯誤消息 Message 用來展示錯誤信息。
雖然 RESTful API 能夠通過 HTTP 狀態碼來標記一個請求的成功或失敗,但 HTTP 狀態碼作為一個通用的標準,并不能很好的表達業務錯誤。
比如一個 500 的錯誤響應,可能是由后端數據庫連接異常引起的、也可能由內部代碼邏輯錯誤引起,這些都無法通過 HTTP 狀態碼感知到,如果程序出現錯誤,不方便開發人員 Debug。
因此我們有必要設計一套用來標識業務錯誤的錯誤碼,這有別于 HTTP 狀態碼,是跟系統具體業務息息相關的。
在設計錯誤碼之前,我們需要明確下錯誤碼應該具備哪些屬性,以滿足業務需要。
錯誤碼必須是唯一的。只有錯誤碼是唯一的才方便在程序出錯時快速定位問題,不然程序出錯,返回錯誤碼不唯一,想要根據錯誤碼排查問題,就要針對這一錯誤碼所表示的錯誤列表進行逐一排查。
錯誤碼需要是可閱讀的。意思是說,通過錯誤碼,我們就能快速定位到是系統的哪個組件出現了錯誤,并且知道錯誤的類型,不然也談不上叫「業務錯誤碼」了。一個清晰可讀的錯誤碼在微服務系統中定位問題尤其有效。
通過錯誤碼能夠方便知道 HTTP 狀態碼。這一點往往容易被人忽略,不過我比較推薦這種做法,因為在 Review 代碼時,通過返回錯誤碼,就能很容易知道接口返回 HTTP 狀態碼,這不僅方便理解代碼,更方便錯誤的統一處理。
錯誤碼的設計我們可以參考業內使用量比較大的開放 API 設計,比較有代表性的是阿里云和新浪網的開放 API。
如以下是一個阿里云 ECS 接口錯誤的返回:
{ "RequestId": "5E571499-13C5-55E3-9EA6-DEFA0DBC85E4", "HostId": "ecs-cn-hangzhou.aliyuncs.com", "Code": "InvalidOperation.NotSupportedEndpoint", "Message": "The specified endpoint can't operate this region. Please use API DescribeRegions to get the appropriate endpoint, or upgrade your SDK to latest version.", "Recommend": "https://next.api.aliyun.com/troubleshoot?q=InvalidOperation.NotSupportedEndpoint&product=Ecs" }
可以發現,Code 和 Message 都為字符串類型,并且還有 RequestId(當前請求唯一標識)、HostId(Host 唯一標識)、Recommend(錯誤診斷地址),可以說這個錯誤信息非常全面了。
再來看下新浪網開放 API 錯誤返回結果的設計:
{ "request": "/statuses/home_timeline.json", "error_code": "20502", "error": "Need you follow uid." }
相比阿里云,新浪網的錯誤返回更簡潔一些。其中 request 為請求路徑,error_code 即為錯誤碼 Code,error 則表示錯誤信息 Message。
錯誤代碼 20502
說明如下:
2 | 05 | 02 |
---|---|---|
服務級錯誤(1為系統級錯誤) | 服務模塊代碼 | 具體錯誤代碼 |
新浪網的錯誤碼為數字類型的字符串,相比阿里云的錯誤碼要簡短不少,并且對程序更加友好,也是我個人更推薦的設計。
結合市面上這些優秀的開放 API 錯誤碼設計,以及我在實際開發中的工作總結,我設計的錯誤碼規則如下:
業務錯誤碼由 8 位純數字組成,類型為 int
。
業務錯誤碼示例格式:40001002
。
錯誤碼說明:
1-3 位 | 4-5 位 | 6-8 位 |
---|---|---|
400 | 01 | 002 |
HTTP 狀態碼 | 組件編號 | 組件內部錯誤碼 |
錯誤碼設計為純數字主要是為了程序中使用起來更加方便,比如根據錯誤碼計算 HTTP 狀態碼,只需要通過簡單的數學取模計算就能做到。
使用兩位數字來標記不同組件,最多能表示 99 個組件,即使項目全部采用微服務開發,一般來說也是足夠用的。
最后三位代表組件內部錯誤碼,最多能表示 1000 個錯誤。其實通常來說一個組件內部是用不上這么多錯誤的,如果組件較小,完全可以設計成兩位數字。
另外,有些廠商中還會設計一些公共的錯誤碼,可以稱為「全局錯誤碼」,這些錯誤碼在各組件間通用,以此來減少定義重復錯誤。在我們的錯誤碼設計中,可以將組件編號為 00
的標記為全局錯誤碼,其他組件編號從 01
開始。
有了錯誤碼,還需要定義錯誤響應格式,設計一個標準的 API 錯誤響應格式如下:
{ "code": 50000000, "message": "系統錯誤", "reference": "https://github.com/jianghushinian/gokit/tree/main/errors" }
code
即為錯誤碼,message
為錯誤信息,reference
則是錯誤文檔地址,用來告知用戶如何解決這個錯誤,對標的是阿里云錯誤響應中的 Recommend
字段。
因為每一個錯誤碼和錯誤信息以及錯誤文檔地址都是一一對應的,所以我們需要一個對象來保存這些信息,在 Go 中可以使用結構體。
可以設計如下結構體:
type apiCode struct { code int msg string ref string }
這是一個私有結構體,外部項目要想使用,則需要一個構造函數:
func NewAPICode(code int, message string, reference ...string) APICoder { ref := "" if len(reference) > 0 { ref = reference[0] } return &apiCode{ code: code, msg: message, ref: ref, } }
其中 reference
被設計為可變參數,如果不傳則默認為空。
NewAPICode
返回值 APICoder
是一個接口,這在 Go 中是一種慣用做法。通過接口可以解耦,方便依賴 apiCode
的代碼編寫測試,用戶可以對 APICoder 進行 Mock;另一方面,我們稍后會為 apiCode
實現對應的錯誤包,使用接口來表示錯誤碼可以方便用戶定義自己的 apiCode
類型。
為了便于使用,apiCode
提供了如下幾個能力:
func (a *apiCode) Code() int { return a.code } func (a *apiCode) Message() string { return a.msg } func (a *apiCode) Reference() string { return a.ref } func (a *apiCode) HTTPStatus() int { v := a.Code() for v >= 1000 { v /= 10 } return v }
至此 APICoder
接口接口的定義也就有了:
type APICoder interface { Code() int Message() string Reference() string HTTPStatus() int }
apiCode
則實現了 APICoder
接口。
現在我們可以通過如下方式創建錯誤碼結構體對象:
var ( CodeBadRequest = NewAPICode(40001001, "請求不合法") CodeUnknownError = NewAPICode(50001001, "系統錯誤", "https://github.com/jianghushinian/gokit/tree/main/errors") )
設計好了錯誤碼,并不能直接使用,我們還需要一個與之配套的錯誤包來簡化錯誤碼的使用。
錯誤包要能夠完美支持上面設計的錯誤碼。所以需要使用 APICoder
來構造錯誤對象。
錯誤包應該能夠查看原始錯誤原因。這就需要實現 Unwrap
方法,Wrap/Unwrap
方法是在 Go 1.13 中被加入進 errors
包的,目的是能夠處理嵌套錯誤。
錯誤包應該能夠支持對內對外展示不同信息。這就需要實現 Format
方法,根據需要可以將錯誤格式化成不同輸出。
錯誤包應該能夠支持展示堆棧信息。這對 Debug 來說相當重要,也是 Go 自帶的 errors
包不足的地方。
為了方便在日志中記錄結構化錯誤信息,錯誤包還要能夠支持 JSON 序列化。這需要實現 MarshalJSON/UnmarshalJSON
兩個方法。
一個錯誤對象結構體設計如下:
type apiError struct { coder APICoder cause error *stack }
其中 coder
用來保存實現了 APICoder
接口的對象,cause
用來記錄錯誤原因,stack
用來展示錯誤堆棧。
錯誤對象的構造函數如下:
var WrapC = NewAPIError func NewAPIError(coder APICoder, cause ...error) error { var c error if len(cause) > 0 { c = cause[0] } return &apiError{ coder: coder, cause: c, stack: callers(), } }
NewAPIError
通過 APICoder
來創建錯誤對象,第二個參數為一個可選的錯誤原因。
其實構造一個錯誤對象也就是對一個錯誤進行 Wrap
的過程,所以我還為構造函數 NewAPIError
定義了一個別名 WrapC
,表示使用錯誤碼將一個錯誤包裝成一個新的錯誤。
一個錯誤對象必須要實現 Error
方法:
func (a *apiError) Error() string { return fmt.Sprintf("[%d] - %s", a.coder.Code(), a.coder.Message()) }
默認情況下,獲取到的錯誤內容只包含錯誤碼 Code 和錯誤信息 Message。
為了方便獲取被包裝錯誤的原始錯誤,還要實現 Unwrap
方法:
func (a *apiError) Unwrap() error { return a.cause }
為了能在打印或寫入日志時展示不同信息,則要實現 Format
方法:
func (a *apiError) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { str := a.Error() if a.Unwrap() != nil { str += " " + a.Unwrap().Error() } _, _ = io.WriteString(s, str) a.stack.Format(s, verb) return } if s.Flag('#') { cause := "" if a.Unwrap() != nil { cause = a.Unwrap().Error() } data, _ := json.Marshal(errorMessage{ Code: a.coder.Code(), Message: a.coder.Message(), Reference: a.coder.Reference(), Cause: cause, Stack: fmt.Sprintf("%+v", a.stack), }) _, _ = io.WriteString(s, string(data)) return } fallthrough case 's': _, _ = io.WriteString(s, a.Error()) case 'q': _, _ = fmt.Fprintf(s, "%q", a.Error()) } }
Format
方法能夠支持在使用 fmt.Printf("%s", apiError)
格式化輸出時打印定制化的信息。
Format
方法支持的不同格式輸出如下:
格式占位符 | 輸出信息 |
---|---|
%s | 錯誤碼、錯誤信息 |
%v | 錯誤碼、錯誤信息,與 %s 等價 |
%+v | 錯誤碼、錯誤信息、錯誤原因、錯誤堆棧 |
%#v | JSON 格式的 錯誤碼、錯誤信息、錯誤文檔地址、錯誤原因、錯誤堆棧 |
%q | 在 錯誤碼、錯誤信息 外層增加了一個雙引號 |
這些錯誤格式基本上能滿足所有業務開發中的需求了,如果還有其他格式需要,則可以在此基礎上進一步開發 Format
方法。
用來進行 JSON 序列化和反序列化的 MarshalJSON/UnmarshalJSON
方法實現如下:
func (a *apiError) MarshalJSON() ([]byte, error) { return json.Marshal(&errorMessage{ Code: a.coder.Code(), Message: a.coder.Message(), Reference: a.coder.Reference(), }) } func (a *apiError) UnmarshalJSON(data []byte) error { e := &errorMessage{} if err := json.Unmarshal(data, e); err != nil { return err } a.coder = NewAPICode(e.Code, e.Message, e.Reference) return nil } type errorMessage struct { Code int `json:"code"` Message string `json:"message"` Reference string `json:"reference,omitempty"` Cause string `json:"cause,omitempty"` Stack string `json:"stack,omitempty"` }
為了不對外部暴露敏感信息,對外的 HTTP API 只會返回 Code
、Message
、Reference
(可選)三個字段,對內需要額外展示錯誤原因以及錯誤堆棧。所以 errorMessage
中 Reference
、Cause
、Stack
字段都帶有 omitempty
屬性,這樣在 MarshalJSON
時只會序列化 Code
、Message
、Reference
這三個字段。
至此,我們就實現了錯誤包的設計。
通過上面的講解,我們了解了錯誤碼和錯誤包的設計規范,接下來看看如何使用它們。這里以錯誤碼及錯誤包在 Gin 中的使用為例進行講解。
使用 Gin 構建一個簡單的 Web Server 如下:
package main import ( "errors" "fmt" "strconv" "github.com/gin-gonic/gin" apierr "github.com/jianghushinian/gokit/errors" ) var ( ErrAccountNotFound = errors.New("account not found") ErrDatabase = errors.New("database error") ) var ( CodeBadRequest = NewAPICode(40001001, "請求不合法") CodeNotFound = NewAPICode(40401001, "資源未找到") CodeUnknownError = NewAPICode(50001001, "系統錯誤", "https://github.com/jianghushinian/gokit/tree/main/errors") ) type Account struct { ID int `json:"id"` Name string `json:"name"` } func AccountOne(id int) (*Account, error) { for _, v := range accounts { if id == v.ID { return &v, nil } } // 模擬返回數據庫錯誤 if id == 500 { return nil, ErrDatabase } return nil, ErrAccountNotFound } var accounts = []Account{ {ID: 1, Name: "account_1"}, {ID: 2, Name: "account_2"}, {ID: 3, Name: "account_3"}, } func ShowAccount(c *gin.Context) { id := c.Param("id") aid, err := strconv.Atoi(id) if err != nil { // 將 errors 包裝成 APIError 返回 ResponseError(c, apierr.WrapC(CodeBadRequest, err)) return } account, err := AccountOne(aid) if err != nil { switch { case errors.Is(err, ErrAccountNotFound): err = apierr.NewAPIError(CodeNotFound, err) case errors.Is(err, ErrDatabase): err = apierr.NewAPIError(CodeUnknownError, fmt.Errorf("account %d: %w", aid, err)) } ResponseError(c, err) return } ResponseOK(c, account) } func main() { r := gin.Default() r.GET("/accounts/:id", ShowAccount) if err := r.Run(":8080"); err != nil { panic(err) } }
在這個 Web Server 中定義了一個 ShowAccount
函數,用來處理獲取賬號邏輯,在 ShowAccount
內部程序執行成功返回 ResponseOK(c, account)
,失敗則返回 ResponseError(c, err)
。
在處理返回失敗的響應時,都會通過 apierr.WrapC
或 apierr.NewAPIError
將底層函數返回的初始錯誤進行一層包裝,根據錯誤級別,包裝成不同的錯誤碼進行返回。
其中 ResponseOK
和 ResponseError
定義如下:
func ResponseOK(c *gin.Context, spec interface{}) { if spec == nil { c.Status(http.StatusNoContent) return } c.JSON(http.StatusOK, spec) } func ResponseError(c *gin.Context, err error) { log(err) e := apierr.ParseCoder(err) httpStatus := e.HTTPStatus() if httpStatus >= 500 { // send error msg to email/feishu/sentry... go fakeSendErrorEmail(err) } c.AbortWithStatusJSON(httpStatus, err) } // log 打印錯誤日志,輸出堆棧 func log(err error) { fmt.Println("========== log start ==========") fmt.Printf("%+v\n", err) fmt.Println("========== log end ==========") } // fakeSendErrorEmail 模擬將錯誤信息發送到郵件,JSON 格式 func fakeSendErrorEmail(err error) { fmt.Println("========== error start ==========") fmt.Printf("%#v\n", err) fmt.Println("========== error end ==========") }
ResponseOK
其實就是 Gin 框架的正常返回,ResponseError
則專門用來處理并返回 API 錯誤。
在 ResponseError
中首先通過 log(err)
來記錄錯誤日志,在其內部使用 fmt.Printf("%+v\n", err)
進行打印。
之后我們還對 HTTP 狀態碼進行了判斷,大于 500 的錯誤將會發送郵件通知,這里使用 fmt.Printf("%#v\n", err)
進行模擬。
其中 apierr.ParseCoder(err)
能夠從一個錯誤對象中獲取到實現了 APICoder
的錯誤碼對象,實現如下:
func ParseCoder(err error) APICoder { for { if e, ok := err.(interface { Coder() APICoder }); ok { return e.Coder() } if errors.Unwrap(err) == nil { return CodeUnknownError } err = errors.Unwrap(err) } }
這樣,我們就能夠通過一個簡單的 Web Server 示例程序來演示如何使用錯誤碼和錯誤包了。
可以通過 go run main.go
啟動這個 Web Server。
先來看下在這個 Web Server 中一個正常的返回結果是什么樣,使用 cURL 來發送一個請求:curl http://localhost:8080/accounts/1
。
客戶端得到如下響應結果:
{ "id": 1, "name": "account_1" }
服務端打印正常的請求日志:
再來測試下請求一個不存在的賬號:curl http://localhost:8080/accounts/12
。
客戶端得到如下響應結果:
{ "code": 40401001, "message": "資源未找到" }
返回結果中沒有 reference
字段,是因為對于 reference
為空的情況,在 JSON 序列化過程中會被隱藏。
服務端打印的錯誤日志如下:
========== log start ==========
[40401001] - 資源未找到 account not found
main.ShowAccount
/app/errors/examples/main.go:56
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.LoggerWithConfig.func1
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620
github.com/gin-gonic/gin.(*Engine).ServeHTTP
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576
net/http.serverHandler.ServeHTTP
/usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
/usr/local/go/src/net/http/server.go:1991
runtime.goexit
/usr/local/go/src/runtime/asm_arm64.s:1165
========== log end ==========
可以發現,錯誤日志中不僅打印了錯誤碼([40401001])和錯誤信息(資源未找到),還打印了錯誤原因(account not found)以及下面的錯誤堆棧。
如此清晰的錯誤日志得益于我們實現的 Format
函數的強大功能。
現在再來觸發一個 HTTP 狀態碼為 500 的錯誤響應:curl http://localhost:8080/accounts/500
。
客戶端得到如下響應結果:
{ "code": 50001001, "message": "系統錯誤", "reference": "https://github.com/jianghushinian/gokit/tree/main/errors" }
這次得到一個帶有 reference
字段的完整錯誤響應。
服務端打印的錯誤日志如下:
========== log start ==========
[50001001] - 系統錯誤 account 500: database error
main.ShowAccount
/app/errors/examples/main.go:58
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.LoggerWithConfig.func1
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240
github.com/gin-gonic/gin.(*Context).Next
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620
github.com/gin-gonic/gin.(*Engine).ServeHTTP
/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576
net/http.serverHandler.ServeHTTP
/usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
/usr/local/go/src/net/http/server.go:1991
runtime.goexit
/usr/local/go/src/runtime/asm_arm64.s:1165
========== log end ==========
[GIN] 2023/03/05 - 02:02:28 | 500 | 426.292µs | 127.0.0.1 | GET "/accounts/500"
========== error start ==========
{"code":50001001,"message":"系統錯誤","reference":"https://github.com/jianghushinian/gokit/tree/main/errors","cause":"account 500: database error","stack":"\nmain.ShowAccount\n\t/app/errors/examples/main.go:58\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.CustomRecoveryWithWriter.func1\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.LoggerWithConfig.func1\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2947\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1991\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_arm64.s:1165"}
========== error end ==========
這一次除了 log
函數打印的日志,還能看到 fakeSendErrorEmail
函數打印的日志,正是一個 JSON 格式的結構化日志。
以上便是我們設計的錯誤碼及錯誤包在實際開發場景中的應用。
根據我的經驗,總結了一些錯誤碼及錯誤包的使用建議,現在將其分享給你。
HTTP 狀態碼大概分為 5 大類,分別是 1XX、2XX、3XX、4XX、5XX。根據我的實際工作經驗,我們并不會使用全部的狀態碼,最常用的狀態碼不超過 10 個。
所以即使我們設計的業務錯誤碼支持攜帶 HTTP 狀態碼,但也不推薦使用過多的 HTTP 狀態碼,以免加重前端工作量。
推薦在錯誤碼中使用的 HTTP 狀態碼如下:
400: 請求不合法
401: 認證失敗
403: 授權失敗
404: 資源未找到
500: 系統錯誤
其中 4XX 代表客戶端錯誤,而如果是服務端錯誤,則統一使用 500 狀態碼,具體錯誤原因可以通過業務錯誤碼定位。
由于我們設計的錯誤包支持 Unwrap
操作,所以建議出現錯誤時的處理流程如下:
最底層代碼遇到錯誤時通過 errors.New/fmt.Errorf
來創建一個錯誤對象,然后將錯誤返回(可選擇性的記錄一條日志)。
func Query(id int) (obj, error) { // do something return nil, fmt.Errorf("%d not found", id) }
中間過程中處理函數遇到下層函數返回的錯誤,不做任何額外處理,直接將其向上層返回。
if err != nil { return err }
在處理用戶請求的 Handler 函數中(如 ShowAccount
)通過 apierr.WrapC
將錯誤包裝成一個 APIError
返回。
if err != nil { return apierr.WrapC(CodeNotFound, err) }
最上層代碼通過在框架層面實現的中間件(如實現一個 after hook middleware)來統一處理錯誤,打印完整錯誤日志、發送郵件提醒等,并將安全的錯誤信息返回給前端。如我們實現的 ResponseError
函數功能。
以上就是關于“go語言規范RESTful API業務錯誤處理的方法是什么”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。