feat(cache): upgrade cache interface

This commit is contained in:
LinkinStars 2023-08-22 16:10:41 +08:00
parent 3b1324b83b
commit 8a037f00db
15 changed files with 83 additions and 58 deletions

View File

@ -1,6 +1,7 @@
package answercmd package answercmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -39,7 +40,7 @@ var (
// @name Authorization // @name Authorization
func Main() { func Main() {
log.SetLogger(zap.NewLogger( log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath)))
Execute() Execute()
} }
@ -59,7 +60,7 @@ func runApp() {
fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision) fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision)
defer cleanup() defer cleanup()
if err := app.Run(); err != nil { if err := app.Run(context.Background()); err != nil {
panic(err) panic(err)
} }
} }

4
go.mod
View File

@ -29,8 +29,8 @@ require (
github.com/ory/dockertest/v3 v3.9.1 github.com/ory/dockertest/v3 v3.9.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405
github.com/segmentfault/pacman v1.0.4 github.com/segmentfault/pacman v1.0.5-0.20230822075009-309985fb8700
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822075009-309985fb8700
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05

8
go.sum
View File

@ -642,10 +642,10 @@ github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 h1:2i
github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/segmentfault/pacman v1.0.4 h1:6UIXuMHUeYMWe5toflV9SXZQizRny1RczjZJLj9kul0= github.com/segmentfault/pacman v1.0.5-0.20230822075009-309985fb8700 h1:VqxiuNGQg86GEKxnzmJegZR2Ufr7EVXo68mdKaf+/MQ=
github.com/segmentfault/pacman v1.0.4/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= github.com/segmentfault/pacman v1.0.5-0.20230822075009-309985fb8700/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0 h1:4x0qG7H2M3qH7Yo2BhGrVlji1iTmRAWgINY/JyENeHs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822075009-309985fb8700 h1:VZpexPTcr7sOxxYUGa/9cneyCESfisAhCNbaEuTpcwI=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822075009-309985fb8700/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk=

View File

@ -27,15 +27,15 @@ func NewAuthRepo(data *data.Data) auth.AuthRepo {
// GetUserCacheInfo get user cache info // GetUserCacheInfo get user cache info
func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken)
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
userInfo = &entity.UserCacheInfo{} userInfo = &entity.UserCacheInfo{}
err = json.Unmarshal([]byte(userInfoCache), userInfo) if !exist {
if err != nil { return nil, nil
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
_ = json.Unmarshal([]byte(userInfoCache), userInfo)
return userInfo, nil return userInfo, nil
} }
@ -81,15 +81,15 @@ func (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *
// GetUserStatus get user status // GetUserStatus get user status
func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID)
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
if !exist {
return nil, nil
}
userInfo = &entity.UserCacheInfo{} userInfo = &entity.UserCacheInfo{}
err = json.Unmarshal([]byte(userInfoCache), userInfo) _ = json.Unmarshal([]byte(userInfoCache), userInfo)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return userInfo, nil return userInfo, nil
} }
@ -104,16 +104,16 @@ func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err er
// GetAdminUserCacheInfo get admin user cache info // GetAdminUserCacheInfo get admin user cache info
func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken) userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return return
} }
userInfo = &entity.UserCacheInfo{} if !exist {
err = json.Unmarshal([]byte(userInfoCache), userInfo) return nil, nil
if err != nil {
return nil, err
} }
userInfo = &entity.UserCacheInfo{}
_ = json.Unmarshal([]byte(userInfoCache), userInfo)
return userInfo, nil return userInfo, nil
} }
@ -144,7 +144,10 @@ func (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken st
// AddUserTokenMapping add user token mapping // AddUserTokenMapping add user token mapping
func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) { func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
key := constant.UserTokenMappingCacheKey + userID key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key) resp, _, err := ar.data.Cache.GetString(ctx, key)
if err != nil {
return err
}
mapping := make(map[string]bool, 0) mapping := make(map[string]bool, 0)
if len(resp) > 0 { if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping) _ = json.Unmarshal([]byte(resp), &mapping)
@ -157,7 +160,10 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
// RemoveUserTokens Log out all users under this user id // RemoveUserTokens Log out all users under this user id
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) { func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
key := constant.UserTokenMappingCacheKey + userID key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key) resp, _, err := ar.data.Cache.GetString(ctx, key)
if err != nil {
return
}
mapping := make(map[string]bool, 0) mapping := make(map[string]bool, 0)
if len(resp) > 0 { if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping) _ = json.Unmarshal([]byte(resp), &mapping)

