保哥笔记

Discuz $_G 全局变量深度解析:初始化时机、性能开销、安全坑与现代框架对比

Discuz! 全局变量 $_G 是论坛框架里最关键的"运行时上下文容器"——保存了当前用户、当前帖子、当前版块、所有缓存、所有设置项。Discuz 几乎所有插件、模板、二开代码都在围绕这一个数组转。但网传的"$_G 字段速查表"只列字段名,没讲清楚它的初始化时机、性能开销、安全坑(直接打印会泄漏数据库密码)、与现代 PHP 全局上下文模式的对比。这一篇把 Discuz $_G 拆透。

$_G 在 Discuz 框架里的位置

Discuz! 在每个请求开始时(source/class/discuz/discuz_application.phpinit 方法)做几件事:

  1. 读 config 配置:从 config/config_global.php 加载站点设置(数据库连接、UCenter URL 等);
  2. 装载缓存:从 Memcached / Redis / 文件缓存里取已编译好的设置 / 用户组 / 版块树等;
  3. 识别用户身份:从 cookie 解出 UID + 用户组,查数据库取当前用户信息;
  4. 解析当前请求参数:fid(版块)、tid(帖子)、mod(模式)等;
  5. 组装 $_G:把上面所有数据汇总到 $GLOBALS['_G'](PHP 超全局),代码里用 $_G 直接引用。

所以 $_G 不是 PHP 原生超全局($_GET$_POST 是原生),是 Discuz 自定义的,但通过 global $_G 或在文件顶部 declare 后能像超全局一样在任何地方使用。

与 PHP 原生超全局的对比

变量来源含义
$_GETPHP 原生URL 查询参数
$_POSTPHP 原生POST 表单数据
$_SERVERPHP 原生请求头 + 服务器信息
$_SESSIONPHP 原生会话数据
$GLOBALSPHP 原生所有全局变量
$_GDiscuz 自定义Discuz 运行时上下文(用户/版块/帖子/设置/缓存全部)
$_ENVPHP 原生环境变量

$_G 的核心字段分组

$_G 里上百个字段按用途分四大类:

当前请求上下文

$_G['uid']         // 当前登录用户 ID(0 表示未登录)
$_G['username']    // 当前用户名
$_G['adminid']     // 管理组 ID(0=普通用户,1=超管)
$_G['groupid']     // 用户组 ID
$_G['fid']         // 当前版块 ID(帖子页/版块页才有)
$_G['tid']         // 当前帖子 ID(帖子页才有)
$_G['mod']         // 当前 mod 参数(forum.php?mod=viewthread)
$_G['inajax']      // 是否 ajax 请求(0/1)
$_G['page']        // 当前分页号
$_G['tpp']         // 每页显示数量

全局配置(来自 config_global.php)

$_G['config']['db'][1]['dbhost']     // 数据库主机
$_G['config']['db'][1]['dbname']     // 数据库名
$_G['config']['db'][1]['dbuser']     // 数据库用户名
$_G['config']['db'][1]['dbpw']       // 数据库密码 ⚠ 敏感
$_G['config']['db'][1]['tablepre']   // 表前缀(默认 pre_)
$_G['config']['security']['authkey'] // 认证密钥 ⚠ 敏感
$_G['config']['cookie']['cookiepre'] // Cookie 前缀

后台设置(来自 wp_common_setting 表)

$_G['setting']['sitename']       // 站点名
$_G['setting']['siteurl']        // 站点 URL
$_G['setting']['icp']            // ICP 备案号
$_G['setting']['attachurl']      // 附件 URL
$_G['setting']['attachdir']      // 附件目录
$_G['setting']['rewriterule']    // 伪静态规则
$_G['setting']['extcredits']     // 积分配置(数组)
$_G['setting']['creditsformula'] // 总积分计算公式
$_G['setting']['plugins']        // 已启用插件列表

缓存数据(来自 Memcached / Redis)

$_G['cache']['groupperms']  // 用户组权限缓存
$_G['cache']['forums']      // 版块树缓存
$_G['cache']['smiley']      // 表情缓存
$_G['cache']['plugin']      // 插件配置缓存
$_G['cache']['styles']      // 风格缓存
$_G['cache']['userstatus']  // 用户状态缓存

安全坑:直接打印 $_G 泄漏数据库密码

这是 Discuz 二开里最容易踩的坑——开发者调试时随手 var_dump($_G)print_r($_G),结果整个数据库密码、authkey、所有缓存内容都打印到页面或日志。如果是测试环境也罢,万一在生产环境的某个调试页面忘了删,攻击者一发现就拿到所有敏感信息。

安全打印 $_G 的方式

// ❌ 危险:暴露所有敏感数据
print_r($_G);

