Compare commits

..

No commits in common. "main" and "feature/errv2" have entirely different histories.

25 changed files with 2326 additions and 766 deletions

View File

@ -9,4 +9,4 @@ test: # 進行測試
fmt: # 格式優化 fmt: # 格式優化
$(GOFMT) -w $(GOFILES) $(GOFMT) -w $(GOFILES)
goimports -w ./ goimports -w ./
golangci-lint run

99
errors/code/define.go Normal file
View File

@ -0,0 +1,99 @@
package code
const (
OK uint32 = 0
)
// Scope
const (
Unset uint32 = iota
CloudEPPortalGW
CloudEPMember
CloudEPPermission
)
// Category for general operations: 100 - 4900
const (
_ = iota
CatInput uint32 = iota * 100
CatDB
CatResource
CatGRPC
CatAuth
CatSystem
CatPubSub
)
// CatArk Category for specific app/service: 5000 - 9900
const (
CatArk uint32 = (iota + 50) * 100
)
// Detail - Input 1xx
const (
_ = iota + CatInput
InvalidFormat
NotValidImplementation
InvalidRange
)
// Detail - Database 2xx
const (
_ = iota + CatDB
DBError // general error
DBDataConvert
DBDuplicate
)
// Detail - Resource 3xx
const (
_ = iota + CatResource
ResourceNotFound
InvalidResourceFormat
ResourceAlreadyExist
ResourceInsufficient
InsufficientPermission
InvalidMeasurementID
ResourceExpired
ResourceMigrated
InvalidResourceState
InsufficientQuota
ResourceHasMultiOwner
)
/* Detail - GRPC */
// The GRPC detail code uses Go GRPC's built-in codes.
// Refer to "google.golang.org/grpc/codes" for more detail.
// Detail - Auth 5xx
const (
_ = iota + CatAuth
Unauthorized
AuthExpired
InvalidPosixTime
SigAndPayloadNotMatched
Forbidden
)
// Detail - System 6xx
const (
_ = iota + CatSystem
SystemInternalError
SystemMaintainError
SystemTimeoutError
)
// Detail - PubSub 7xx
const (
_ = iota + CatPubSub
Publish
Consume
MsgSizeTooLarge
)
// Detail - Ark 5xxx
const (
_ = iota + CatArk
ArkInternal
ArkHTTP400
)

13
errors/code/messsage.go Normal file
View File

@ -0,0 +1,13 @@
package code
// CatToStr collects general error messages for each Category
// It is used to send back to API caller
var CatToStr = map[uint32]string{
CatInput: "Invalid Input Data",
CatDB: "Database Error",
CatResource: "Resource Error",
CatGRPC: "Internal Service Communication Error",
CatAuth: "Authentication Error",
CatArk: "Internal Service Communication Error",
CatSystem: "System Error",
}

471
errors/easy_func.go Normal file
View File

