
先问一个问题你上一次在代码里看到一个“没用的功能”是什么反应大概率是两种要么顺手删掉因为看起来没人用留着还占维护成本要么在代码评审里多写一句“这个逻辑是多余的”然后让它继续留着因为没人敢确定删了会不会出事。这其实是很多开发团队每天都在发生的事情。所谓“没用的功能”很少是真正没用的更多时候只是它的使用场景已经不在你的视野里。它可能是在某个版本里为特定用户设计的可能是因为某个历史需求留下的兼容逻辑也可能只是当时的设计人没有把上下文写清楚。当你看到它的时候它看起来就是一行“无用代码”但真正的风险恰恰藏在“看起来没用”这个判断里。这篇文章要讨论的就是这件事如何判断一个功能是真的没用还是只是没有被你用起来。我会先给一个判断框架然后从 Git、构建工具、数据库、接口设计、后端代码几个具体场景讲清楚“没用”背后的真实逻辑再给出可执行的排查方式。如果你正在做代码评审、负责维护老项目或者被产品需求里“这个功能没人用”的判断困扰过这篇内容应该能给你一些新的思考角度。1. 为什么“没用的功能”越来越多先承认一个事实任何项目里都会积累大量低使用率的功能。这不是某个团队执行力的问题而是软件系统演进的必然结果。一个系统在初期通常很简单核心功能就那么几个。但随着业务扩展、客户定制、技术方案迭代系统里会不断加入新功能。每个新功能在上线那一刻都是有理由存在的。真正的问题在于很多功能在完成它的历史使命之后并不会被主动清理而是继续留在代码里成为“看起来没用”的功能。这里面有几类典型来源。第一类是面向特定客户或特定场景的定制功能。比如某个大客户提了一个特殊需求开发团队在主干代码里加了一个开关。客户那边用了半年觉得不需要了但代码里的开关还留着。后来维护的人根本不知道这个开关是给谁用的只觉得它没用。第二类是技术演进之后留下的兼容层。比如系统从旧接口迁移到新接口为了保证旧客户端能继续使用保留了一套兼容逻辑。等到旧客户端全部下线之后兼容逻辑还在但它已经没有触发条件了。从代码层面看这就是“死代码”但从历史角度看它是曾经不可或缺的一部分。第三类是产品决策中的“防御性功能”。有些功能上线时就不是给普通用户用的而是为了应对某些极端情况、合规要求或者运营需要。这类功能平时根本看不见入口只有在特定条件下才会被触发所以大多数开发者会觉得它没用。第四类是纯粹的“拍脑袋功能”。产品经理说这个功能可能有用户需要开发就做了。上线后发现根本没用户用但它也不会自己消失就那样默默存在了好几年。这四类情况除了第四类真的属于资源浪费之外前三类都有一个共同特点它们不是没用而是“只在特定条件下有用”。当那个特定条件不再出现在你的认知范围里你就会把它归为“没用”。这就是“没用的功能”越来越多的本质原因系统的复杂度增长之后没有任何一个人能完整记住所有功能的设计背景。于是那些对于当前业务没有明显价值、但历史上有存在理由的功能就会在代码里被标记为“没用”。理解了这一点再去看“没用的功能”你看到的不再是代码质量问题而是系统演进中的信息丢失问题。2. 判断一个功能有没有用的三层模型既然“没用”往往只是认知偏差那么有没有一套标准来判断一个功能到底是不是真没用我建议用一个三层模型来判断功能价值、场景匹配、维护成本。第一层功能本身有没有价值。这是最基础的判断。如果一个功能解决了一个真实存在的问题哪怕这个问题很罕见它也是有价值的。比如一个接口支持了错误重试虽然大多数时候不会触发但一旦触发就能避免一次线上事故。这类功能就是有价值的功能。第二层当前场景是否需要它。这是最容易产生分歧的地方。功能有价值不代表它应该在每一个项目里都存在。一个为金融系统设计的对账功能放在一个内容管理项目里就是多余的。这并不意味着对账功能本身没用而是它不匹配当前场景。第三层维护成本是否可接受。有些功能虽然有价值、也匹配场景但维护成本太高比如它依赖的底层库已经不再更新、它的代码实现过于复杂导致没人敢动、它占用的资源远超它的实际收益。这种情况下删除它反而是更好的选择。把这三层组合起来可以得到一个简单的判断表功能价值场景匹配维护成本判断结论有价值匹配低核心功能保留并持续迭代有价值匹配高值得保留但需要重构降成本有价值不匹配低可以保留作为备选能力有价值不匹配高谨慎评估建议下线无价值匹配低重新审视可能判断错价值无价值不匹配高真没用可以删除这个模型看起来简单但实际用起来最关键的是第一层和第二层的数据从哪来。很多团队判断一个功能没用仅仅依据“没有用户反馈”或者“没有日志记录”这其实是不够充分的。比如一个功能日志比较少可能不是因为它没人用而是因为它的关键路径上没有埋点一个功能没有用户反馈可能不是因为它没有用户而是因为用户遇到问题就绕道走了根本没有反馈通道。所以在做判断之前先要搞清楚三个问题这个功能当初为什么被做出来它当前的触发条件是什么如果删除它会影响哪些调用方这三个问题都答不上来的时候不要轻易下结论说它没用。3. 那些“看起来没用但在关键时刻很有用”的功能说完了判断模型我们看几个具体例子。这些例子都是开发工作中常见的功能很多团队会觉得它们没用甚至已经把它们删掉了但实际它们在特定场景下价值巨大。3.1 Git 历史追踪git log --follow很多开发者对 Git 的日常使用停留在 add、commit、push、pull 这几个命令上。遇到“git log --follow”这类参数第一反应是“我用不上”。但实际上这个参数在排查历史文件迁移问题时非常有用。场景是这样的你把一个工具类从一个包移动到另一个包顺手改了几行代码。后来发现某个函数的行为和预期不一致想看看这个函数从诞生到现在的完整修改记录。如果使用普通的 git log只能看到移动之后的新文件历史加上 --follow 参数Git 会尝试追踪文件重命名之前的记录一层一层地追回去。# 查看某个文件从诞生到现在的完整提交历史 git log --follow --oneline -- src/main/java/com/example/util/HttpUtils.java这个功能看起来很“冷门”但在做代码追责、问题定位、老接口兼容性分析时它就是救命稻草。尤其是当历史版本代码经过多次重构之后没有这种追踪能力你根本说不清楚某段逻辑是什么时候加进去的、为了什么原因加的。如果只盯着“当前代码能不能跑”这一件事git log --follow 确实没多大用。但如果你需要理解代码的演变过程、需要判断“这段逻辑能不能删”它就是最直接的证据来源。3.2 构建工具的 --dry-run 参数在发布流程里很多人对 dry-run 这种参数不太重视。尤其是前端项目习惯性地执行 npm run build 之后直接部署觉得干跑一遍纯属浪费时间。但 dry-run 解决的是“构建能成功但我不知道它会改什么”的问题。在持续集成中构建产物会覆盖旧的产物如果某一次构建生成了异常文件或者把配置文件里的变量替换成了错的值你根本不会在构建过程中发现。# 示例构建前先查看打包过程和产物情况 npm run build -- --dry-run # 或者查看某个依赖的版本解析结果 npm explain some-package实际的部署流程中更推荐先跑一遍干净的构建命令检查构建日志里有没有异常告警再执行部署。对于后端项目Maven 的 dry-run 能力也类似可以提前验证依赖解析、插件执行是否正常不需要真的打出一个可运行的 jar。这类功能的问题在于它在 99% 的情况下确实没有输出什么有用的信息所以大家会认为它没用。但正是那 1% 的异常情况能帮你避免一次线上事故。用一次几次的时间成本换一次重大故障的规避这个账是划算的。3.3 代码里的防御性判断后端开发里经常会看到这样的代码public User getUserById(Long userId) { if (userId null) { return null; } return userMapper.selectById(userId); }很多刚入门的人看到这里的判空会觉得这是多余的。因为调用方传参之前已经做了非空校验userId 根本不可能为 null那这一行判断不是没用是什么但实际情况是接口可能被新的调用方直接调用新的调用方可能没有做同样的非空校验接口可能被反射调用参数可能被绕过数据库查询可能返回异常userId 又恰好被上层传成了 null。任何一个环节出了问题这一行判断都能把错误挡在入口处而不是让它变成一个 500 错误。防御性判断的真正价值不是处理“会发生的情况”而是处理“你以为不会发生、但真的发生了会很难排查的情况”。什么情况下防御性判断才是真的没用当它保护的对象已经被语言或者框架的约束机制牢牢限制住的时候。比如 Kotlin 的非空类型已经帮你保证了传参不会为 null那这种手工判空才是真多余。不同技术栈、不同版本、不同调用场景需要单独判断不能一刀切。3.4 慢查询日志数据库的慢查询日志很多小团队的认知是“开了也白开”。平时业务量不大查询都很快日志文件却一直在增长看起来就是在浪费磁盘。但慢查询日志存在的原因不是记录“已经慢的查询”而是记录“逐渐变慢的查询”。SQL 的执行计划会随着数据量增长而变化索引可能因为数据分布变化而失效一张表的数据量从 10 万涨到 1000 万同一句 SQL 的耗时可能从 50ms 变成 5s。如果没有慢查询日志这个变化过程是隐藏的等到用户投诉了才发现问题往往已经很难处理。-- 查看 MySQL 慢查询日志是否开启 SHOW VARIABLES LIKE slow_query_log; -- 设置慢查询阈值单位是秒 SET GLOBAL long_query_time 1;慢查询日志是那些“十次里有九次没有用、但一次有用就值得”的功能的典型代表。它更像是系统的监控体系而不是一个业务功能。当你的项目还没有监控系统、还没有全链路追踪的时候慢查询日志几乎是你唯一能依赖的性能判断依据。3.5 CHANGELOG 与版本说明还有一个容易被忽略的“没用的功能”CHANGELOG。很多内部项目根本没有 CHANGELOG版本发布记录靠微信群里的几句聊天。一旦版本迭代超过一年再想查“这个接口为什么从 v2 改成了 v3”就完全找不到依据了。CHANGELOG 看起来不产生任何代码价值但它记录了功能的变化原因。当你在评审代码时看到一段“没用”的代码第一件事就是查它是什么版本引入的当时的 CHANGELOG 里有没有写原因。如果写了你就能很快判断它的使用场景如果没写你就只能在代码注释和 Git 历史里摸索。一个轻量的 CHANGELOG 不必复杂Markdown 格式就够用# Changelog ## v1.4.0 - 2024-05-12 ### Added - 新增用户批量导入接口 /users/batch ### Changed - 调整用户搜索接口的分页逻辑由偏移量改为游标分页 ### Removed - 下线旧版导出接口 /users/export总结一下这些“看起来没用”的功能你会发现一个规律它们都不是用户直接感知的功能而是支撑系统长期健康运行的能力。它们没有一个能直接带来收入但删除它们之后系统会逐渐失去可追溯性、可观测性和容错能力。这些问题不会马上爆发但会在某个意外时刻集中体现。4. 实战用三个命令快速检验一个功能是否“真没用”前面讲了大量概念这一节给出一套可执行的验证方法。当你对某个功能产生“这功能是不是没用”的疑问时不用靠感觉判断按下面三个步骤做一次技术体检。4.1 查使用频率第一步先确认这个功能到底有没有被调用。对于后端接口可以查接口日志看调用量、调用来源、最近一次调用时间。对于前端功能可以查埋点数据或者用户操作日志。如果项目里没有这些数据可以先加上访问日志观察一到两个版本周期。# 例统计某个接口最近30天的调用量 grep POST /api/v1/users/export app-access.log | wc -l这里要提醒一点不要只看调用量绝对值。如果一次性导入导出工具每天调用一次也算正常使用如果新的用户增长根本不经过这个接口那调用量下降才是真实信号。4.2 查设计背景第二步回到代码仓库查这个功能是什么时候进来的、提交信息里有没有说明原因。# 查看某个功能的最近提交记录 git log --oneline -- src/main/java/com/example/service/OldExportService.java # 查看某个文件的完整提交历史包括重命名之前的记录 git log --follow --oneline -- src/main/java/com/example/service/OldExportService.java如果提交信息里写了“为 XX 客户新增个性化导出格式”你就能直接定位到需求方可以进一步确认这个需求是否仍然有效。如果提交信息只有“add code”这种无意义内容那这个功能的背景就只能靠问人了。4.3 查依赖关系第三步看这个功能被哪些上层模块依赖。这一步最关键因为很多功能虽然入口没人用但它可能是某个公共模块的一部分被其他功能间接依赖着。# 用 IDE 的 Find Usages 功能或者使用 grep 搜索引用 grep -r OldExportService --include*.java src/如果搜索结果为空这个类就是典型的“顶级无引用类”。但注意在动态加载、反射、Spring 配置文件引入类的场景下静态搜索会漏掉真实的依赖所以需要结合项目使用的技术栈来判断。这套验证方法做下来通常会有三种结果第一种功能有明确的调用方和使用场景只是使用频率低。这种情况建议保留但可以加监控定期观察它是不是继续被使用。第二种功能已经没有任何调用方也没有运行时依赖。这种情况可以考虑删除但删除之前要确认它的数据表、配置项、缓存 key 是否也被一起清理干净。第三种功能没有直接调用方但存在潜在调用场景。这种情况最麻烦比如某个工具函数被定时任务反射调用静态搜索看不到关联实际上每天都会执行。这时候应该先补上调用记录再判断。5. 删除一个“没用”功能的安全姿势经过验证如果一个功能确实已经失去价值删除是合理的。但删除功能并不是删一行代码那么简单涉及数据、配置、接口、文档、监控等多个层面。这里给出一份完整的下线检查清单适合在生产环境改造前做评审参考。检查项说明建议调用方排查接口、页面、定时任务、消息队列是否还在调用先停新流量再逐步关停数据表处理功能对应的数据表是否还有存量数据需要迁移先备份再决定保留或删除配置项清理功能对应配置项是否还需要启用先下线再删除外围依赖是否被监控、告警、报表、运营工具依赖先确认无引用再清理文档更新接口文档、部署文档、架构图是否需要同步修改在变更单中一并处理上线方案是否需要灰度、回滚计划是否明确分阶段执行避免一步到位更重要的是流程管控。在团队协作中删除功能不应该是一个人拍脑袋的决定而是要有一个轻量级的“功能下线提案”。提案里写清楚三件事这个功能现在处于什么状态、删除的依据是什么、出现什么情况需要回滚。下面是一个可复用的模板## 功能下线申请 功能名称旧版用户导出接口 代码位置/users/export 删除原因 - 该接口最近 180 天无外部调用 - 新版导出接口已全量替代 - 相关数据表无存量业务数据 删除风险 - 有外部对接方可能直接调用旧接口需提前 1 周通知 回滚方案 - 代码回滚 恢复接口路由 验证方式 - 发布后查看网关日志确认无旧接口调用记录有了这样的流程即使后来发现误删了也可以通过回滚方案快速恢复不会让一个“下线决定”变成一次故障。6. 从“没用的功能”看产品与工程师的判断差异虽然这是技术博客但“没用的功能”这个问题其实不只是技术问题。产品经理和工程师对“有用”的定义经常不一致这种不一致会产生大量的沟通成本。在很多产品经理看来一个功能只要有人用就是有用的哪怕只有 1% 的用户在用也是有存在价值的。但工程师的视角通常更偏向维护成本一个功能只有 1% 的用户用但每次版本升级都要为它做兼容、做回归测试那这个功能的成本就远超收益。这种分歧没有一个绝对正确的答案。真正有效的做法是建立统一的“功能价值评估标准”让产品经理和工程师用同一套指标来衡量功能去留。指标可以包括使用频率、维护成本、替代方案、业务风险。比如一个后台管理功能只有运营人员会用使用频率很低但它承担着关键的运营动作一旦下线运营就没办法做事。这时候即使使用频率低也不能只从技术成本角度说它没用。反过来如果一个功能使用频率低、也有现成替代方案、维护成本却很高那就应该推动产品经理一起做下线决策。工程师不能只从代码角度反对也不能只从代码角度决定删除而是要拿出数据和影响面分析让决策有依据。这个能力是资深工程师和新手工程师的明显差异之一。新手看到“没用”的代码会想删或者想骂资深工程师会先问“它为什么存在删了之后会发生什么”。7. 团队如何积累“功能设计背景”前面几节反复提到“设计背景”这个词。为什么要强调它因为大部分功能之所以会被误判为没用不是因为功能真的没用而是因为设计背景丢失了。一个功能在上线时设计者心里非常清楚它给谁用、解决什么问题、为什么这么设计。但当这个设计者离职、转岗、或者只是离开这个模块半年之后这些信息就跟着记忆消失了。后来接手的人面对代码时只能看到“有这个功能”看不到“这个功能承载了什么决策”。所以真正要解决“没用的功能介绍”这篇文章里描述的问题团队最需要做的不是让每个人都能删功能而是建立一个持续记录功能背景的机制。具体做法有三个。第一代码注释里写“为什么”而不是写“是什么”。很多团队要求代码必须有注释但写出来的全是“获取用户信息”这种废话。真正的注释要回答“为什么这里要判空”“为什么这个方法要加锁”“为什么要兼容这个异常输入”。如果每段“没用”的逻辑旁边都有一句原因说明后续维护的人就不会轻易删错。第二需求文档和代码建立索引。当一个需求实现了可以把需求单号写在提交信息里。这样后来者通过 git 提交信息就能直接找到对应的需求文档。很多团队已经有这个习惯但执行得不够彻底。第三定期做“功能地图”复盘。每半年花半天时间一起梳理当前系统有哪些功能、各自的使用情况、是否还需要保留。这个动作不需要特别正式也不一定要产出精美的文档关键是让主持人和参与者都重新过一遍系统的全貌把那些“自己负责的模块里有什么”重新盘一遍往往能发现很多被遗忘的角落。功能背景的维护本质上是一种知识管理。它不能直接提升系统的并发能力也不能降低接口延迟但它能防止团队在“不知道自己在删什么”的情况下破坏系统。8. 常见误区与排查方法围绕“没用的功能”这个话题团队里经常会出现一些误区。下面把这些误区整理成一张表格每一项都给出排查方式和正确的处理思路。常见误区可能原因排查方式正确处理看到没用的代码就顺手删掉误以为自己的视野覆盖了所有场景先查 Git 历史、调用方和依赖关系按第 4 节的三步验证法确认后再删除认为没人反馈就是没人用用户没找到反馈入口或者问题不严重不值得反馈查日志、查埋点、和一线支持人员沟通用数据说话不依赖感觉删除功能是纯代码工作忽略了配置、数据、文档、监控等多层依赖用第 5 节的下线检查清单逐项核对按功能下线流程执行并准备回滚功能使用率低就等于没价值把使用频率当成唯一指标查看该功能的业务影响和替代方案用功能价值三层模型评估老功能留着总比删了好害怕删除后的未知风险评估维护成本和潜在故障风险无法确认风险的功能加监控观察而不是放任不管新员工说没用就允许删新员工没有完整的系统上下文由熟悉历史的同事交叉验证建立代码评审必须检查“删除类变更”的制度这里单独展开说最后一条。代码评审时删除类变更往往被轻视。因为删除代码的 diff 看起来很少几行红代码评审人扫一眼就通过了。但删除代码的影响往往比新增代码更隐蔽因为代码一旦被删依赖它的功能就会在运行时突然失败这种失败不会在编译期暴露。最稳妥的做法是针对删除类变更单独做一个评审环节至少要回答下面几个问题这段代码为什么被删它的调用方怎么处理有没有兼容策略回滚方案是什么如果这些问题都答不上来那就说明删除条件还不成熟。9. 总结别急着说“没用”回到最开始的问题。为什么“没用的功能介绍”这个系列能一直写下来因为“没用”是一个高度依赖语境的判断。同一个功能在开发者手里可能确实用不上在运营手里可能每天都要用在一个项目里是死代码在另一个项目里是核心能力。所以当你下一次看到一段“没用”的代码时不要急着删也不要急着怼。先用三五分钟做一次技术体检查一下它的调用情况翻一下它提交历史里的上下文看看它到底为什么存在。如果确认它已经没有存在理由再按照下线的流程把它安全地删除。真正有价值的代码维护能力不只是写出优雅的新代码也包括理解旧代码为什么会变成今天这个样子。理解了这一点“没用的功能”反而会变成你最珍贵的信息来源。