View File

@ -43,19 +43,18 @@ func (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, conf
return return
} }
func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error) { func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actionInfo *entity.ActionRecordInfo, err error) {
now := time.Now() now := time.Now()
cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02"))
actioninfo = &entity.ActionRecordInfo{} res, exist, err := cr.data.Cache.GetString(ctx, cacheKey)
res, err := cr.data.Cache.GetString(ctx, cacheKey)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, err
} }
err = json.Unmarshal([]byte(res), actioninfo) actionInfo = &entity.ActionRecordInfo{}
if err != nil { if exist {
return actioninfo, nil _ = json.Unmarshal([]byte(res), actionInfo)
} }
return actioninfo, nil return actionInfo, nil
} }
func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) { func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) {
@ -79,9 +78,12 @@ func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err
// GetCaptcha get captcha from cache // GetCaptcha get captcha from cache
func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) { func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) {
captcha, err = cr.data.Cache.GetString(ctx, key) captcha, exist, err := cr.data.Cache.GetString(ctx, key)
if err != nil { if err != nil {
log.Debug(err) return "", err
}
if !exist {
return "", fmt.Errorf("captcha not exist")
} }
return captcha, nil return captcha, nil
} }

View File

@ -28,7 +28,7 @@ func NewConfigRepo(data *data.Data) config.ConfigRepo {
func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) { func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) {
cacheKey := fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, id) cacheKey := fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, id)
if cacheData, err := cr.data.Cache.GetString(ctx, cacheKey); err == nil && len(cacheData) > 0 { if cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey); err == nil && exist {
c = &entity.Config{} c = &entity.Config{}
c.BuildByJSON([]byte(cacheData)) c.BuildByJSON([]byte(cacheData))
if c.ID > 0 { if c.ID > 0 {
@ -54,7 +54,7 @@ func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Confi
func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) { func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) {
cacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key cacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key
if cacheData, err := cr.data.Cache.GetString(ctx, cacheKey); err == nil && len(cacheData) > 0 { if cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey); err == nil && exist {
c = &entity.Config{} c = &entity.Config{}
c.BuildByJSON([]byte(cacheData)) c.BuildByJSON([]byte(cacheData))
if c.ID > 0 { if c.ID > 0 {

View File

@ -33,9 +33,12 @@ func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration
// VerifyCode verify the code if out of date // VerifyCode verify the code if out of date
func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) { func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) {
content, err = e.data.Cache.GetString(ctx, code) content, exist, err := e.data.Cache.GetString(ctx, code)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return "", err
} }
return if !exist {
return "", nil
}
return content, nil
} }

View File

@ -259,8 +259,8 @@ func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int
// try to get sitemap data from cache // try to get sitemap data from cache
cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page) cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page)
cacheData, err := qr.data.Cache.GetString(ctx, cacheKey) cacheData, exist, err := qr.data.Cache.GetString(ctx, cacheKey)
if err == nil && len(cacheKey) > 0 { if err == nil && exist {
_ = json.Unmarshal([]byte(cacheData), &questionIDList) _ = json.Unmarshal([]byte(cacheData), &questionIDList)
return questionIDList, nil return questionIDList, nil
} }

View File