@ -0,0 +1,471 @@
package errors
import (
"errors"
"fmt"
"strings"
"code.30cm.net/digimon/library-go/errors/code"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func newErr(scope, detail uint32, msg string) *LibError {
cat := detail / 100 * 100
return &LibError{
category: cat,
code: detail,
scope: scope,
msg: msg,
}
}
func newBuiltinGRPCErr(scope, detail uint32, msg string) *LibError {
return &LibError{
category: code.CatGRPC,
code: detail,
scope: scope,
msg: msg,
}
}
// FromError tries to let error as Err
// it supports to unwrap error that has Err
// return nil if failed to transfer
func FromError(err error) *LibError {
if err == nil {
return nil
}
var e *LibError
if errors.As(err, &e) {
return e
}
return nil
}
// FromCode parses code as following
// Decimal: 120314
// 12 represents Scope
// 03 represents Category
// 14 represents Detail error code
func FromCode(code uint32) *LibError {
scope := code / 10000
detail := code % 10000
return &LibError{
category: detail / 100 * 100,
code: detail,
scope: scope,
msg: "",
}
}
// FromGRPCError transfer error to Err
// useful for gRPC client
func FromGRPCError(err error) *LibError {
s, _ := status.FromError(err)
e := FromCode(uint32(s.Code()))
e.msg = s.Message()
// For GRPC built-in code
if e.Scope() == code.Unset && e.Category() == 0 && e.Code() != code.OK {
e = newBuiltinGRPCErr(Scope, e.Code(), s.Message())
}
return e
}
// Deprecated: check GRPCStatus() in Errs struct
// ToGRPCError returns the status.Status
// Useful to return error in gRPC server
func ToGRPCError(e *LibError) error {
return status.New(codes.Code(e.FullCode()), e.Error()).Err()
}
/*** System ***/
// SystemTimeoutError returns Err
func SystemTimeoutError(s ...string) *LibError {
return newErr(Scope, code.SystemTimeoutError, fmt.Sprintf("system timeout: %s", strings.Join(s, " ")))
}
// SystemTimeoutErrorL logs error message and returns Err
func SystemTimeoutErrorL(l logx.Logger, s ...string) *LibError {
e := SystemTimeoutError(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// SystemInternalError returns Err struct
func SystemInternalError(s ...string) *LibError {
return newErr(Scope, code.SystemInternalError, fmt.Sprintf("internal error: %s", strings.Join(s, " ")))
}
// SystemInternalErrorL logs error message and returns Err
func SystemInternalErrorL(l logx.Logger, s ...string) *LibError {
e := SystemInternalError(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// SystemMaintainErrorL logs error message and returns Err
func SystemMaintainErrorL(l logx.Logger, s ...string) *LibError {
e := SystemMaintainError(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// SystemMaintainError returns Err struct
func SystemMaintainError(s ...string) *LibError {
return newErr(Scope, code.SystemMaintainError, fmt.Sprintf("service under maintenance: %s", strings.Join(s, " ")))
}
/*** CatInput ***/
// InvalidFormat returns Err struct
func InvalidFormat(s ...string) *LibError {
return newErr(Scope, code.InvalidFormat, fmt.Sprintf("invalid format: %s", strings.Join(s, " ")))
}
// InvalidFormatL logs error message and returns Err
func InvalidFormatL(l logx.Logger, s ...string) *LibError {
e := InvalidFormat(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InvalidRange returns Err struct
func InvalidRange(s ...string) *LibError {
return newErr(Scope, code.InvalidRange, fmt.Sprintf("invalid range: %s", strings.Join(s, " ")))
}
// InvalidRangeL logs error message and returns Err
func InvalidRangeL(l logx.Logger, s ...string) *LibError {
e := InvalidRange(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// NotValidImplementation returns Err struct
func NotValidImplementation(s ...string) *LibError {
return newErr(Scope, code.NotValidImplementation, fmt.Sprintf("not valid implementation: %s", strings.Join(s, " ")))
}
// NotValidImplementationL logs error message and returns Err
func NotValidImplementationL(l logx.Logger, s ...string) *LibError {
e := NotValidImplementation(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
/*** CatDB ***/
// DBError returns Err
func DBError(s ...string) *LibError {
return newErr(Scope, code.DBError, fmt.Sprintf("db error: %s", strings.Join(s, " ")))
}
// DBErrorL logs error message and returns Err
func DBErrorL(l logx.Logger, s ...string) *LibError {
e := DBError(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// DBDataConvert returns Err
func DBDataConvert(s ...string) *LibError {
return newErr(Scope, code.DBDataConvert, fmt.Sprintf("data from db convert error: %s", strings.Join(s, " ")))
}
// DBDataConvertL logs error message and returns Err
func DBDataConvertL(l logx.Logger, s ...string) *LibError {
e := DBDataConvert(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// DBDuplicate returns Err
func DBDuplicate(s ...string) *LibError {
return newErr(Scope, code.DBDuplicate, fmt.Sprintf("data Duplicate key error: %s", strings.Join(s, " ")))
}
// DBDuplicateL logs error message and returns Err
func DBDuplicateL(l logx.Logger, s ...string) *LibError {
e := DBDuplicate(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
/*** CatResource ***/
// ResourceNotFound returns Err and logging
func ResourceNotFound(s ...string) *LibError {
return newErr(Scope, code.ResourceNotFound, fmt.Sprintf("resource not found: %s", strings.Join(s, " ")))
}
// ResourceNotFoundL logs error message and returns Err
func ResourceNotFoundL(l logx.Logger, s ...string) *LibError {
e := ResourceNotFound(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InvalidResourceFormat returns Err
func InvalidResourceFormat(s ...string) *LibError {
return newErr(Scope, code.InvalidResourceFormat, fmt.Sprintf("invalid resource format: %s", strings.Join(s, " ")))
}
// InvalidResourceFormatL logs error message and returns Err
func InvalidResourceFormatL(l logx.Logger, s ...string) *LibError {
e := InvalidResourceFormat(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InvalidResourceState returns status not correct.
// for example: company should be destroy, agent should be no-sensor/fail-install ...
func InvalidResourceState(s ...string) *LibError {
return newErr(Scope, code.InvalidResourceState, fmt.Sprintf("invalid resource state: %s", strings.Join(s, " ")))
}
// InvalidResourceStateL logs error message and returns status not correct.
func InvalidResourceStateL(l logx.Logger, s ...string) *LibError {
e := InvalidResourceState(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
func ResourceInsufficient(s ...string) *LibError {
return newErr(Scope, code.ResourceInsufficient,
fmt.Sprintf("insufficient resource: %s", strings.Join(s, " ")))
}
func ResourceInsufficientL(l logx.Logger, s ...string) *LibError {
e := ResourceInsufficient(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InsufficientPermission returns Err
func InsufficientPermission(s ...string) *LibError {
return newErr(Scope, code.InsufficientPermission,
fmt.Sprintf("insufficient permission: %s", strings.Join(s, " ")))
}
// InsufficientPermissionL returns Err and log
func InsufficientPermissionL(l logx.Logger, s ...string) *LibError {
e := InsufficientPermission(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// ResourceAlreadyExist returns Err
func ResourceAlreadyExist(s ...string) *LibError {
return newErr(Scope, code.ResourceAlreadyExist, fmt.Sprintf("resource already exist: %s", strings.Join(s, " ")))
}
// ResourceAlreadyExistL logs error message and returns Err
func ResourceAlreadyExistL(l logx.Logger, s ...string) *LibError {
e := ResourceAlreadyExist(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InvalidMeasurementID returns Err
func InvalidMeasurementID(s ...string) *LibError {
return newErr(Scope, code.InvalidMeasurementID, fmt.Sprintf("missing measurement id: %s", strings.Join(s, " ")))
}
// InvalidMeasurementIDL logs error message and returns Err
func InvalidMeasurementIDL(l logx.Logger, s ...string) *LibError {
e := InvalidMeasurementID(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// ResourceExpired returns Err
func ResourceExpired(s ...string) *LibError {
return newErr(Scope, code.ResourceExpired, fmt.Sprintf("resource expired: %s", strings.Join(s, " ")))
}
// ResourceExpiredL logs error message and returns Err
func ResourceExpiredL(l logx.Logger, s ...string) *LibError {
e := ResourceExpired(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// ResourceMigrated returns Err
func ResourceMigrated(s ...string) *LibError {
return newErr(Scope, code.ResourceMigrated, fmt.Sprintf("resource migrated: %s", strings.Join(s, " ")))
}
// ResourceMigratedL logs error message and returns Err
func ResourceMigratedL(l logx.Logger, s ...string) *LibError {
e := ResourceMigrated(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InsufficientQuota returns Err
func InsufficientQuota(s ...string) *LibError {
return newErr(Scope, code.InsufficientQuota, fmt.Sprintf("insufficient quota: %s", strings.Join(s, " ")))
}
// InsufficientQuotaL logs error message and returns Err
func InsufficientQuotaL(l logx.Logger, s ...string) *LibError {
e := InsufficientQuota(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
/*** CatAuth ***/
// Unauthorized returns Err
func Unauthorized(s ...string) *LibError {
return newErr(Scope, code.Unauthorized, fmt.Sprintf("unauthorized: %s", strings.Join(s, " ")))
}
// UnauthorizedL logs error message and returns Err
func UnauthorizedL(l logx.Logger, s ...string) *LibError {
e := Unauthorized(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// AuthExpired returns Err
func AuthExpired(s ...string) *LibError {
return newErr(Scope, code.AuthExpired, fmt.Sprintf("expired: %s", strings.Join(s, " ")))
}
// AuthExpiredL logs error message and returns Err
func AuthExpiredL(l logx.Logger, s ...string) *LibError {
e := AuthExpired(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// InvalidPosixTime returns Err
func InvalidPosixTime(s ...string) *LibError {
return newErr(Scope, code.InvalidPosixTime, fmt.Sprintf("invalid posix time: %s", strings.Join(s, " ")))
}
// InvalidPosixTimeL logs error message and returns Err
func InvalidPosixTimeL(l logx.Logger, s ...string) *LibError {
e := InvalidPosixTime(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// SigAndPayloadNotMatched returns Err
func SigAndPayloadNotMatched(s ...string) *LibError {
return newErr(Scope, code.SigAndPayloadNotMatched, fmt.Sprintf("signature and the payload are not match: %s", strings.Join(s, " ")))
}
// SigAndPayloadNotMatchedL logs error message and returns Err
func SigAndPayloadNotMatchedL(l logx.Logger, s ...string) *LibError {
e := SigAndPayloadNotMatched(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// Forbidden returns Err
func Forbidden(s ...string) *LibError {
return newErr(Scope, code.Forbidden, fmt.Sprintf("forbidden: %s", strings.Join(s, " ")))
}
// ForbiddenL logs error message and returns Err
func ForbiddenL(l logx.Logger, s ...string) *LibError {
e := Forbidden(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// IsAuthUnauthorizedError check the err is unauthorized error
func IsAuthUnauthorizedError(err *LibError) bool {
switch err.Code() {
case code.Unauthorized, code.AuthExpired, code.InvalidPosixTime,
code.SigAndPayloadNotMatched, code.Forbidden,
code.InvalidFormat, code.ResourceNotFound:
return true
default:
return false
}
}
/*** CatXBC ***/
// ArkInternal returns Err
func ArkInternal(s ...string) *LibError {
return newErr(Scope, code.ArkInternal, fmt.Sprintf("ark internal error: %s", strings.Join(s, " ")))
}
// ArkInternalL logs error message and returns Err
func ArkInternalL(l logx.Logger, s ...string) *LibError {
e := ArkInternal(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
/*** CatPubSub ***/
// Publish returns Err
func Publish(s ...string) *LibError {
return newErr(Scope, code.Publish, fmt.Sprintf("publish: %s", strings.Join(s, " ")))
}
// PublishL logs error message and returns Err
func PublishL(l logx.Logger, s ...string) *LibError {
e := Publish(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}
// Consume returns Err
func Consume(s ...string) *LibError {
return newErr(Scope, code.Consume, fmt.Sprintf("consume: %s", strings.Join(s, " ")))
}
// MsgSizeTooLarge returns Err
func MsgSizeTooLarge(s ...string) *LibError {
return newErr(Scope, code.MsgSizeTooLarge, fmt.Sprintf("kafka error: %s", strings.Join(s, " ")))
}
// MsgSizeTooLargeL logs error message and returns Err
func MsgSizeTooLargeL(l logx.Logger, s ...string) *LibError {
e := MsgSizeTooLarge(s...)
l.WithCallerSkip(1).Error(e.Error())
return e
}

1069
errors/easy_func_test.go Normal file

File diff suppressed because it is too large Load Diff

225
errors/errors.go Normal file
View File

@ -0,0 +1,225 @@
package errors
import (
"errors"
"fmt"
"net/http"
code2 "code.30cm.net/digimon/library-go/errors/code"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Scope 全域變數應由服務或模組設置
var Scope = code2.Unset
// LibError 6 碼,服務 2, 大類 2, 詳細錯誤 2
type LibError struct {
category uint32
code uint32
scope uint32
msg string
internalErr error
}
// Error 是錯誤的介面
// 私有屬性 "msg" 的 getter 函數
func (e *LibError) Error() string {
if e == nil {
return ""
}
// 如果 internal err 存在,連接錯誤字串
var internalErrStr string
if e.internalErr != nil {
internalErrStr = e.internalErr.Error()
}
if e.msg != "" {
if internalErrStr != "" {
return fmt.Sprintf("%s: %s", e.msg, internalErrStr)
}
return e.msg
}
generalErrStr := e.GeneralError()
if internalErrStr != "" {
return fmt.Sprintf("%s: %s", generalErrStr, internalErrStr)
}
return generalErrStr
}
// Category 私有屬性 "category" 的 getter 函數
func (e *LibError) Category() uint32 {
if e == nil {
return 0
}
return e.category
}
// Scope 私有屬性 "scope" 的 getter 函數
func (e *LibError) Scope() uint32 {
if e == nil {
return code2.Unset
}
return e.scope
}
// CodeStr 返回帶有零填充的錯誤代碼字串
func (e *LibError) CodeStr() string {
if e == nil {
return "00000"
}
if e.Category() == code2.CatGRPC {
return fmt.Sprintf("%02d%04d", e.Scope(), e.Category()+e.Code())
}
return fmt.Sprintf("%02d%04d", e.Scope(), e.Code())
}
// Code 私有屬性 "code" 的 getter 函數
func (e *LibError) Code() uint32 {
if e == nil {
return code2.OK
}
return e.code
}
func (e *LibError) FullCode() uint32 {
if e == nil {
return 0
}
if e.Category() == code2.CatGRPC {
return e.Scope()*10000 + e.Category() + e.Code()
}
return e.Scope()*10000 + e.Code()
}
// HTTPStatus 返回對應的 HTTP 狀態碼
func (e *LibError) HTTPStatus() int {
if e == nil || e.Code() == code2.OK {
return http.StatusOK
}
// 根據 code 判斷狀態碼
switch e.Code() {
case code2.ResourceInsufficient:
// 400
return http.StatusBadRequest
case code2.Unauthorized, code2.InsufficientPermission:
// 401
return http.StatusUnauthorized
case code2.InsufficientQuota:
// 402
return http.StatusPaymentRequired
case code2.InvalidPosixTime, code2.Forbidden:
// 403
return http.StatusForbidden
case code2.ResourceNotFound:
// 404
return http.StatusNotFound
case code2.ResourceAlreadyExist, code2.InvalidResourceState:
// 409
return http.StatusConflict
case code2.NotValidImplementation:
// 501
return http.StatusNotImplemented
default:
}
// 根據 category 判斷狀態碼
switch e.Category() {
case code2.CatInput:
return http.StatusBadRequest
default:
// 如果沒有符合的條件,返回狀態碼 500
return http.StatusInternalServerError
}
}
// GeneralError 轉換 category 級別錯誤訊息
// 這是給客戶或 API 調用者的一般錯誤訊息
func (e *LibError) GeneralError() string {
if e == nil {
return ""
}
errStr, ok := code2.CatToStr[e.Category()]
if !ok {
return ""
}
return errStr
}
// Is 在執行 errors.Is() 時調用。
// 除非你非常確定你在做什麼,否則不要直接使用這個函數。
// 請使用 errors.Is 代替。
// 此函數比較兩個錯誤變量是否都是 *Err並且具有相同的 code不檢查包裹的內部錯誤
func (e *LibError) Is(f error) bool {
var err *LibError
ok := errors.As(f, &err)
if !ok {
return false
}
return e.Code() == err.Code()
}
// Unwrap 返回底層錯誤
// 解除包裹錯誤的結果本身可能具有 Unwrap 方法;
// 我們稱通過反覆解除包裹產生的錯誤序列為錯誤鏈。
func (e *LibError) Unwrap() error {
if e == nil {
return nil
}
return e.internalErr
}
// Wrap 將內部錯誤設置到 Err 結構
func (e *LibError) Wrap(internalErr error) *LibError {
if e != nil {
e.internalErr = internalErr
}
return e
}
func (e *LibError) GRPCStatus() *status.Status {
if e == nil {
return status.New(codes.OK, "")
}
return status.New(codes.Code(e.FullCode()), e.Error())
}
// 工廠函數
// NewErr 創建新的 Err
func NewErr(scope, category, detail uint32, msg string) *LibError {
return &LibError{
category: category,
code: detail,
scope: scope,
msg: msg,
}
}
// NewGRPCErr 創建新的 gRPC Err
func NewGRPCErr(scope, detail uint32, msg string) *LibError {
return &LibError{
category: code2.CatGRPC,
code: detail,
scope: scope,
msg: msg,
}
}

298
errors/errors_test.go Normal file
View File

@ -0,0 +1,298 @@
package errors
import (
"errors"
"fmt"
"net/http"
"testing"
code2 "code.30cm.net/digimon/library-go/errors/code"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestCode_GivenNilReceiver_CodeReturnOK_CodeStrReturns00000(t *testing.T) {
// setup
var e *LibError = nil
// act & assert
assert.Equal(t, code2.OK, e.Code())
assert.Equal(t, "00000", e.CodeStr())
assert.Equal(t, "", e.Error())
}
func TestCode_GivenScope99DetailCode6687_ShouldReturn996687(t *testing.T) {
// setup
e := LibError{scope: 99, code: 6687}
// act & assert
assert.Equal(t, uint32(6687), e.Code())
assert.Equal(t, "996687", e.CodeStr())
}
func TestCode_GivenScope0DetailCode87_ShouldReturn87(t *testing.T) {
// setup
e := LibError{scope: 0, code: 87}
// act & assert
assert.Equal(t, uint32(87), e.Code())
assert.Equal(t, "00087", e.CodeStr())
}
func TestFromCode_Given870005_ShouldHasScope87_Cat0_Detail5(t *testing.T) {
// setup
e := FromCode(870005)
// assert
assert.Equal(t, uint32(87), e.Scope())
assert.Equal(t, uint32(0), e.Category())
assert.Equal(t, uint32(5), e.Code())
assert.Equal(t, "", e.Error())
}
func TestFromCode_Given0_ShouldHasScope0_Cat0_Detail0(t *testing.T) {
// setup
e := FromCode(0)
// assert
assert.Equal(t, uint32(0), e.Scope())
assert.Equal(t, uint32(0), e.Category())
assert.Equal(t, uint32(0), e.Code())
assert.Equal(t, "", e.Error())
}
func TestFromCode_Given9105_ShouldHasScope0_Cat9100_Detail9105(t *testing.T) {
// setup
e := FromCode(9105)
// assert
assert.Equal(t, uint32(0), e.Scope())
assert.Equal(t, uint32(9100), e.Category())
assert.Equal(t, uint32(9105), e.Code())
assert.Equal(t, "", e.Error())
}
func TestErr_ShouldImplementErrorFunction(t *testing.T) {
// setup a func return error
f := func() error { return InvalidFormat("fake field") }
// act
err := f()
// assert
assert.NotNil(t, err)
assert.Contains(t, fmt.Sprint(err), "fake field") // can be printed
}
func TestGeneralError_GivenNilErr_ShouldReturnEmptyString(t *testing.T) {
// setup
var e *LibError = nil
// act & assert
assert.Equal(t, "", e.GeneralError())
}
func TestGeneralError_GivenNotExistCat_ShouldReturnEmptyString(t *testing.T) {
// setup
e := LibError{category: 123456}
// act & assert
assert.Equal(t, "", e.GeneralError())
}
func TestGeneralError_GivenCatDB_ShouldReturnDBError(t *testing.T) {
// setup
e := LibError{category: code2.CatDB}
catErrStr := code2.CatToStr[code2.CatDB]
// act & assert
assert.Equal(t, catErrStr, e.GeneralError())
}
func TestError_GivenEmptyMsg_ShouldReturnCatGeneralErrorMessage(t *testing.T) {
// setup
e := LibError{category: code2.CatDB, msg: ""}
// act
errMsg := e.Error()
// assert
assert.Equal(t, code2.CatToStr[code2.CatDB], errMsg)
}
func TestError_GivenMsg_ShouldReturnGiveMsg(t *testing.T) {
// setup
e := LibError{msg: "FAKE"}
// act
errMsg := e.Error()
// assert
assert.Equal(t, "FAKE", errMsg)
}
func TestIs_GivenNilErr_ShouldReturnFalse(t *testing.T) {
var nilErrs *LibError
// act
result := errors.Is(nilErrs, DBError())
result2 := errors.Is(DBError(), nilErrs)
// assert
assert.False(t, result)
assert.False(t, result2)
}
func TestIs_GivenNil_ShouldReturnFalse(t *testing.T) {
// act
result := errors.Is(nil, DBError())
result2 := errors.Is(DBError(), nil)
// assert
assert.False(t, result)
assert.False(t, result2)
}
func TestIs_GivenNilReceiver_ShouldReturnCorrectResult(t *testing.T) {
var nilErr *LibError = nil
// test 1: nilErr != DBError
var dbErr error = DBError("fake db error")
assert.False(t, nilErr.Is(dbErr))
// test 2: nilErr != nil error
var nilError error
assert.False(t, nilErr.Is(nilError))
// test 3: nilErr == another nilErr
var nilErr2 *LibError = nil
assert.True(t, nilErr.Is(nilErr2))
}
func TestIs_GivenDBError_ShouldReturnTrue(t *testing.T) {
// setup
dbErr := DBError("fake db error")
// act
result := errors.Is(dbErr, DBError("not care"))
result2 := errors.Is(DBError(), dbErr)
// assert
assert.True(t, result)
assert.True(t, result2)
}
func TestIs_GivenDBErrorAssignToErrorType_ShouldReturnTrue(t *testing.T) {
// setup
var dbErr error = DBError("fake db error")
// act
result := errors.Is(dbErr, DBError("not care"))
result2 := errors.Is(DBError(), dbErr)
// assert
assert.True(t, result)
assert.True(t, result2)
}
func TestWrap_GivenNilErr_ShouldNoPanic(t *testing.T) {
// act & assert
assert.NotPanics(t, func() {
var e *LibError = nil
_ = e.Wrap(fmt.Errorf("test"))
})
}
func TestWrap_GivenErrorToWrap_ShouldReturnErrorWithWrappedError(t *testing.T) {
// act & assert
wrappedErr := fmt.Errorf("test")
wrappingErr := SystemInternalError("WrappingError").Wrap(wrappedErr)
unWrappedErr := wrappingErr.Unwrap()
assert.Equal(t, wrappedErr, unWrappedErr)
}
func TestUnwrap_GivenNilErr_ShouldReturnNil(t *testing.T) {
var e *LibError = nil
internalErr := e.Unwrap()
assert.Nil(t, internalErr)
}
func TestErrorsIs_GivenNilErr_ShouldReturnFalse(t *testing.T) {
var e *LibError = nil
assert.False(t, errors.Is(e, fmt.Errorf("test")))
}
func TestErrorsAs_GivenNilErr_ShouldReturnFalse(t *testing.T) {
var internalErr *testErr
var e *LibError = nil
assert.False(t, errors.As(e, &internalErr))
}
func TestGRPCStatus(t *testing.T) {
// setup table driven tests
tests := []struct {
name string
given *LibError
expect *status.Status
expectConvert error
}{
{
"nil errs.Err",
nil,
status.New(codes.OK, ""),
nil,
},
{
"InvalidFormat Err",
InvalidFormat("fake"),
status.New(codes.Code(101), "invalid format: fake"),
status.New(codes.Code(101), "invalid format: fake").Err(),
},
}
// act & assert
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := test.given.GRPCStatus()
assert.Equal(t, test.expect.Code(), s.Code())
assert.Equal(t, test.expect.Message(), s.Message())
assert.Equal(t, test.expectConvert, status.Convert(test.given).Err())
})
}
}
func TestErr_HTTPStatus(t *testing.T) {
tests := []struct {
name string
err *LibError
want int
}{
{name: "nil error", err: nil, want: http.StatusOK},
{name: "invalid measurement id", err: &LibError{category: code2.CatResource, code: code2.InvalidMeasurementID}, want: http.StatusInternalServerError},
{name: "resource already exists", err: &LibError{category: code2.CatResource, code: code2.ResourceAlreadyExist}, want: http.StatusConflict},
{name: "invalid resource state", err: &LibError{category: code2.CatResource, code: code2.InvalidResourceState}, want: http.StatusConflict},
{name: "invalid posix time", err: &LibError{category: code2.CatAuth, code: code2.InvalidPosixTime}, want: http.StatusForbidden},
{name: "unauthorized", err: &LibError{category: code2.CatAuth, code: code2.Unauthorized}, want: http.StatusUnauthorized},
{name: "db error", err: &LibError{category: code2.CatDB, code: code2.DBError}, want: http.StatusInternalServerError},
{name: "insufficient permission", err: &LibError{category: code2.CatResource, code: code2.InsufficientPermission}, want: http.StatusUnauthorized},
{name: "resource insufficient", err: &LibError{category: code2.CatResource, code: code2.ResourceInsufficient}, want: http.StatusBadRequest},
{name: "invalid format", err: &LibError{category: code2.CatInput, code: code2.InvalidFormat}, want: http.StatusBadRequest},
{name: "resource not found", err: &LibError{code: code2.ResourceNotFound}, want: http.StatusNotFound},
{name: "ok", err: &LibError{code: code2.OK}, want: http.StatusOK},
{name: "not valid implementation", err: &LibError{category: code2.CatInput, code: code2.NotValidImplementation}, want: http.StatusNotImplemented},
{name: "forbidden", err: &LibError{category: code2.CatAuth, code: code2.Forbidden}, want: http.StatusForbidden},
{name: "insufficient quota", err: &LibError{category: code2.CatResource, code: code2.InsufficientQuota}, want: http.StatusPaymentRequired},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// act
got := tt.err.HTTPStatus()
// assert
assert.Equal(t, tt.want, got)
})
}
}

21
errors/go.mod Normal file
View File

@ -0,0 +1,21 @@
module code.30cm.net/digimon/library-go/errors
go 1.22
require (
github.com/zeromicro/go-zero v1.7.0
google.golang.org/grpc v1.65.0
)
require (
github.com/fatih/color v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
golang.org/x/sys v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)

125
errors/readme.md Normal file
View File

@ -0,0 +1,125 @@
## Purpose of errs package
1. compatible with `error` interface
2. encapsulate error message with functions in `easy_function.go`
3. easy for gRPC client/server
4. support err's chain by [Working with Errors in Go 1.13](https://blog.golang.org/go1.13-errors)
## Example - Normal function
Using builtin functions `InvalidInput` to generate `Err struct`
**please add your own functions if not exist**
```go
package main
import "adc.github.trendmicro.com/commercial-mgcp/library-go/pkg/errs"
func handleParam(s string) error {
// check user_id format
if ok := userIDFormat(s); !ok {
return errs.InvalidFormat("param user_id")
}
return nil
}
```
## Example - gRPC Server
`GetAgent` is a method of gRPC server, it wraps `Err` struct to `status.Status` struct
```go
func (as *agentService) GetAgent(ctx context.Context, req *cloudep.GetAgentRequest) (*cloudep.GetAgentResponse, error) {
l := log.WithFields(logger.Fields{"tenant_id": req.TenantId, "agent_id": req.AgentId, "method": "GetAgent"})
tenantID, err := primitive.ObjectIDFromHex(req.TenantId)
if err != nil {
// err maybe errs.Err or general error
// it's safe to use Convert() here
return nil, status.Convert(err).Err()
}
...
}
```
## Example - gRPC Client
Calling `GetAgent` and retry when Category is "DB"
```go
client := cloudep.NewAgentServiceClient(conn)
req := cloudep.GetAgentRequest{
TenantId: "not-a-valid-object-id",
AgentId: "5eb4fa99006d53c0cb6f9cfe",
}
// Retry if DB error
for retry := 3; retry > 0 ; retry-- {
resp, err := client.GetAgent(context.Background(), &req)
if err != nil {
e := errs.FromGRPCError(err)
if e.Category() == code.CatGRPC {
if e.Code() == uint32(codes.Unavailable) {
log.warn("GRPC service unavailable. Retrying...")
continue
}
log.errorf("GRPC built-in error: %v", e)
}
if e.Category() == code.CatDB {
log.warn("retry...")
continue
}
}
break
}
```
## Example - REST server
1. handling gRPC client error
2. transfer to HTTP code
3. transfer to Error body
```go
func Handler(c *gin.Context) {
// handle error from gRPC client
resp, err := client.GetAgent(context.Background(), &req)
if err != nil {
// to Err
e := errs.FromGRPCError(err)
// get HTTP code & response struct
// 2nd parameter true means return general error message to user
c.JSON(e.HTTPStatus(), general.NewError(e, true))
}
}
```
## Example - Error Chain
1. set internal error by func `Wrap`
2. check Err has any error in err's chain matches the target by `errors.Is`
3. finds the first error in err's chain that matches target by `errors.As`
```go
// define a specific err type
type testErr struct {
code int
}
func (e *testErr) Error() string {
return strconv.Itoa(e.code)
}
func main() {
layer1Err := &testErr{code: 123}
// error chain: InvalidFormat -> layer 1 err
layer2Err := InvalidFormat("field A", "")
layer2Err.Wrap(layer1Err) //set internal error
// errors.Is should report true
hasLayer1Err := errors.Is(layer2Err, layer1Err)
// errors.As should return internal error
var internalErr *testErr
ok := errors.As(layer2Err, &internalErr)
}
```

View File

@ -70,6 +70,5 @@ const (
const ( const (
_ = iota + CatService _ = iota + CatService
ArkInternal // Ark 內部錯誤 ArkInternal // Ark 內部錯誤
ThirdParty ArkHTTP400 // Ark HTTP 400 錯誤
ArkHTTP400 // Ark HTTP 400 錯誤
) )

View File

@ -7,8 +7,4 @@ const (
CloudEPMember CloudEPMember
CloudEPPermission CloudEPPermission
CloudEPNotification CloudEPNotification
CloudEPTweeting
CloudEPOrder
CloudEPFileStorage
CloudEPProduct
) )

View File

@ -1,87 +0,0 @@
package errs
import (
"code.30cm.net/digimon/library-go/errs/code"
"fmt"
"github.com/zeromicro/go-zero/core/logx"
"strings"
)
type ErrorCode uint32
func (e ErrorCode) ToUint32() uint32 {
return uint32(e)
}
func ThirdPartyError(scope uint32, ec ErrorCode, s ...string) *LibError {
return NewError(scope, code.ThirdParty, ec.ToUint32(), fmt.Sprintf("thirty error: %s", strings.Join(s, " ")))
}
func ThirdPartyErrorL(scope uint32, ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := ThirdPartyError(scope, ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}
func DatabaseErrorWithScope(scope uint32, ec ErrorCode, s ...string) *LibError {
return NewError(scope, code.DBError, ec.ToUint32(), strings.Join(s, " "))
}
func DatabaseErrorWithScopeL(scope uint32,
ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := DatabaseErrorWithScope(scope, ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}
func ResourceNotFoundWithScope(scope uint32, ec ErrorCode, s ...string) *LibError {
return NewError(scope, code.ResourceNotFound, ec.ToUint32(), fmt.Sprintf("resource not found: %s", strings.Join(s, " ")))
}
func ResourceNotFoundWithScopeL(scope uint32, ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := ResourceNotFoundWithScope(scope, ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}
func InvalidRangeWithScope(scope uint32, ec ErrorCode, s ...string) *LibError {
return NewError(scope, code.CatInput, ec.ToUint32(), fmt.Sprintf("invalid range: %s", strings.Join(s, " ")))
}
func InvalidRangeWithScopeL(scope uint32, ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := InvalidRangeWithScope(scope, ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}
func InvalidFormatWithScope(scope uint32, s ...string) *LibError {
return NewError(scope, code.CatInput, code.InvalidFormat, fmt.Sprintf("invalid range: %s", strings.Join(s, " ")))
}
func InvalidFormatWithScopeL(scope uint32,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := InvalidFormatWithScope(scope, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}
func ForbiddenWithScope(scope uint32, ec ErrorCode, s ...string) *LibError {
return NewError(scope, code.Forbidden, ec.ToUint32(), fmt.Sprintf("forbidden: %s", strings.Join(s, " ")))
}
func ForbiddenWithScopeL(scope uint32, ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *LibError {
e := ForbiddenWithScope(scope, ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}

3
go.mod
View File

@ -1,3 +0,0 @@
module code.30cm.net/digimon/library-go
go 1.22.3

View File

@ -1,11 +1,9 @@
go 1.22.3 go 1.22.3
use ( use (
./errors
./validator ./validator
./worker_pool ./worker_pool
./jwt ./jwt
./errs ./errs
.
./utils/invited_code
./utils/bitmap
) )

View File

@ -46,6 +46,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
@ -168,12 +170,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=

View File

@ -1,172 +0,0 @@
package usecase
// Bitmap 基礎結構
// Bitmap 是一個位圖結構,使用 byte slice 來表示大量的位bit
// 每個 byte 由 8 個位組成因此可以高效地管理大量的開關狀態true/false
// Bitmap 的優點在於它能節省空間,尤其是在需要大量布爾值的場合。
// 缺點是,如果需要動態擴充 Bitmap 的大小,會導致效率下降,因為需要重新分配和移動內存。
// 因此,最好在初始化時就規劃好所需的位數大小,避免在之後頻繁擴充。
type Bitmap []byte
// MakeBitmapWithBitSize 通過指定的 bit 數創建一個新的 Bitmap。
// 參數 nBits 表示所需的位bit數。
// 如果指定的位數少於 64則默認將 Bitmap 初始化為 64 位(這是最低的限制)。
// 此外位數nBits會被自動調整為 8 的倍數,以適配 byte 的長度(每 8 位為一個 byte
// 返回值是一個 Bitmapbyte slice其大小根據位數確定。
func MakeBitmapWithBitSize(nBits int) Bitmap {
// 如果指定的位數少於 64則設置為 64 位8 個 byte
if nBits < 64 {
nBits = 64
}
// 計算需要的 byte 數,確保每 8 位為一個 byte並調整 nBits 以達到 8 的倍數
return MustBitMap((nBits + 7) / 8)
}
// MustBitMap 根據指定的 byte 數創建一個 Bitmapbyte slice
// 參數 nBytes 表示所需的 byte 數。
// 返回值是一個長度為 nBytes 的 Bitmap。
func MustBitMap(nBytes int) Bitmap {
// 使用 make 函數創建一個 byte slice大小為 nBytes。
return make([]byte, nBytes)
}
// SetTrue 設置指定位置的 bit 為 true1
// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。
// 這個操作會找到該 bit 所在的 byte然後通過位運算將該位置的 bit 設置為 1。
func (b Bitmap) SetTrue(bitPos uint32) {
// |= 是一種位運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位或運算bitwise OR並將結果賦值
b[bitPos/8] |= 1 << (bitPos % 8)
}
// SetFalse 設置指定位置的 bit 為 false0
// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。
// 這個操作會找到該 bit 所在的 byte然後通過位運算將該位置的 bit 設置為 0。
func (b Bitmap) SetFalse(bitPos uint32) {
// 取出對應 byte使用位與和取反運算將對應的 bit 設置為 0
// 假設我們有以下情況:
// • b[1](即 b[bitPos/8])是 10101111十進制 175
// • bitPos = 10也就是我們想清除第 10 位的值。
//
// 操作步驟:
//
// 1. bitPos/8 = 1所以我們要修改 b[1] 這個 byte。
// 2. bitPos % 8 = 2表示我們要清除的位是這個 byte 中的第 3 位(從右數起第 3 位)。
// 3. 1 << (bitPos % 8) = 1 << 2 = 00000100生成位掩碼 00000100。
// 4. 取反:^(1 << 2) = ^00000100 = 11111011這樣的掩碼表示除了第 3 位,其他位都是 1。
// 5. 位與運算10101111 & 11111011 = 10101011結果將第 3 位清除其餘位保持不變。即b[1] 變成了 10101011十進制 171
// &= 是一種 位與運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位與運算bitwise AND然後將結果賦值給左邊的變數。
b[bitPos/8] &= ^(1 << (bitPos % 8))
}
// IsTrue 檢查指定位置的 bit 是否為 true1
// 參數 bitPos 是要檢查的位的位置(以 0 為基準的位索引)。
// 如果該 bit 是 1則返回 true否則返回 false。
func (b Bitmap) IsTrue(bitPos uint32) bool {
/*
這一行程式碼 b[bitPos/8]&(1<<(bitPos%8)) != 0 是用來檢查 指定位bit 是否為 true1
它的核心是位運算讓我們逐步拆解並解釋這一行程式碼
1. 背景知識
位運算 是在二進制層面操作數字每個 byte 8 個位bit所以位圖結構是以 byte 來表示位的集合
b 是一個 Bitmap 結構也就是 []byte byte 的切片
bitPos 是一個 uint32 類型的變數表示我們想要檢查的位bit在整個位圖中的索引位置
3. 完整流程舉例
假設
b = []byte{0b10101010, 0b01010101} 即二進制表示的兩個 byte分別是 10101010 01010101
bitPos = 10我們要檢查第 10 位是否為 1
操作順序
1. 計算 bitPos/8 = 10/8 = 1所以我們要檢查的是第二個 byte0b01010101
2. 計算 bitPos%8 = 10%8 = 2所以我們要檢查的是該 byte 中的第 3 從右數起第 3
3. 位移1 << 2 = 00000100
4. 位與0b01010101 & 0b00000100 = 0b00000100因為該 byte 的第 3 位是 1結果不等於 0
5. 判斷結果結果不等於 0因此第 10 位是 1true
4. 總結
b[bitPos/8]&(1<<(bitPos%8)) != 0 是一個經典的位操作用來檢查位圖中某一個位是否為 1
bitPos/8 找到對應的 bytebitPos % 8 找到該位在這個 byte 中的具體位置
最後的位與運算和比較用來確定該位的狀態是 true1還是 false0
*/
return b[bitPos/8]&(1<<(bitPos%8)) != 0
}
// Reset 重置 Bitmap使所有的 bit 都設置為 false0
// 這個操作會將整個 Bitmap 的所有 byte 都設置為 0從而達到重置的效果。
func (b Bitmap) Reset() {
// 迭代 Bitmap 中的每個 byte並將其設置為 0
for i := range b {
b[i] = 0
}
}
// ByteSize 返回 Bitmap 的 byte 長度。
// 這個函數返回 Bitmap 目前占用的 byte 數量,該值等於 Bitmap 的長度slice 的長度)。
func (b Bitmap) ByteSize() int {
return len(b)
}
// BitSize 返回 Bitmap 的位bit長度。
// 這個函數通過將 byte 長度乘以 8 來計算 Bitmap 中的總位數。
func (b Bitmap) BitSize() int {
// 每個 byte 包含 8 個 bit因此將 byte 長度乘以 8
return len(b) * 8
}
// Resize 重新調整 Bitmap 的大小,將其擴展為指定的 bit 大小。
// 原來的數據會被保留,新增加的部分位將初始化為 0。
// 參數 newBitSize 是 Bitmap 需要擴展到的位大小。
func (b Bitmap) Resize(newBitSize int) Bitmap {
newByteSize := (newBitSize + 7) / 8
if newByteSize <= len(b) {
// 如果新大小比原大小小或相等,不做任何操作,直接返回原 Bitmap
return b
}
// 創建新的 Bitmap
newBitmap := make([]byte, newByteSize)
// 將原 Bitmap 複製到新 Bitmap 中
copy(newBitmap, b)
// 返回新的 Bitmap
return newBitmap
}
// UpdateFrom 會將傳入的 Bitmap 的值更新到當前 Bitmap 中。
// 如果傳入的 Bitmap 大於當前 Bitmap則當前 Bitmap 會自動擴展並保存新 Bitmap 的數據。
// 如果傳入的 Bitmap 小於當前 Bitmap僅更新前面部分的數據。
func (b Bitmap) UpdateFrom(newBitmap Bitmap) Bitmap {
// 如果 newBitmap 比當前 Bitmap 長,則擴展當前 Bitmap 的大小
if len(newBitmap) > len(b) {
// 創建一個擴展過的 Bitmap
expandedBitmap := make([]byte, len(newBitmap))
copy(expandedBitmap, b) // 將當前 Bitmap 的數據複製到擴展的 Bitmap 中
b = expandedBitmap // 更新當前 Bitmap 的引用
}
return b
}
// SetAllTrue 將 Bitmap 中的所有位設置為 true1
func (b Bitmap) SetAllTrue() Bitmap {
// 將 Bitmap 中的每個 byte 設置為 0xFF表示所有 bit 都為 1
for i := range b {
b[i] = 0xFF
}
return b
}
// SetAllFalse 將 Bitmap 中的所有位設置為 false0
func (b Bitmap) SetAllFalse() Bitmap {
// 將 Bitmap 中的每個 byte 設置為 0表示所有 bit 都為 0
for i := range b {
b[i] = 0
}
return b
}

View File

@ -1,69 +0,0 @@
package usecase
import "testing"
// 基準測試 SetTrue 函數,測試在不同大小的 Bitmap 上設置位元為 true 的效能
func BenchmarkBitmapSetTrue(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
bitmap.SetTrue(uint32(i % 1000000)) // 設置位元為 true
}
}
// 基準測試 SetFalse 函數,測試在不同大小的 Bitmap 上清除位元為 false 的效能
func BenchmarkBitmapSetFalse(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
bitmap.SetFalse(uint32(i % 1000000)) // 清除位元為 false
}
}
// 基準測試 IsTrue 函數,測試在不同大小的 Bitmap 上檢查位元狀態的效能
func BenchmarkBitmapIsTrue(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
_ = bitmap.IsTrue(uint32(i % 1000000)) // 檢查位元是否為 true
}
}
// 基準測試 Reset 函數,測試重置不同大小的 Bitmap 的效能
func BenchmarkBitmapReset(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
bitmap.Reset() // 重置 Bitmap
}
}
// 基準測試 BitSize 函數,測試返回位圖的 bit 長度的效能
func BenchmarkBitmapBitSize(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
_ = bitmap.BitSize() // 測試返回位圖的 bit 長度
}
}
// 基準測試 ByteSize 函數,測試返回位圖的 byte 長度的效能
func BenchmarkBitmapByteSize(b *testing.B) {
// 以 10^6 位元作為基準測試的 Bitmap 大小
bitmap := MakeBitmapWithBitSize(1000000)
b.ResetTimer() // 重設計時器,排除初始化的時間
for i := 0; i < b.N; i++ {
_ = bitmap.ByteSize() // 測試返回位圖的 byte 長度
}
}

View File

@ -1,157 +0,0 @@
package usecase
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBitmap_SetTrueAndIsTrue(t *testing.T) {
tests := []struct {
name string
bitPos uint32
expected bool
}{
{"Set bit 0 to true", 0, true},
{"Set bit 1 to true", 1, true},
{"Set bit 63 to true", 63, true},
{"Set bit 64 to true", 64, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
bitmap.SetTrue(tt.bitPos)
result := bitmap.IsTrue(tt.bitPos)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBitmap_SetFalse(t *testing.T) {
tests := []struct {
name string
bitPos uint32
expected bool
}{
{"Set bit 0 to false", 0, false},
{"Set bit 1 to false", 1, false},
{"Set bit 63 to false", 63, false},
{"Set bit 64 to false", 64, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
bitmap.SetTrue(tt.bitPos) // 先設置該 bit 為 true
bitmap.SetFalse(tt.bitPos) // 然後設置該 bit 為 false
result := bitmap.IsTrue(tt.bitPos)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBitmap_Reset(t *testing.T) {
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
bitmap.SetTrue(0)
bitmap.SetTrue(64)
// 確認 bit 0 和 bit 64 是 true
assert.True(t, bitmap.IsTrue(0))
assert.True(t, bitmap.IsTrue(64))
bitmap.Reset() // 重置位圖
// 確認所有的位都已經重置為 false
assert.False(t, bitmap.IsTrue(0))
assert.False(t, bitmap.IsTrue(64))
}
func TestBitmap_ByteSize(t *testing.T) {
bitmap := MakeBitmapWithBitSize(64) // 初始化一個 64 位的 Bitmap
assert.Equal(t, 8, bitmap.ByteSize()) // 64 位應該佔用 8 個 byte
}
func TestBitmap_BitSize(t *testing.T) {
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
assert.Equal(t, 128, bitmap.BitSize()) // 128 位應該有 128 個 bit
}
func TestBitmap_Resize(t *testing.T) {
tests := []struct {
name string
initialSize int
newBitSize int
expectedLen int
}{
{"Resize to larger size", 64, 128, 16},
{"Resize to smaller size", 128, 64, 16}, // 大小應保持不變
{"Resize to equal size", 64, 64, 8}, // 大小應保持不變
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 初始化一個 Bitmap
bitmap := MakeBitmapWithBitSize(tt.initialSize)
// 調用 Resize
resizedBitmap := bitmap.Resize(tt.newBitSize)
// 檢查結果的 byte 大小
assert.Equal(t, tt.expectedLen, len(resizedBitmap))
})
}
}
func TestBitmap_UpdateFrom(t *testing.T) {
tests := []struct {
name string
initialSize int
newBitmapSize int
expectedBitPos uint32
expectedResult bool
}{
{"Update to larger bitmap", 64, 128, 50, true},
{"Update to smaller bitmap", 128, 64, 50, true}, // 新的 Bitmap 將只更新前面部分
{"Update to equal size bitmap", 64, 64, 20, true}, // 將相同大小的 Bitmap 進行合併
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 初始化一個初始大小的 Bitmap
bitmap := MakeBitmapWithBitSize(tt.initialSize)
bitmap.SetTrue(tt.expectedBitPos)
// 初始化一個新的 Bitmap
newBitmap := MakeBitmapWithBitSize(tt.newBitmapSize)
newBitmap.SetTrue(tt.expectedBitPos)
// 更新 Bitmap
updatedBitmap := bitmap.UpdateFrom(newBitmap)
// 檢查預期的位是否為 true
result := updatedBitmap.IsTrue(tt.expectedBitPos)
assert.Equal(t, tt.expectedResult, result)
})
}
}
func TestBitmap_SetAllTrueAndSetAllFalse(t *testing.T) {
bitmap := MakeBitmapWithBitSize(128)
// 測試 SetAllTrue
bitmap.SetAllTrue()
for i := 0; i < bitmap.BitSize(); i++ {
if !bitmap.IsTrue(uint32(i)) {
t.Errorf("Expected bit %d to be true, but got false", i)
}
}
// 測試 SetAllFalse
bitmap.SetAllFalse()
for i := 0; i < bitmap.BitSize(); i++ {
if bitmap.IsTrue(uint32(i)) {
t.Errorf("Expected bit %d to be false, but got true", i)
}
}
}

View File

@ -1,11 +0,0 @@
module code.30cm.net/digimon/library-go/utils/bitmap
go 1.22.3
require github.com/stretchr/testify v1.9.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,16 +0,0 @@
package usecase
type BitMapUseCase interface {
// SetTrue 設定該 Bit 狀態為 true
SetTrue(bitPos uint32)
// SetFalse 設定該Bit 狀態為 false
SetFalse(bitPos uint32)
// IsTrue 確認是否為真
IsTrue(bitPos uint32) bool
// Reset 重設 BitMap
Reset()
// ByteSize 最大 Byte 數
ByteSize() int
// BitSize 最大 Byte * 8
BitSize() int
}

View File

@ -1,14 +0,0 @@
package invited_code
const (
DefaultCodeLen = 8
InitAutoId = 10000000
)
var ConvertTable = []string{
"O", "D", "W", "X", "Y",
"G", "B", "C", "H", "E",
"F", "A", "Q", "I", "J",
"L", "M", "N", "Z", "K",
"P", "V", "R", "S", "T",
}

View File

@ -1,80 +0,0 @@
package invited_code
import (
"fmt"
"math"
"strings"
)
type Converter struct {
Base int
Length int
ConvertTable []string
}
func (c *Converter) EncodeFromNum(id int64) (string, error) {
code, err := generateCode(id, c.Base, c.Length, c.ConvertTable)
if err != nil {
return "", err
}
return code, nil
}
func (c *Converter) DecodeFromCode(code string) (int64, error) {
var result int64 = 0
length := len(code)
for i := 0; i < length; i++ {
char := string(code[i])
index := -1
for j, v := range c.ConvertTable {
if v == char {
index = j
break
}
}
if index >= 0 {
result = result*int64(c.Base) + int64(index)
} else {
return 0, fmt.Errorf("character not found in convert table")
}
}
return result, nil
}
// generateCode 從 UID 生成 referralCode
func generateCode(id int64, base int, length int, convertTable []string) (string, error) {
maxReferralUIDBoundary := int64(math.Pow(float64(len(ConvertTable)), float64(DefaultCodeLen)))
if id > maxReferralUIDBoundary {
return "", fmt.Errorf("encode out of range")
}
encoded := encodeToBase(id, base, length, convertTable)
return encoded, nil
}
func encodeToBase(num int64, base int, length int, convertTable []string) string {
result := ""
for num > 0 {
index := num % int64(base)
result = convertTable[index] + result
num /= int64(base)
}
if len(result) < length {
result = strings.Repeat(convertTable[0], length-len(result)) + result
}
return result
}
func MustConverter(base int, length int, convertTable []string) ConvertUseCase {
return &Converter{
Base: base,
Length: length,
ConvertTable: convertTable,
}
}

View File

@ -1,124 +0,0 @@
package invited_code
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 測試 ConvertUseCase 的功能
func TestConverter(t *testing.T) {
tests := []struct {
name string
id int64
base int
length int
wantCode string
wantError bool
}{
{
name: "Test Case 1: Valid ID 1000000",
id: 10000000,
base: 10,
length: 7,
wantCode: "DOOOOOOO",
wantError: false,
},
{
name: "Test Case 2: Valid ID 12345678",
id: 12345678,
base: 10,
length: 8,
wantCode: "DWXYGBCH",
wantError: false,
},
{
name: "Test Case 3: Valid ID 98765432",
id: 98765432,
base: 10,
length: 8,
wantCode: "EHCBGYXW",
wantError: false,
},
{
name: "Test Case 4: ID too large",
id: 10000000000000001, // ID 超過界限
base: 10,
length: 8,
wantCode: "",
wantError: true,
},
}
converter := MustConverter(10, 8, ConvertTable)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, err := converter.EncodeFromNum(tt.id)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantCode, code)
}
if !tt.wantError {
// 測試解碼
decodedID, err := converter.DecodeFromCode(code)
assert.NoError(t, err)
assert.Equal(t, tt.id, decodedID)
}
})
}
}
func TestDecodeFromCode(t *testing.T) {
converter := MustConverter(10, 8, ConvertTable)
tests := []struct {
name string
code string
wantID int64
wantError bool
}{
{
name: "Decode valid code 1",
code: "DOOOOOOO", // 對應於 id 10000000
wantID: 10000000,
wantError: false,
},
{
name: "Decode valid code 2",
code: "DWXYGBCH", // 對應於 id 12345678
wantID: 12345678,
wantError: false,
},
{
name: "Decode valid code 3",
code: "EHCBGYXW", // 對應於 id 98765432
wantID: 98765432,
wantError: false,
},
{
name: "Decode invalid code with character not in table",
code: "UWOXZZZ", // 包含 ZZZ不在轉換表中
wantID: 0,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := converter.DecodeFromCode(tt.code)
if tt.wantError {
assert.Error(t, err)
assert.Equal(t, int64(0), result)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantID, result)
}
})
}
}

View File

@ -1,11 +0,0 @@
module code.30cm.net/digimon/library-go/utils/invited_code
go 1.22.3
require github.com/stretchr/testify v1.9.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,6 +0,0 @@
package invited_code
type ConvertUseCase interface {
EncodeFromNum(id int64) (string, error)
DecodeFromCode(code string) (int64, error)
}