// ✅ 安全:只打印当前关心的字段 var_dump([ 'uid' => $_G['uid'], 'username' => $_G['username'], 'fid' => $_G['fid'], 'tid' => $_G['tid'], 'mod' => $_G['mod'], ]);

// ✅ 或者打印过滤敏感字段后的副本 function safe_dump_g() { $copy = $GLOBALS['_G']; unset($copy['config']['db']); unset($copy['config']['security']); unset($copy['cache']['plugin']); // 插件可能有 API key return $copy; } print_r(safe_dump_g());

调试代码的标准防护

所有调试输出都包在 if (DEBUG) 里:

// 在 config 里加常量
define('DEBUG', false);   // 上线必须 false

// 调试代码包起来 if (DEBUG) { echo '<pre>'; print_r(safe_dump_g()); echo '</pre>'; }

这样上线时只要 DEBUG=false,所有调试输出全静默。永远不要靠"我记得删了"——人类记不住。

性能开销:$_G 每次请求都要装一遍

$_G 在每个 PHP 请求开始时全量装载,开销不小:

装载步骤典型耗时
读 config_global.php~ 1 ms
装载用户身份(含查 user 表)5-15 ms
装载缓存(Memcached/Redis)2-5 ms(命中)/ 50-200 ms(未命中要重建)
装载版块树(forum 数 × 1ms)论坛 100 版块约 5-10 ms
装载已启用插件配置每插件 1-2 ms

典型中型论坛每请求装 $_G 总耗时 30-80 ms。这是 Discuz 性能的主要瓶颈之一——所以缓存是否命中(Memcached/Redis 在线)极其关键。

缓存全部从 Redis 读取的优化

Discuz X3.5 起原生支持 Redis:

// config/config_global.php
$_config['memory']['redis']['server']   = '127.0.0.1';
$_config['memory']['redis']['port']     = 6379;
$_config['memory']['redis']['serializer'] = 1;
$_config['memory']['redis']['pconnect'] = 1;
$_config['memory']['redis']['timeout']  = 3;

开了之后所有 cache 字段从 Redis 拿,比文件缓存快 10 倍。Memcached 也类似,二选一。

什么时候用 $_G 什么时候直接查 SQL

开发 Discuz 插件时常见两难:从 $_G['cache'] 取还是直接查数据库?决策矩阵:

数据特征从 $_G['cache'] 取直接查 SQL
变化频率低✓(缓存命中率高)慢且没必要
需要最新(如订单状态)不行(缓存过期)✓(实时)
大数据量(10 万+)不行(不能全装内存)✓(分页查)
跨用户共享(如版块树)✓(一次缓存所有用户共用)
用户私有(如未读消息)不适合(缓存按用户分太多)

原则:"配置类、共享类、变化少"用缓存;"业务类、用户私有、变化多"直接查

$_G 与 Discuz X 各版本的差异

版本主要变化
Discuz X 1.x引入 $_G 替代之前 X1 时代的零散全局变量
Discuz X 2.x$_G 字段大幅扩展,加入 plugin / styles 缓存
Discuz X 3.0支持 Redis / Memcached 加速 $_G 装载
Discuz X 3.2$_G['config']['security'] 引入
Discuz X 3.4移动端字段 $_G['mobile'] 加入
Discuz X 3.5原生 PHP 8 兼容、Redis 序列化器选项

跨版本写插件要注意——X3.5 移除了某些老字段,老插件直接用可能 NOTICE。建议用 isset() 守护:if (isset($_G['xxx'])) { ... }

$_G 与现代 PHP 框架的对比

$_G 这种"全局上下文容器"在 2026 年的现代 PHP 框架里被替换为更优雅的写法:

框架等价机制
Discuz$_G 全局数组
LaravelService Container(依赖注入)
SymfonyContainer + Request 对象
YiiYii::$app 单例
WordPress$GLOBALS['post'] / $GLOBALS['wp_query']
ThinkPHPapp() 容器

$_G 的设计在 2010 年代是合理的——简单、直接。但在 2026 年看显然有几个问题:

但 Discuz 已经成熟稳定,这套架构不大可能重构。理解 $_G 是与 Discuz 共存的现实。

扩展 $_G 的实战

插件经常需要给 $_G 加自定义字段。标准做法:

// 在插件入口(plugin.xml 指定的 module 文件)
require_once libfile('function/myplugin', 'class/plugin');

// 给 $_G 加一个字段 global $_G; $_G['myplugin']['version'] = '1.0'; $_G['myplugin']['settings'] = C::t('common_setting')->fetch('myplugin_settings');

// 之后任何文件可以直接用 echo $_G['myplugin']['version'];

注意:

调试技巧:在线观察 $_G

开发新插件时实时观察 $_G 内容:

// 在管理员账号下显示一个调试浮层
if ($_G['adminid'] == 1 && isset($_GET['debug_g'])) {
    echo '<div style="position:fixed;bottom:0;left:0;background:#fff;border:2px solid red;padding:10px;max-height:50vh;overflow:auto;z-index:9999">';
    echo '<pre>';
    print_r(safe_dump_g());
    echo '</pre>';
    echo '</div>';
}

访问 ?debug_g=1 弹出浮层只对管理员可见(adminid=1),不影响普通用户。

性能优化:减少 $_G 装载开销

大型论坛 $_G 装载是性能瓶颈,几个优化方向:

  1. 开 Redis / Memcached:所有 cache 字段从内存读,比文件缓存快 10x;
  2. 关掉不必要的插件:每个启用插件都给 $_G['cache']['plugin'] 加东西;
  3. 用 OPcache:让 PHP 缓存编译后的 bytecode,加快每次请求启动;
  4. 分离静态资源:CSS/JS/图片走 CDN,论坛 PHP 进程只处理动态请求;
  5. 升级 PHP 8.1+:Discuz X3.5 支持 PHP 8,性能比 PHP 7 好 10-30%。

与现代化部署的协同

把 Discuz 上 Docker / K8s 时,$_G 的几个相关注意点:

$_G 滥用的反模式

实战见过的反模式(不要学):

常见问题解答

$_G 与 $GLOBALS['_G'] 是同一个东西吗?

是。$_G 是通过 global $_G 声明后从 $GLOBALS['_G'] 取出的引用。在文件顶部(Discuz 通过 declare 自动注入)声明后,整个文件作用域都能用 $_G。直接 $GLOBALS['_G'] 在所有位置都能用,不需要 global 声明。

插件给 $_G 加自定义字段会冲突吗?

会,如果命名不规范。建议每个插件用插件标识做前缀,比如 $_G['myplugin']['xxx']$_G['plugin_myplugin_xxx']。Discuz 自家保留字段(uid、username、fid、tid 等)禁止覆盖。命名空间规范了 100% 不会冲突。

为什么 $_G['fid'] / $_G['tid'] 有时为空?

fid/tid 只在帖子相关页面(forum.php?mod=viewthread / forum.php?mod=forumdisplay)才有值。在首页、个人中心、其它非论坛页面,这两个字段为空或不存在。代码里使用前要 isset($_G['fid']) && $_G['fid'] > 0 守护。

$_G 是只读的吗?

不是。Discuz 没有强制锁定 $_G,任何代码都能写。但不应该修改核心字段(uid、username 等),否则会破坏后续逻辑(比如权限检查依赖 uid)。插件加自定义字段是 OK 的(用插件命名空间)。

$_G['cache'] 的缓存什么时候失效?

取决于具体字段。① forums(版块树)在版块新增/修改/删除时;② groupperms(权限)在用户组改时;③ plugins(插件配置)在启用/禁用插件时。后台 → 工具 → 更新缓存可以手动触发清缓存。

能不能把 $_G 序列化保存?

能但不建议。$_G 体积大(含所有缓存),序列化后 100KB+ 很常见。要保存当前请求快照(用于复现 bug),建议只保存关键字段:serialize(['uid'=>$_G['uid'], 'fid'=>$_G['fid'], 'tid'=>$_G['tid'], 'mod'=>$_G['mod']])

升级 Discuz 后老插件读 $_G 报错?

多数是字段名变了。X3.4 → X3.5 移除了几个老字段(如 $_G['member'] 部分子字段,要走 $_G['member'] = getuserbyuid($_G['uid']) 显式查)。修法:用 isset() 守护 + 给老字段写兜底逻辑。

$_G 能跨请求保持吗?

不能。$_G 是请求级的——每个 PHP 请求开始时重新装载,请求结束就释放。要跨请求保持的状态用 Session($_SESSION)或 Cookie。混淆这点会导致"为什么我设的值下次请求就丢了"困惑。

$_G 的内存占用大概多少?

典型中型论坛(10 万用户、100 个版块、20 个插件)单请求 $_G 内存约 5-15 MB。如果论坛规模大(千万级用户、万级版块),$_G 可能膨胀到 50MB+。这时要限制单页面装载范围(按需查而不是全装)。

有没有让 $_G 自动补全的 IDE 插件?

有限。VS Code 的 PHP Intellisense 可以识别全局变量但对动态数组键支持差。PHPStorm 较好,配合 .phpstorm.meta.php 文件能给 $_G 主要字段加类型提示。社区也有 dev_phpstorm_stub 项目专门给 Discuz 写存根,开发体验大幅提升。

因本文不是用Markdown格式的编辑器书写的,转换的页面可能不符合AMP标准。