@ -61,12 +61,14 @@ func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string) (siteInf
} }
func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) { func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) {
siteInfo = &entity.SiteInfo{} siteInfoCache, exist, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)
siteInfoCache, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)
if err != nil { if err != nil {
return nil return nil
} }
_ = json.Unmarshal([]byte(siteInfoCache), siteInfo) siteInfo = &entity.SiteInfo{}
if exist {
_ = json.Unmarshal([]byte(siteInfoCache), siteInfo)
}
return siteInfo return siteInfo
} }

View File

@ -85,10 +85,13 @@ func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
// GetCacheUserExternalLoginInfo cache user info for external login // GetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo( func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) { ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key) res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
if err != nil { if err != nil {
return info, err return info, err
} }
_ = json.Unmarshal([]byte(res), &info) info = &schema.ExternalLoginUserInfoCache{}
if exist {
_ = json.Unmarshal([]byte(res), &info)
}
return info, nil return info, nil
} }

View File

@ -41,6 +41,9 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if userCacheInfo == nil {
return nil, nil
}
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID) cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
if cacheInfo != nil { if cacheInfo != nil {
userCacheInfo.UserStatus = cacheInfo.UserStatus userCacheInfo.UserStatus = cacheInfo.UserStatus

View File

@ -95,12 +95,15 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI
return dashboardInfo, nil return dashboardInfo, nil
} }
func (ds *dashboardService) getFromCache(ctx context.Context) (*schema.DashboardInfo, error) { func (ds *dashboardService) getFromCache(ctx context.Context) (dashboardInfo *schema.DashboardInfo, err error) {
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey) infoStr, exist, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
dashboardInfo := &schema.DashboardInfo{} dashboardInfo = &schema.DashboardInfo{}
if !exist {
return dashboardInfo, nil
}
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil { if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
return nil, err return nil, err
} }

View File

@ -145,7 +145,7 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str
func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) { func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) {
content, err := es.emailRepo.VerifyCode(ctx, code) content, err := es.emailRepo.VerifyCode(ctx, code)
if err != nil { if err != nil {
log.Warn(err) log.Error(err)
} }
return content return content
} }

View File

@ -46,13 +46,13 @@ func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRed
redBot := &schema.RedDot{} redBot := &schema.RedDot{}
inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID) inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID)
achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID) achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID)
inboxValue, err := ns.data.Cache.GetInt64(ctx, inboxKey) inboxValue, _, err := ns.data.Cache.GetInt64(ctx, inboxKey)
if err != nil { if err != nil {
redBot.Inbox = 0 redBot.Inbox = 0
} else { } else {
redBot.Inbox = inboxValue redBot.Inbox = inboxValue
} }
achievementValue, err := ns.data.Cache.GetInt64(ctx, achievementKey) achievementValue, _, err := ns.data.Cache.GetInt64(ctx, achievementKey)
if err != nil { if err != nil {
redBot.Achievement = 0 redBot.Achievement = 0
} else { } else {

View File

@ -8,12 +8,14 @@ import (
type Cache interface { type Cache interface {
Base Base
GetString(ctx context.Context, key string) (string, error) GetString(ctx context.Context, key string) (data string, exist bool, err error)
SetString(ctx context.Context, key, value string, ttl time.Duration) error SetString(ctx context.Context, key, value string, ttl time.Duration) (err error)
GetInt64(ctx context.Context, key string) (int64, error) GetInt64(ctx context.Context, key string) (data int64, exist bool, err error)
SetInt64(ctx context.Context, key string, value int64, ttl time.Duration) error SetInt64(ctx context.Context, key string, value int64, ttl time.Duration) (err error)
Del(ctx context.Context, key string) error Increase(ctx context.Context, key string, value int64) (data int64, err error)
Flush(ctx context.Context) error Decrease(ctx context.Context, key string, value int64) (data int64, err error)
Del(ctx context.Context, key string) (err error)
Flush(ctx context.Context) (err error)
} }
var ( var (