- Published on
Protobuf Edition 2026 — 收紧的三个默认值,以及 protoc 首次强制的模式大小上限
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 比公告早四天发布的 RC
- 用一段话说清 Edition 这套机制
- protoc 从哪个版本开始接受 edition = "2026"
- 默认值变化①:enforce_naming_style 变为 STYLE2026
- 默认值变化②:default_symbol_visibility 变为 STRICT
- 默认值变化③:enforce_proto_limits — 这才是真正的变化
- 检查由 protoc 打开,运行时是关闭的
- 新引入的东西(简述)
- 诚实的权衡与局限
- 那么,现在该做什么
- 结语
- 参考资料
引言 — 比公告早四天发布的 RC
2026年7月13日,protobuf.dev 的新闻页面上出现了 Edition 2026 公告。这是三天前的事。可是翻看 GitHub 的发布列表会发现,v36.0-rc1 早在比公告还要早四天的7月9日就已经是 published 状态了。公告写着"计划在36.x、2026年第三季度发布"的时候,那个36.x的第一个RC其实已经出来了。
本文不是对那份公告的摘要。公告很短,有几处和实际实现对不上,而且干脆漏掉了其中最有意思的一项变化。所以本文整理的是直接阅读v36.0-rc1标签下的实际源码 — descriptor.proto、descriptor.h、code_generator.h、command_line_interface.cc — 之后确认的内容。
先说结论。Edition 2026没有新增任何语法。 公告最后一句话原封不动地成立 — "There is no new or removed grammar in Edition 2026."取而代之的是收紧了三个默认值,其中一个是 protoc 十五年多来都没做过的事。
用一段话说清 Edition 这套机制
Protocol Buffers 本身和二进制序列化格式的比较已经写过了,这里只简单提一下 Edition。以前是在文件最上面写syntax = "proto2"或syntax = "proto3",那一个词就把字段存在性、enum 的开放/封闭、默认打包方式等一堆互不相关的行为捆在一起决定了。Edition 把这个捆绑拆成一个个独立的特性(feature),文件最上面只需要写一个年份,比如edition = "2024"。每个特性在不同 Edition 下有不同的默认值,需要的话也可以在文件级别或某个具体元素上单独覆盖。
关键在这里 — 不升级 Edition,就什么都不会变。 默认值是绑定在 Edition 上的,所以停留在edition = "2023"的文件完全不会经历 Edition 2026 的默认值变化。新的 Edition 是一种"从现在起这样做更好"的默认值声明机制,而不是破坏既有文件的机制。
看descriptor.proto里的Edition enum,已发布的 Edition 是这样排列的。
// Editions that have been released. The specific values are arbitrary and
// should not be depended on, but they will always be time-ordered for easy
// comparison.
EDITION_2023 = 1000;
EDITION_2024 = 1001;
EDITION_2026 = 1002;
没有2025。数值是1000、1001、1002连续的,不是跳过了,而是压根没做过2025这个版本。(注释本身就说"数值本身是任意的,不要依赖,但总是按时间顺序排列",所以也没有理由去解读年份间隔的含义。)
protoc 从哪个版本开始接受 edition = "2026"
"Edition 2026 到底是从哪个发布版本开始的",靠code_generator.h里的一行代码就能确认。
// The maximum edition supported by protoc.
constexpr auto ProtocMaximumEdition() { return Edition::EDITION_2026; }
把这个值按发布标签一路追下去,边界就很清楚了。
v32.0 (2025-08-14) ProtocMaximumEdition() = EDITION_2024
v33.0 (2025-10-15) ProtocMaximumEdition() = EDITION_2024
v34.0 (2026-02-25) ProtocMaximumEdition() = EDITION_2024
v35.0 (2026-05-19) ProtocMaximumEdition() = EDITION_2024
v35.1 (2026-06-11) ProtocMaximumEdition() = EDITION_2024 <- 当前 stable
v36.0-rc1 (2026-07-09) ProtocMaximumEdition() = EDITION_2026 <- 从这里开始
也就是说,v36.0-rc1是第一个能在文件顶部写下edition = "2026"的 protoc。把2026的文件喂给更早的 protoc,会因为超出最大支持 Edition 而被拒绝。而且截至今天(2026-07-16),stable 仍然是v35.1,v36.0还只是 RC,不是 GA。
用同样的办法核实过去,也能确认这套流程的可信度。Edition 2024 的公告在2025年6月27日说会"在32.x、2025年第三季度"发布,v32.0在2025年8月14日发布,ProtocMaximumEdition()正是在那里升到了EDITION_2024。说到做到。这次的公告用的是同样的措辞 — "36.x、2026年第三季度" — RC也已经出来了。
默认值变化①:enforce_naming_style 变为 STYLE2026
descriptor.proto里的默认值表是这样的。
enum EnforceNamingStyle {
ENFORCE_NAMING_STYLE_UNKNOWN = 0;
STYLE2024 = 1;
STYLE_LEGACY = 2;
STYLE2026 = 3;
}
// edition_defaults:
// EDITION_LEGACY -> STYLE_LEGACY
// EDITION_2024 -> STYLE2024
// EDITION_2026 -> STYLE2026
公告是这样描述这条规则的 — 在有字段x的消息里,禁止出现has_x、set_x、get_x、clear_x、x_value,字段名也不能叫descriptor。
但读实际实现(descriptor.cc,2026-04-07提交)会发现,比公告更精细。被禁止的前缀是has_、get_、set_、clear_这四个,被禁止的后缀是_value这一个,但不是无条件禁止,而是有条件的。
if (absl::StartsWith(name, prefix)) {
absl::string_view without_prefix = name;
without_prefix.remove_prefix(prefix.size());
if ((message->FindFieldByName(without_prefix) != nullptr ||
message->FindOneofByName(without_prefix) != nullptr)) {
*error = absl::StrCat("should not begin with ", prefix,
" if a field named ", without_prefix,
" exists. This can cause collisions in "
"generated code.");
return false;
}
}
只有当同一个消息里真的存在对应的x时,has_x才会报错。如果没有x,一个字面上就叫has_x的字段完全可以通过。也就是说,这与其说是命名规则,不如说是冲突检查 — 它精准针对的是生成代码里has_x()访问器和has_x字段会撞上同一个符号的情形。而且在寻找对应项时,不仅查字段,还会查oneof 名(FindOneofByName),检查本身也同时作用于字段和 oneof。相比之下,descriptor则是无条件禁止。
选择退出分两档这一点,公告里也没写。源码里有两个错误消息常量。
constexpr absl::string_view kNamingStyleOptOutMessage =
" (features.enforce_naming_style = STYLE_LEGACY can be used to opt out of "
"this check)";
constexpr absl::string_view kNamingStyleCollisionsOptOutMessage =
" (features.enforce_naming_style = STYLE2024 can be used to opt out of "
"this check)";
降到STYLE2024,只会关掉2026新增的冲突检查,2024的风格检查仍然保留。要全部关掉,得降到STYLE_LEGACY。迁移时实际会用到的手段,大多是前者。
最后还有一个小而有趣的瑕疵。上面这个 enum 里,STYLE_LEGACY是2,STYLE2024是1,数值顺序和严格程度的顺序对不上。 因此,判断"这个风格是否达到某个严格程度以上"的辅助函数里,硬塞了一个特殊情况。
bool IsStyleOrGreater(const DescriptorT* descriptor,
FeatureSet::EnforceNamingStyle style) {
return internal::InternalFeatureHelper::GetFeatures(*descriptor)
.enforce_naming_style() >= style &&
// Required because STYLE_LEGACY comes after STYLE2024 in enum
// definition.
internal::InternalFeatureHelper::GetFeatures(*descriptor)
.enforce_naming_style() != FeatureSet::STYLE_LEGACY;
}
enum 值的顺序事后没法改,这是靠代码绕过去留下的痕迹。一个专门处理协议演进的项目,被自己 enum 的演进绊了一下 — 这种痕迹留下来,其实才是正常的。
默认值变化②:default_symbol_visibility 变为 STRICT
Edition 2024 引入了export/local关键字和default_symbol_visibility特性,当时的默认值是EXPORT_TOP_LEVEL — 顶层符号默认 export,嵌套符号默认 local。Edition 2026 把它提升到STRICT。
descriptor.proto里的注释原样说明了每个值的含义。
// Default pre-EDITION_2024, all UNSET visibility are export.
EXPORT_ALL = 1;
// All top-level symbols default to export, nested default to local.
EXPORT_TOP_LEVEL = 2;
// All symbols default to local.
LOCAL_ALL = 3;
// All symbols local by default. Nested types cannot be exported.
// With special case caveat for message { enum {} reserved 1 to max; }
// This is the recommended setting for new protos.
STRICT = 4;
STRICT 下会变的有三点 — 所有符号默认都是 local,export/local关键字只能用在顶层的 message/enum 声明上,给嵌套符号加上这两个关键字会是语法错误。这正是和LOCAL_ALL的区别所在。LOCAL_ALL只是把默认值改成 local,但仍然可以把嵌套类型重新标成export;STRICT 则把这个逃生口彻底堵死。
不过逃生口并没有完全消失,为了避免污染 C++ 命名空间而使用的一个惯用写法留了一个例外。当顶层 message 是local、并且所有字段都是 reserved 时,才允许嵌套的export enum。
local message MyNamespaceMessage {
export enum Enum {
MY_VAL = 1;
}
// Ensure no fields are added to the message.
reserved 1 to max;
}
意思是,只有当这个消息纯粹被当作命名空间的外壳来用 — 用reserved 1 to max;钉死"绝不会添加任何字段" — 才会被放过。条件卡得这么紧,读起来就是有意在防止它被当成一种通用的绕过手段。
这项变化给出的理由是1-1-1最佳实践 — 一个.proto文件只放一个消息,让别人只导入那一个文件,从而遏制依赖膨胀。至于这为什么会变成一个"大小"问题,正如符号可见性文档所解释的,protobuf 是按FileDescriptorSet/FileDescriptor为单位打包描述符数据的,所以一个文件里的全部定义,加上这个文件导入的一切,都会整个一起被带上。
默认值变化③:enforce_proto_limits — 这才是真正的变化
而这正是7月13日的公告里只字未提的功能。公告的"Changes to Existing Features"里只有前面这两项,"New Features"里也只有 enum 值的 JSON 自定义字符串、C++ 的namespace选项、C++ 自定义选项的拆分,以及 C# 的 nullable 引用类型。但v36.0-rc1的descriptor.proto里却有这个。
message ProtoLimitsFeature {
enum EnforceProtoLimits {
PROTO_LIMITS_UNKNOWN = 0;
// Default pre-EDITION_2026: there are no limit enforcement at the protoc
// level. Practical limits still exist, but they will tend to fail while
// compiling protoc-generated code, and these limits tend to be language
// or toolchain specific.
LEGACY_NO_EXPLICIT_LIMITS = 1;
// A set of limits enforced by Edition 2026 by default. For a detailed
// list of all the limits please consult the Edition 2026 documentation.
PROTO_LIMITS2026 = 2;
}
}
那段注释的第二句话,就是这个功能存在的全部理由。上限一直都存在。 只是 protoc 不知道,所以失败总是在莫名其妙的地方冒出来。descriptor.h里的注释说得更直白。
// Enforce protobuf limits at the descriptor level. Pre Edition 2026, there
// is no limit enforcement in the protobuf compiler when parsing proto files.
// As a result, it is possible to write a protobuf file that compiles in
// protoc, and code generation works as intended, but the code generation
// output is uncompilable because it runs into language-specific or
// compiler-specific limits.
//
// Starting in Edition 2026, certain limits will start to be enforced. The
// goal of this enforcement is to help ensure that any file that is accepted
// by the protobuf compiler will be compilable on standard tool chains.
void EnforceProtoLimits(bool enforce) { enforce_proto_limits_ = enforce; }
"在 protoc 里能编译通过,代码生成也按预期运作,但生成出来的代码本身编译不了"的情形。真正大规模用过 protobuf 的人都懂这句话在说什么 — 不停地往 enum 里加值,某天 javac 突然吐出和常量有关的报错,而你得花上好一阵子才意识到,罪魁祸首是自己的.proto文件。
回头看看这些上限过去是怎么被管理的,会更有意思。protobuf.dev 上有一份叫Proto Limits的文档,那份文档是这样自我介绍的。
This information is a collection of discovered limitations by many engineers, but is not exhaustive and may be incorrect/outdated in some areas.
也就是说,这是许多工程师踩坑之后发现、汇总起来的东西,文档自己也承认既不完整,某些地方可能是错的或过时的。里面的数字实际上是这个调调 — 每条消息的字段上限是65,535,但像 Boolean 这种只有单一字段的消息,会在"~2100个(proto2)"、"~3100个(proto3,不用 optional)"处崩溃,enum 则是"最低的上限出现在 Java,约~1700个"。带着波浪号的民间经验。
Edition 2026 把这些民间经验变成了 protoc 强制执行的数字。descriptor.h里的常量是这样的。
// Edition 2026 introduces default limits for proto files as the descriptor gets
// built from protoc. To opt out of these limits for edition 2026, you may use
// features.enforce_proto_limits = LEGACY_NO_EXPLICIT_LIMITS.
inline constexpr int kLimit2026FieldsPerMessage = 1500;
inline constexpr int kLimit2026OneofsPerMessage = 1000;
inline constexpr int kLimit2026FieldsPerOneof = 1200;
inline constexpr int kLimit2026ValuesPerEnum = 1700;
这四个数字在三个地方是一致的 — 上面这份头文件常量、引入这个功能的提交信息(2026-05-14),以及Feature Settings for Editions 文档里enforce_proto_limits那一节。三处的值完全相同。
Edition 2024 及以下 Edition 2026
每条消息的字段数 无限制(以 protoc 为准) 1500
每条消息的 oneof 数 无限制 1000
每个 oneof 的字段数 无限制 1200
每个 enum 的值数 无限制 1700
这里最显眼的是 enum 的1700,正好和 Proto Limits 文档里"Java约~1700"这个数字对上。字段的1500也比文档里的"~2100(proto2)"设得更低。也就是说,这些新上限看起来不是随便定的整数,而是划在实际崩溃点内侧的一条线。(不过,protobuf 这边有没有公开文档说明选定这些数字的依据,在我核实的范围内没有找到。能确认的只是数字对得上这件事,因果关系是我的推测。)
还有一处实务上会挨疼的地方。这个选择退出没法按文件级别设置。 看descriptor.proto里这个功能的targets,只有 ENUM、MESSAGE、FIELD、ONEOF,没有 FILE。
optional ProtoLimitsFeature.EnforceProtoLimits enforce_proto_limits = 9 [
retention = RETENTION_SOURCE,
targets = TARGET_TYPE_ENUM,
targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_FIELD,
targets = TARGET_TYPE_ONEOF,
feature_support = {
edition_introduced: EDITION_2026
},
...
];
Feature Settings 文档也用代码注释说了同一件事 — "Must be set at the level of the oversized message, enum, or oneof as this feature is not allowed at the file level."没法在文件最上面写一行就整体关掉,得一个个找出体积过大的消息、enum、oneof,逐个单独加上覆盖。把庞大的遗留 schema 迁移到 Edition 2026 时,这就直接变成了工作量。
再补一点。上面feature_support里的edition_introduced: EDITION_2026意味着,这个功能在 Edition 2026 之前根本没法配置。 前面两个功能都是edition_introduced: EDITION_2024,所以在 Edition 2024 的文件里也能动手;但大小上限检查只有升到2026才能碰到。没有提前打开、提前准备的途径。
检查由 protoc 打开,运行时是关闭的
这三项检查并不是一直在跑。看command_line_interface.cc会发现,是 protoc CLI 显式打开的。
descriptor_pool->EnforceSymbolVisibility(true);
descriptor_pool->EnforceNamingStyle(true);
descriptor_pool->EnforceProtoLimits(true);
而DescriptorPool的成员默认值是关闭的 — descriptor.h里把enforce_proto_limits_ = false;、enforce_symbol_visibility_ = false;作为初始化值。也就是说,在运行时直接构建描述符池的代码(比如动态加载 schema),不会自动受到这些检查的约束。这是合理的 — 对一个已经通过 protoc 检验、已经存在的描述符,在运行时再拒绝一次,没有任何好处。
而且这三个功能全部都是retention = RETENTION_SOURCE。只在源码层面有意义,不会带进生成出来的描述符里。这是编译期的 lint,不是运行时行为。
这里有一点必须强调 — 线格式(wire format)没有任何变化。 Edition 从来没有动过编码方式,Edition 2026 也不例外。靠字段编号和 wire type 运转的字节流,原封不动。本文里的任何内容,都不会影响已经部署的服务之间的兼容性。
新引入的东西(简述)
公告在"New Features"里列出的这些,大多是各语言绑定自己的事。
- enum 值的 JSON 自定义字符串。 如果说原有的
json_name改的是键名,那这次改的是值本身 —FOO_BAR = 5 [(pb.enumvalue.json).string = "custom_string_here"];。公告给出的理由是规避冲突、辅助迁移、满足外部要求。 - C++ 的
namespace选项。option (pb.file.cpp).namespace = "clock_time";这样的写法,可以让生成绑定的命名空间独立于.proto包名单独指定。 - C++ 自定义选项挪出
descriptor.proto。 C++ 专属选项被拆到独立文件,通过import option "google/protobuf/cpp_features.proto";引入。这一节里还混进了repeated_type = PROXY和RepeatedFieldProxy的内容,但公告 markdown 里的代码围栏是坏的,光看原文很难判断这段内容到底属于哪一节。 - C# 的 nullable 引用类型。 opt-in,
v36.0-rc1的发布说明里也有对应条目 — "Advertise that C# supports Edition 2026, and move C# Nullable Reference Type support into that edition."
诚实的权衡与局限
目前还不是 GA。 截至今天,stable 是v35.1,v36.0是 rc1。公告自己也这样说明。
These describe changes as we anticipate them being implemented, but due to the flexible nature of software some of these changes may not land or may vary from how they are described in this topic.
"可能落不了地,也可能和这里描述的不一样。"本文里的每一个数字和行为,都是基于v36.0-rc1这个标签,GA 时都可能变。
公告和代码对不上。 enforce_proto_limits在代码里,也在文档(Feature Settings)里,唯独不在7月13日的公告里。只看公告来判断 Edition 2026,会把影响最大的一项变化整个漏掉。反过来,Feature Settings 文档里enforce_proto_limits的示例写着"要保留一个过大的消息需要覆盖设置",给出的代码却是设置PROTO_LIMITS2026(是打开,不是关闭)。文档刚写出来,还很粗糙。规范性的事实,还是去descriptor.proto的targets/edition_defaults里确认更保险。
语言支持是分开跟进的。 这里写的大部分是 protoc 和 C++ 运行时的事。各语言运行时得先宣布支持 Edition 2026,才能真正用上(这正是 C# 那一项要在发布说明里单独出现的原因),第三方实现或不走 protoc 的工具链,是完全另一回事。
收益不是看得见的那种。 这三个功能没有一个能让消息变小或解析变快。全都是编译期的 lint。得到的是"本该在后面某个奇怪地方崩溃的东西,现在提前变成一个清楚的报错",这件事对有的团队值钱,对有的团队不值钱。
那么,现在该做什么
大多数情况下,现在什么都不用做。 如果你用的是syntax = "proto2"/syntax = "proto3",Edition 2026 今天和你没有任何关系。在descriptor.proto的默认值表里,proto2/proto3/Edition 2023 这三个功能全都停在遗留值(STYLE_LEGACY、EXPORT_ALL、LEGACY_NO_EXPLICIT_LIMITS),只要不升级 Edition 就一直如此。没有理由着急。
如果已经在用 Edition 2024、并且打算看看2026,顺序大致该这样排。命名冲突检查可以先用STYLE2024提前清理,可见性也可以先用显式的export/local标注理顺。但大小上限,正如前面所说,2026之前没有办法提前试,所以 schema 里有没有超过1500字段的消息、超过1700值的 enum,只能自己数一遍。如果有,那多半会是迁移成本的大头 — 因为没法按文件级别关掉。
如果没有理由升到 Edition 2026、却只想要这些检查,多半没有办法,就算有也是局部的。这是 Edition 这套设计有意造成的结果 — 把一捆默认值按年份打包出售,正是 Edition 的本质。
反过来,什么情况下值得用,答案很清楚。schema 很大、要生成多种语言、经历过"为什么偏偏 Java 构建会崩"的组织。对这样的团队来说,protoc 一旦接受了 schema,就能保证它在标准工具链上编译通过,这件事相当值钱。descriptor.h注释定下的目标,正是这个 — "any file that is accepted by the protobuf compiler will be compilable on standard tool chains."
结语
Edition 2026 不是一次华丽的发布。新增语法为零,线格式变化为零,性能提升也为零。取而代之的是收紧三个默认值 — 禁止命名冲突,把符号默认设为 local,第一次给 schema 大小划下一条线。
而这正是 Edition 这套机制本来就想做的事。在 proto2/proto3 的年代,就算知道"这个默认值不对",也没有办法改 — 一改,全世界的.proto就会跟着崩。Edition 把默认值绑定到年份上,让旧文件留在旧默认值里,新文件从更好的默认值开始,开出了一条路。Edition 2026 差不多是这台机器真正转起来的第一个案例 — 一次三个,各自带着选择退出,同时不碰任何一个既有文件。
我最喜欢的是大小上限那部分。原本写在一份 wiki 式文档里、带着波浪号的数字 — "许多工程师发现的、不完整、也可能有误" — 变成了编译器强制执行的四个常量。民间经验变成规范的那一刻,通常悄无声息,通常就是这个样子。
不过截至今天,这仍然只是一个 RC,公告漏掉了自己的一项功能,文档示例里也有 bug。要确认,就去descriptor.proto里看。不是看公告。
参考资料
- Changes Announced on July 13, 2026 — Edition 2026 公告(原始 Markdown)
- descriptor.proto @ v36.0-rc1 — Edition enum,以及
enforce_naming_style、default_symbol_visibility、enforce_proto_limits的默认值表 - descriptor.h @ v36.0-rc1 —
kLimit2026*常量与EnforceProtoLimits的注释 - code_generator.h @ v36.0-rc1 —
ProtocMaximumEdition() - Feature Settings for Editions — 各功能按 Edition 划分的默认值表
- Symbol Visibility — STRICT 的精确规则与例外
- Proto Limits — Edition 2026 之前的"已发现上限"文档
- Protocol Buffers v36.0-rc1 发布(2026-07-09)
- Changes Announced on June 27, 2025 — Edition 2024 公告(用于对比履约记录)
- gRPC 与 Protocol Buffers 基础(相关文章)
- 二进制序列化格式比较 — protobuf/thrift/avro/flatbuffers/capnproto(相关文章)