漫话Redis源码之七十二

2024-02-06 09:38
文章标签 源码 redis 漫话 七十二

本文主要是介绍漫话Redis源码之七十二,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这里主要是枚举和配置相关的:

configEnum syslog_facility_enum[] = {{"user",    LOG_USER},{"local0",  LOG_LOCAL0},{"local1",  LOG_LOCAL1},{"local2",  LOG_LOCAL2},{"local3",  LOG_LOCAL3},{"local4",  LOG_LOCAL4},{"local5",  LOG_LOCAL5},{"local6",  LOG_LOCAL6},{"local7",  LOG_LOCAL7},{NULL, 0}
};configEnum loglevel_enum[] = {{"debug", LL_DEBUG},{"verbose", LL_VERBOSE},{"notice", LL_NOTICE},{"warning", LL_WARNING},{NULL,0}
};configEnum supervised_mode_enum[] = {{"upstart", SUPERVISED_UPSTART},{"systemd", SUPERVISED_SYSTEMD},{"auto", SUPERVISED_AUTODETECT},{"no", SUPERVISED_NONE},{NULL, 0}
};configEnum aof_fsync_enum[] = {{"everysec", AOF_FSYNC_EVERYSEC},{"always", AOF_FSYNC_ALWAYS},{"no", AOF_FSYNC_NO},{NULL, 0}
};configEnum repl_diskless_load_enum[] = {{"disabled", REPL_DISKLESS_LOAD_DISABLED},{"on-empty-db", REPL_DISKLESS_LOAD_WHEN_DB_EMPTY},{"swapdb", REPL_DISKLESS_LOAD_SWAPDB},{NULL, 0}
};configEnum tls_auth_clients_enum[] = {{"no", TLS_CLIENT_AUTH_NO},{"yes", TLS_CLIENT_AUTH_YES},{"optional", TLS_CLIENT_AUTH_OPTIONAL},{NULL, 0}
};configEnum oom_score_adj_enum[] = {{"no", OOM_SCORE_ADJ_NO},{"yes", OOM_SCORE_RELATIVE},{"relative", OOM_SCORE_RELATIVE},{"absolute", OOM_SCORE_ADJ_ABSOLUTE},{NULL, 0}
};configEnum acl_pubsub_default_enum[] = {{"allchannels", USER_FLAG_ALLCHANNELS},{"resetchannels", 0},{NULL, 0}
};configEnum sanitize_dump_payload_enum[] = {{"no", SANITIZE_DUMP_NO},{"yes", SANITIZE_DUMP_YES},{"clients", SANITIZE_DUMP_CLIENTS},{NULL, 0}
};/* Output buffer limits presets. */
clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = {{0, 0, 0}, /* normal */{1024*1024*256, 1024*1024*64, 60}, /* slave */{1024*1024*32, 1024*1024*8, 60}  /* pubsub */
};/* OOM Score defaults */
int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT] = { 0, 200, 800 };/* Generic config infrastructure function pointers* int is_valid_fn(val, err)*     Return 1 when val is valid, and 0 when invalid.*     Optionally set err to a static error string.* int update_fn(val, prev, err)*     This function is called only for CONFIG SET command (not at config file parsing)*     It is called after the actual config is applied,*     Return 1 for success, and 0 for failure.*     Optionally set err to a static error string.*     On failure the config change will be reverted.*//* Configuration values that require no special handling to set, get, load or* rewrite. */
typedef struct boolConfigData {int *config; /* The pointer to the server config this value is stored in */const int default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(int val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(int val, int prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} boolConfigData;typedef struct stringConfigData {char **config; /* Pointer to the server config this value is stored in. */const char *default_value; /* Default value of the config on rewrite. */int (*is_valid_fn)(char* val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(char* val, char* prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */int convert_empty_to_null; /* Boolean indicating if empty strings shouldbe stored as a NULL value. */
} stringConfigData;typedef struct sdsConfigData {sds *config; /* Pointer to the server config this value is stored in. */const char *default_value; /* Default value of the config on rewrite. */int (*is_valid_fn)(sds val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(sds val, sds prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */int convert_empty_to_null; /* Boolean indicating if empty SDS strings shouldbe stored as a NULL value. */
} sdsConfigData;typedef struct enumConfigData {int *config; /* The pointer to the server config this value is stored in */configEnum *enum_value; /* The underlying enum type this data represents */const int default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(int val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(int val, int prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} enumConfigData;typedef enum numericType {NUMERIC_TYPE_INT,NUMERIC_TYPE_UINT,NUMERIC_TYPE_LONG,NUMERIC_TYPE_ULONG,NUMERIC_TYPE_LONG_LONG,NUMERIC_TYPE_ULONG_LONG,NUMERIC_TYPE_SIZE_T,NUMERIC_TYPE_SSIZE_T,NUMERIC_TYPE_OFF_T,NUMERIC_TYPE_TIME_T,
} numericType;typedef struct numericConfigData {union {int *i;unsigned int *ui;long *l;unsigned long *ul;long long *ll;unsigned long long *ull;size_t *st;ssize_t *sst;off_t *ot;time_t *tt;} config; /* The pointer to the numeric config this value is stored in */int is_memory; /* Indicates if this value can be loaded as a memory value */numericType numeric_type; /* An enum indicating the type of this value */long long lower_bound; /* The lower bound of this numeric value */long long upper_bound; /* The upper bound of this numeric value */const long long default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(long long val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(long long val, long long prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} numericConfigData;typedef union typeData {boolConfigData yesno;stringConfigData string;sdsConfigData sds;enumConfigData enumd;numericConfigData numeric;
} typeData;typedef struct typeInterface {/* Called on server start, to init the server with default value */void (*init)(typeData data);/* Called on server startup and CONFIG SET, returns 1 on success, 0 on error* and can set a verbose err string, update is true when called from CONFIG SET */int (*set)(typeData data, sds value, int update, const char **err);/* Called on CONFIG GET, required to add output to the client */void (*get)(client *c, typeData data);/* Called on CONFIG REWRITE, required to rewrite the config state */void (*rewrite)(typeData data, const char *name, struct rewriteConfigState *state);
} typeInterface;typedef struct standardConfig {const char *name; /* The user visible name of this config */const char *alias; /* An alias that can also be used for this config */const unsigned int flags; /* Flags for this specific config */typeInterface interface; /* The function pointers that define the type interface */typeData data; /* The type specific data exposed used by the interface */
} standardConfig;#define MODIFIABLE_CONFIG 0 /* This is the implied default for a standard * config, which is mutable. */
#define IMMUTABLE_CONFIG (1ULL<<0) /* Can this value only be set at startup? */
#define SENSITIVE_CONFIG (1ULL<<1) /* Does this value contain sensitive information */standardConfig configs[];/*-----------------------------------------------------------------------------* Enum access functions*----------------------------------------------------------------------------*//* Get enum value from name. If there is no match INT_MIN is returned. */
int configEnumGetValue(configEnum *ce, char *name) {while(ce->name != NULL) {if (!strcasecmp(ce->name,name)) return ce->val;ce++;}return INT_MIN;
}/* Get enum name from value. If no match is found NULL is returned. */
const char *configEnumGetName(configEnum *ce, int val) {while(ce->name != NULL) {if (ce->val == val) return ce->name;ce++;}return NULL;
}/* Wrapper for configEnumGetName() returning "unknown" instead of NULL if* there is no match. */
const char *configEnumGetNameOrUnknown(configEnum *ce, int val) {const char *name = configEnumGetName(ce,val);return name ? name : "unknown";
}/* Used for INFO generation. */
const char *evictPolicyToString(void) {return configEnumGetNameOrUnknown(maxmemory_policy_enum,server.maxmemory_policy);
}/*-----------------------------------------------------------------------------* Config file parsing*----------------------------------------------------------------------------*/int yesnotoi(char *s) {if (!strcasecmp(s,"yes")) return 1;else if (!strcasecmp(s,"no")) return 0;else return -1;
}void appendServerSaveParams(time_t seconds, int changes) {server.saveparams = zrealloc(server.saveparams,sizeof(struct saveparam)*(server.saveparamslen+1));server.saveparams[server.saveparamslen].seconds = seconds;server.saveparams[server.saveparamslen].changes = changes;server.saveparamslen++;
}void resetServerSaveParams(void) {zfree(server.saveparams);server.saveparams = NULL;server.saveparamslen = 0;
}void queueLoadModule(sds path, sds *argv, int argc) {int i;struct moduleLoadQueueEntry *loadmod;loadmod = zmalloc(sizeof(struct moduleLoadQueueEntry));loadmod->argv = zmalloc(sizeof(robj*)*argc);loadmod->path = sdsnew(path);loadmod->argc = argc;for (i = 0; i < argc; i++) {loadmod->argv[i] = createRawStringObject(argv[i],sdslen(argv[i]));}listAddNodeTail(server.loadmodule_queue,loadmod);
}/* Parse an array of CONFIG_OOM_COUNT sds strings, validate and populate* server.oom_score_adj_values if valid.*/static int updateOOMScoreAdjValues(sds *args, const char **err, int apply) {int i;int values[CONFIG_OOM_COUNT];for (i = 0; i < CONFIG_OOM_COUNT; i++) {char *eptr;long long val = strtoll(args[i], &eptr, 10);if (*eptr != '\0' || val < -2000 || val > 2000) {if (err) *err = "Invalid oom-score-adj-values, elements must be between -2000 and 2000.";return C_ERR;}values[i] = val;}/* Verify that the values make sense. If they don't omit a warning but* keep the configuration, which may still be valid for privileged processes.*/if (values[CONFIG_OOM_REPLICA] < values[CONFIG_OOM_MASTER] ||values[CONFIG_OOM_BGCHILD] < values[CONFIG_OOM_REPLICA]) {serverLog(LOG_WARNING,"The oom-score-adj-values configuration may not work for non-privileged processes! ""Please consult the documentation.");}/* Store values, retain previous config for rollback in case we fail. */int old_values[CONFIG_OOM_COUNT];for (i = 0; i < CONFIG_OOM_COUNT; i++) {old_values[i] = server.oom_score_adj_values[i];server.oom_score_adj_values[i] = values[i];}/* When parsing the config file, we want to apply only when all is done. */if (!apply)return C_OK;/* Update */if (setOOMScoreAdj(-1) == C_ERR) {/* Roll back */for (i = 0; i < CONFIG_OOM_COUNT; i++)server.oom_score_adj_values[i] = old_values[i];if (err)*err = "Failed to apply oom-score-adj-values configuration, check server logs.";return C_ERR;}return C_OK;
}void initConfigValues() {for (standardConfig *config = configs; config->name != NULL; config++) {config->interface.init(config->data);}
}void loadServerConfigFromString(char *config) {const char *err = NULL;int linenum = 0, totlines, i;int slaveof_linenum = 0;sds *lines;int save_loaded = 0;lines = sdssplitlen(config,strlen(config),"\n",1,&totlines);for (i = 0; i < totlines; i++) {sds *argv;int argc;linenum = i+1;lines[i] = sdstrim(lines[i]," \t\r\n");/* Skip comments and blank lines */if (lines[i][0] == '#' || lines[i][0] == '\0') continue;/* Split into arguments */argv = sdssplitargs(lines[i],&argc);if (argv == NULL) {err = "Unbalanced quotes in configuration line";goto loaderr;}/* Skip this line if the resulting command vector is empty. */if (argc == 0) {sdsfreesplitres(argv,argc);continue;}sdstolower(argv[0]);/* Iterate the configs that are standard */int match = 0;for (standardConfig *config = configs; config->name != NULL; config++) {if ((!strcasecmp(argv[0],config->name) ||(config->alias && !strcasecmp(argv[0],config->alias)))){if (argc != 2) {err = "wrong number of arguments";goto loaderr;}if (!config->interface.set(config->data, argv[1], 0, &err)) {goto loaderr;}match = 1;break;}}if (match) {sdsfreesplitres(argv,argc);continue;}/* Execute config directives */if (!strcasecmp(argv[0],"bind") && argc >= 2) {int j, addresses = argc-1;if (addresses > CONFIG_BINDADDR_MAX) {err = "Too many bind addresses specified"; goto loaderr;}/* Free old bind addresses */for (j = 0; j < server.bindaddr_count; j++) {zfree(server.bindaddr[j]);}for (j = 0; j < addresses; j++)server.bindaddr[j] = zstrdup(argv[j+1]);server.bindaddr_count = addresses;} else if (!strcasecmp(argv[0],"unixsocketperm") && argc == 2) {errno = 0;server.unixsocketperm = (mode_t)strtol(argv[1], NULL, 8);if (errno || server.unixsocketperm > 0777) {err = "Invalid socket file permissions"; goto loaderr;}} else if (!strcasecmp(argv[0],"save")) {/* We don't reset save params before loading, because if they're not part* of the file the defaults should be used.*/if (!save_loaded) {save_loaded = 1;resetServerSaveParams();}if (argc == 3) {int seconds = atoi(argv[1]);int changes = atoi(argv[2]);if (seconds < 1 || changes < 0) {err = "Invalid save parameters"; goto loaderr;}appendServerSaveParams(seconds,changes);} else if (argc == 2 && !strcasecmp(argv[1],"")) {resetServerSaveParams();}} else if (!strcasecmp(argv[0],"dir") && argc == 2) {if (chdir(argv[1]) == -1) {serverLog(LL_WARNING,"Can't chdir to '%s': %s",argv[1], strerror(errno));exit(1);}} else if (!strcasecmp(argv[0],"logfile") && argc == 2) {FILE *logfp;zfree(server.logfile);server.logfile = zstrdup(argv[1]);if (server.logfile[0] != '\0') {/* Test if we are able to open the file. The server will not* be able to abort just for this problem later... */logfp = fopen(server.logfile,"a");if (logfp == NULL) {err = sdscatprintf(sdsempty(),"Can't open the log file: %s", strerror(errno));goto loaderr;}fclose(logfp);}} else if (!strcasecmp(argv[0],"include") && argc == 2) {loadServerConfig(argv[1], 0, NULL);} else if ((!strcasecmp(argv[0],"slaveof") ||!strcasecmp(argv[0],"replicaof")) && argc == 3) {slaveof_linenum = linenum;sdsfree(server.masterhost);if (!strcasecmp(argv[1], "no") && !strcasecmp(argv[2], "one")) {server.masterhost = NULL;continue;}server.masterhost = sdsnew(argv[1]);char *ptr;server.masterport = strtol(argv[2], &ptr, 10);if (server.masterport < 0 || server.masterport > 65535 || *ptr != '\0') {err = "Invalid master port"; goto loaderr;}server.repl_state = REPL_STATE_CONNECT;} else if (!strcasecmp(argv[0],"list-max-ziplist-entries") && argc == 2){/* DEAD OPTION */} else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) {/* DEAD OPTION */} else if (!strcasecmp(argv[0],"rename-command") && argc == 3) {struct redisCommand *cmd = lookupCommand(argv[1]);int retval;if (!cmd) {err = "No such command in rename-command";goto loaderr;}/* If the target command name is the empty string we just* remove it from the command table. */retval = dictDelete(server.commands, argv[1]);serverAssert(retval == DICT_OK);/* Otherwise we re-add the command under a different name. */if (sdslen(argv[2]) != 0) {sds copy = sdsdup(argv[2]);retval = dictAdd(server.commands, copy, cmd);if (retval != DICT_OK) {sdsfree(copy);err = "Target command name already exists"; goto loaderr;}}} else if (!strcasecmp(argv[0],"cluster-config-file") && argc == 2) {zfree(server.cluster_configfile);server.cluster_configfile = zstrdup(argv[1]);} else if (!strcasecmp(argv[0],"client-output-buffer-limit") &&argc == 5){int class = getClientTypeByName(argv[1]);unsigned long long hard, soft;int soft_seconds;if (class == -1 || class == CLIENT_TYPE_MASTER) {err = "Unrecognized client limit class: the user specified ""an invalid one, or 'master' which has no buffer limits.";goto loaderr;}hard = memtoll(argv[2],NULL);soft = memtoll(argv[3],NULL);soft_seconds = atoi(argv[4]);if (soft_seconds < 0) {err = "Negative number of seconds in soft limit is invalid";goto loaderr;}server.client_obuf_limits[class].hard_limit_bytes = hard;server.client_obuf_limits[class].soft_limit_bytes = soft;server.client_obuf_limits[class].soft_limit_seconds = soft_seconds;} else if (!strcasecmp(argv[0],"oom-score-adj-values") && argc == 1 + CONFIG_OOM_COUNT) {if (updateOOMScoreAdjValues(&argv[1], &err, 0) == C_ERR) goto loaderr;} else if (!strcasecmp(argv[0],"notify-keyspace-events") && argc == 2) {int flags = keyspaceEventsStringToFlags(argv[1]);if (flags == -1) {err = "Invalid event class character. Use 'g$lshzxeA'.";goto loaderr;}server.notify_keyspace_events = flags;} else if (!strcasecmp(argv[0],"user") && argc >= 2) {int argc_err;if (ACLAppendUserForLoading(argv,argc,&argc_err) == C_ERR) {char buf[1024];const char *errmsg = ACLSetUserStringError();snprintf(buf,sizeof(buf),"Error in user declaration '%s': %s",argv[argc_err],errmsg);err = buf;goto loaderr;}} else if (!strcasecmp(argv[0],"loadmodule") && argc >= 2) {queueLoadModule(argv[1],&argv[2],argc-2);} else if (!strcasecmp(argv[0],"sentinel")) {/* argc == 1 is handled by main() as we need to enter the sentinel* mode ASAP. */if (argc != 1) {if (!server.sentinel_mode) {err = "sentinel directive while not in sentinel mode";goto loaderr;}queueSentinelConfig(argv+1,argc-1,linenum,lines[i]);}} else {err = "Bad directive or wrong number of arguments"; goto loaderr;}sdsfreesplitres(argv,argc);}/* Sanity checks. */if (server.cluster_enabled && server.masterhost) {linenum = slaveof_linenum;i = linenum-1;err = "replicaof directive not allowed in cluster mode";goto loaderr;}/* To ensure backward compatibility and work while hz is out of range */if (server.config_hz < CONFIG_MIN_HZ) server.config_hz = CONFIG_MIN_HZ;if (server.config_hz > CONFIG_MAX_HZ) server.config_hz = CONFIG_MAX_HZ;sdsfreesplitres(lines,totlines);return;loaderr:fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR (Redis %s) ***\n",REDIS_VERSION);fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);fprintf(stderr, ">>> '%s'\n", lines[i]);fprintf(stderr, "%s\n", err);exit(1);
}/* Load the server configuration from the specified filename.* The function appends the additional configuration directives stored* in the 'options' string to the config file before loading.** Both filename and options can be NULL, in such a case are considered* empty. This way loadServerConfig can be used to just load a file or* just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {sds config = sdsempty();char buf[CONFIG_MAX_LINE+1];FILE *fp;/* Load the file content */if (filename) {if ((fp = fopen(filename,"r")) == NULL) {serverLog(LL_WARNING,"Fatal error, can't open config file '%s': %s",filename, strerror(errno));exit(1);}while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)config = sdscat(config,buf);fclose(fp);}/* Append content from stdin */if (config_from_stdin) {serverLog(LL_WARNING,"Reading config from stdin");fp = stdin;while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)config = sdscat(config,buf);}/* Append the additional options */if (options) {config = sdscat(config,"\n");config = sdscat(config,options);}loadServerConfigFromString(config);sdsfree(config);
}

这篇关于漫话Redis源码之七十二的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/683873

相关文章

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

Redis的数据过期策略和数据淘汰策略

《Redis的数据过期策略和数据淘汰策略》本文主要介绍了Redis的数据过期策略和数据淘汰策略,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录一、数据过期策略1、惰性删除2、定期删除二、数据淘汰策略1、数据淘汰策略概念2、8种数据淘汰策略

Redis存储的列表分页和检索的实现方法

《Redis存储的列表分页和检索的实现方法》在Redis中,列表(List)是一种有序的数据结构,通常用于存储一系列元素,由于列表是有序的,可以通过索引来访问元素,因此可以很方便地实现分页和检索功能,... 目录一、Redis 列表的基本操作二、分页实现三、检索实现3.1 方法 1:客户端过滤3.2 方法

Python中操作Redis的常用方法小结

《Python中操作Redis的常用方法小结》这篇文章主要为大家详细介绍了Python中操作Redis的常用方法,文中的示例代码简洁易懂,具有一定的借鉴价值,有需要的小伙伴可以了解一下... 目录安装Redis开启、关闭Redisredis数据结构redis-cli操作安装redis-py数据库连接和释放增

redis防止短信恶意调用的实现

《redis防止短信恶意调用的实现》本文主要介绍了在场景登录或注册接口中使用短信验证码时遇到的恶意调用问题,并通过使用Redis分布式锁来解决,具有一定的参考价值,感兴趣的可以了解一下... 目录1.场景2.排查3.解决方案3.1 Redis锁实现3.2 方法调用1.场景登录或注册接口中,使用短信验证码场

Redis 多规则限流和防重复提交方案实现小结

《Redis多规则限流和防重复提交方案实现小结》本文主要介绍了Redis多规则限流和防重复提交方案实现小结,包括使用String结构和Zset结构来记录用户IP的访问次数,具有一定的参考价值,感兴趣... 目录一:使用 String 结构记录固定时间段内某用户 IP 访问某接口的次数二:使用 Zset 进行

解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)

《解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)》该文章介绍了使用Redis的阻塞队列和Stream流的消息队列来优化秒杀系统的方案,通过将秒杀流程拆分为两条流水线,使用Redi... 目录Redis秒杀优化方案(阻塞队列+Stream流的消息队列)什么是消息队列?消费者组的工作方式每

Redis如何使用zset处理排行榜和计数问题

《Redis如何使用zset处理排行榜和计数问题》Redis的ZSET数据结构非常适合处理排行榜和计数问题,它可以在高并发的点赞业务中高效地管理点赞的排名,并且由于ZSET的排序特性,可以轻松实现根据... 目录Redis使用zset处理排行榜和计数业务逻辑ZSET 数据结构优化高并发的点赞操作ZSET 结

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操