禅与计算机 禅与计算机
首页
  • Java基础

    • 一文搞懂Java核心技术
    • Java面向对象知识点大总结,建议收藏
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
    • Java异常:从原理到实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • AI时代专注力培养
    • 如何阅读一本书:技术书籍的读书笔记方法论
  • 开发工具

    • IDEA配置详解与高效使用指南
    • Windows环境下JDK安装与环境变量配置
    • Windows 10 下的 Maven 安装配置教程
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
    • 感觉你因为AI退化了?用Redis SCAN源码打脸AI时代的孔乙己
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • Elasticsearch核心原理与架构设计
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • AI时代的Go语言基础语法认知
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
    • 基于 Claude Code 复刻 Redis 慢查询指令实践
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
    • 对账核销场景设计与重构实践
    • 千万级交易流水慢查询综合治理实践
    • 记一次StarRocks源码级排错和既有架构优化实践
    • 基于快照合并修复Seata AT回滚补偿与Flink批攒导致StarRocks数据不一致最佳实践
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
  • AI工具链

    • Claude Code 实战指南:从安装配置到企业级开发流程
    • 一次 Claude Code 启动失败的 AI 辅助排查复盘
    • 基于提示词工程与KITE框架的Redis签到功能开发实践
    • Claude Code 记忆管理:CLAUDE.md 最佳实践
    • Claude Code 规则管理:Rules 拆分编排与迭代实践(文末送书)
    • VSCode与Claude Code后端开发环境搭建与AI编程工作流实践
    • AI 写的企业级组件不敢用?我替你验过了
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 一文搞懂Java核心技术
    • Java面向对象知识点大总结,建议收藏
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
    • Java异常:从原理到实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • AI时代专注力培养
    • 如何阅读一本书:技术书籍的读书笔记方法论
  • 开发工具

    • IDEA配置详解与高效使用指南
    • Windows环境下JDK安装与环境变量配置
    • Windows 10 下的 Maven 安装配置教程
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
    • 感觉你因为AI退化了?用Redis SCAN源码打脸AI时代的孔乙己
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • Elasticsearch核心原理与架构设计
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • AI时代的Go语言基础语法认知
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
    • 基于 Claude Code 复刻 Redis 慢查询指令实践
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
    • 对账核销场景设计与重构实践
    • 千万级交易流水慢查询综合治理实践
    • 记一次StarRocks源码级排错和既有架构优化实践
    • 基于快照合并修复Seata AT回滚补偿与Flink批攒导致StarRocks数据不一致最佳实践
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
  • AI工具链

    • Claude Code 实战指南:从安装配置到企业级开发流程
    • 一次 Claude Code 启动失败的 AI 辅助排查复盘
    • 基于提示词工程与KITE框架的Redis签到功能开发实践
    • Claude Code 记忆管理:CLAUDE.md 最佳实践
    • Claude Code 规则管理:Rules 拆分编排与迭代实践(文末送书)
    • VSCode与Claude Code后端开发环境搭建与AI编程工作流实践
    • AI 写的企业级组件不敢用?我替你验过了
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Go基础

  • mini-redis实战

  • Go面试题

    • Go语言常见面试题解析(上)语言基础与核心概念
      • 前言
      • go语言基础
        • 为什么选择Go语言
        • go语言和Java语言的区别
        • 语言设计
        • 应用生态
        • 性能表现
        • GoRoot 和 GoPath 有什么用
        • 介绍一下 defer关键字作用
        • 多个 defer 的顺序
        • defer 在什么时机会修改返回值
        • go出现panic的场景
        • go是否支持while循环,如何实现这种机制
        • Go 允许多个返回值吗
        • 写go单元测试的规范
        • Go 有异常类型吗?为什么?
        • 函数、方法的区别
        • 具名函数
        • 匿名函数
        • 二者区别
        • 如何理解Go语言中的方法
        • 切片传参是引用传参吗
      • Go 编译相关的命令详解
        • go编译常见指令有哪些?
        • Go 编译链接过程概述
      • 基本数据类型
        • uint8 类型溢出问题
        • 能介绍下 rune 类型吗
        • 单引号,双引号,反引号的区别?
        • 如何高效地拼接字符串
        • 如何交换 2 个变量的值
        • go里面的int和int32是同一个概念吗
      • 参考
  • Go语言
  • Go面试题
sharkchili
2026-05-20
目录

Go语言常见面试题解析(上)语言基础与核心概念

# 前言

整理这篇面试题的时候,笔者也是感慨万千。随着 AI 的飞速发展,知识获取的效率被极大提升,AI已经可以很好的解决传统编程模式下的基础应用层面上的问题,例如:

  • 基础语法
  • 标准库用法
  • 常见陷阱

所以,只要能够准确表述出自己的意图,AI是可以非常高效且准确的生成对应的指令编码,但这并不意味着,现代研发者无需关注语言基础,笔者认为AI是提升个人研发效率的有利工具,只有深厚的基础知识才能支撑和AI的一切推理和决策。

在当前时代,最重要的就是面对一个技术问题时,我们要学会从"是什么"穿透到"为什么这样设计"以及"在什么场景下该做什么取舍"。比如:

  • Go 为什么舍弃继承而选择组合?
  • goroutine 的调度模型为什么这样设计?

这些问题的答案背后,体现的是一种技术决策的思维方式:先理解约束条件,再分析设计动机,最后推导出合理的技术选型:

所以,笔者在整理这篇面经时,尝试建立这样一个分析框架:每个知识点不仅回答"是什么",更着重拆解背后的"为什么——即从设计哲学、工程权衡、场景适配三个维度来阐述问题。

希望读者在掌握基础的同时,能够逐步建立起一套属于自己的技术推理方法论,以应对更加灵活的面试场景和实际工程决策。

# go语言基础

# 为什么选择Go语言

尽管 AI 降低了学习和切换编程语言的门槛,但每种编程语言依然都有各自擅长的领域和设计取舍(至少目前是这样),所以正确理解每个语言的专长并在合适的场景加以应用,依然是现代开发者必备的决策能力之一。

所以,在回答该问题时,我们不应该过分列举特性清单,而是从 Go 诞生的原始约束,理解它为了解决什么问题、做了哪些取舍、适合什么场景来分析。

我们先从设计动机触发,从官方的Why did you create a new language一问中可以看出,Go诞生的初衷是解决 Google 内部大型 C++ 和 Java 代码库在编译速度、代码可读性、并发编程三个维度上的工程痛点。这意味着 Go 自诞生起就是一门要做到简单且高效的语言:

基于这个出发点,Go 的设计哲学可以归纳为一个核心原则:少即是多,也就是通过减少语言特性来降低开发和协作成本。只有明确了这一理念,我们才能更好的去说明为什么选择这个技术栈,按照正常的问答逻辑,我们应从完整的开发链来逐步展开这一点,即从:

  1. 语法层面:Go只有25个关键字,语法规则高度统一,刻意剔除了异常、泛型、继承、宏等复杂概念,确保团队风格统一,降低code review和维护成本。
  2. 标准库层面:Go 标准库虽不追求覆盖所有场景,但在 HTTP服务、JSON处理、加密、I/O 等最常见的以做到开箱即用
  3. 编程风格层面:组合优于继承,用embedding替代类型继承体系,统一了继承、组合、装饰、代理等设计模式实现上的统一
  4. 原生高效并发支持:通过 goroutine(用户态轻量线程)+ channel(通信原语)的组合,提供一种"开箱即用"的并发编程体验。
  5. 编译打包部署:Go 源码直接编译为静态链接的可执行文件,无需运行时环境(如 JVM)或外部依赖
  6. 工具链:Go 将格式化(gofmt)、静态检查(go vet)、测试(go test)、文档生成(godoc)等工具全部内置到官方工具链中,确保全流程开发强一致和高度集成

我们举个最简单的例子,我们希望生成一个继承User属性的Employee,能够并发写入消息队列,并让对端消费者序列化打印:

按照Java的理念,我们需要考虑:

  1. 对象继承/组合/装饰器的可维护性考量
  2. 集成第三方序列化工具
  3. Thread/线程池/Future选用
  4. 并发消费容器选取

Go则是简单高效:

  1. 结构体组合
  2. go关键字启动协程投递
  3. channel阻塞消费
  4. 通过内置序列化工具打印
type Employee struct { // 【3】组合优于继承:嵌入替代extends,仅此一种复用方式
	User
	Role string
}

func main() {
	// 场景:并发获取3个员工信息,通过channel收集,JSON序列化输出
	emps := []Employee{
		{User: User{Name: "张三"}, Role: "工程师"},
		{User: User{Name: "李四"}, Role: "经理"},
		{User: User{Name: "王五"}, Role: "设计师"},
	}

	// 【4】原生并发:3个goroutine并发"查询",channel收集结果
	ch := make(chan Employee, len(emps))
	for _, emp := range emps {
		go func(emp Employee) {
			time.Sleep(100 * time.Millisecond) // 模拟耗时查询
			ch <- emp                          // 查询完成,通过channel发送
		}(emp)
	}

	// 【1】语法简洁 + 【2】标准库:收集结果并JSON序列化
	for i := 0; i < len(emps); i++ {
		emp := <-ch                    // 从channel接收,谁先完成先处理谁
		data, err := json.Marshal(emp) // 【2】标准库JSON开箱即用
		if err != nil {                // 【1】错误处理只有 if err != nil
			fmt.Println(err)
			continue
		}
		fmt.Println(string(data))
	}

	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

所以,在明确Go语言这些设计上的取舍之后,选择Go的原因也就很明了:对于云原生基础设施(Docker、Kubernetes、Prometheus 均用 Go 编写)、微服务/API 网关、CLI 工具、网络代理等需要高并发 + 快速部署 + 长期维护的场景,且业务上没有任何历史包袱的情况下,我们可以优先选择GO。对于需要复杂类型系统和领域建模的企业应用则优先选择Java系生态。

# go语言和Java语言的区别

比较编程语言的区别,更多要从各自的理念和设计角度出发进行分析。以Go和Java为例,Java本质上是为解决C++繁琐的编程负担而诞生的、带自动垃圾回收的高级语言。而Go则是在语言爆发时代下,尝试以更独特的姿态做到全方位覆盖、沿袭优秀理念但保持轻量的一门语言。

针对编程语言上的区别,面试官更希望看到的是一个具有经验的开发者在语言特质的比对,而非拘泥于语法上的差异,所以笔者推荐从如下角度展开说明:

  • 语言设计特点
  • 应用生态
  • 性能表现

# 语言设计

我们先来说说语言设计上,Java在继承这一方面的设计上,通过extends关键字实现继承功能,同时,为避免菱形问题,直接采用单继承,这使得在面向对象的设计上,需要结合接口进行一些组合。

而Go则舍弃了继承体系,为保证语法实现上的简单,通过struct embedding(组合)实现代码复用,例如我们希望Dog类能够直接复用Animal的方法,就可以直接通过声明、组合、使用三个步骤完成类似于继承效果的设计:

对应的编写步骤如下:

1. 声明Animal及SayHi方法:

type Animal struct {
	Name string
}

func (a Animal) SayHi() string {
	return "Hi, I am " + a.Name
}
1
2
3
4
5
6
7

2. 声明Dog并组合Animal

type Dog struct {
	Animal
	Swimmer
}
1
2
3
4

3. 初始化Dog,编译器自动做方法提升(Method Promotion),一达到类似于继承效果的直接调用,输出 Hi, I am Buddy

d := Dog{Animal: Animal{Name: "Buddy"}}

fmt.Println(d.SayHi()) // 直接调用 Animal 的 SayHi,输出 Hi, I am Buddy
1
2
3

对于多态性,Go也是简单粗暴提出如下原则:

  • 只要接口方法签名一致,即可认为该类型实现此接口,无需像Java通过implements声明
  • 运行时,接口值内部持有一张 itab(接口方法表),记录了具体类型的方法函数指针。不同类型实现同一接口后,itab 指向各自的函数表,方法调用时通过 itab 做动态分发,从而完成多态。

例如我们定义了一个speak接口并声明一个返回String类型的行为能力,我们希望创建Dog和Cat类型实现这一能力:

对应代码落地步骤也很简单,首先声明接口:

type Speaker interface {
	Speak() string
}
1
2
3

以Dog类型为例,明确Speak方法签名格式后,我们只需实现一个值接受者为Dog,方法名为Speak,返回值为string的方法,Go语言方法签名匹配机制就会认为这个类型实现了Speaker接口,这也就是所谓的隐式接口机制:

type Dog struct{ Name string }

// 无需 implements,方法签名匹配即自动实现 Speaker 接口
func (d Dog) Speak() string {
	return d.Name + ": 汪汪!"
}

1
2
3
4
5
6
7

基于该匹配机制,我们将Dog作为MakeSound函数入参之后,也是可以正确执行的:

// MakeSound 接收接口类型,运行时通过 itab 动态分发到具体类型的方法
func MakeSound(s Speaker) {
	fmt.Println(s.Speak())
}

MakeSound(dog)   // itab -> Dog.Speak
1
2
3
4
5
6

也正是因为这种简单灵活的规约,使得Go语言继承体系十分灵活,此时,如果我们声明一个Animal且带有相同签名的Speak方法,Go语言匹配机制也会认为Dog继承了Animal类:

对应我们也可以通过编码来印证这一点:

type Animal interface {
	Speak() string
}

1
2
3
4

通过隐式接口赋值,若程序正常输出,则说明dog实现了animal类型:

// 类型断言:判断 dog 是否是 Animal 类型
	var a Animal = dog // 隐式接口赋值
	fmt.Printf("\n[断言] dog 实现了 Animal: %T\n", a) //[断言] dog 实现了 Animal: main.Dog

1
2
3
4

# 应用生态

再来说说二者的生态。因为有着 Spring 这个强大的框架体系支持,Java 几乎覆盖了企业应用 Web 程序、微服务、数据、消息、安全所有的场景,所以在传统企业级应用(ORM、Web 框架、大数据/ML)上,Go 确实相对薄弱。但在云原生基础设施(Docker、Kubernetes、Prometheus)这些特定领域,Go 有着统治级的生态。这并非偶然——Go 的单二进制部署、静态编译、低资源消耗等特性,恰好契合了容器化场景对"轻量、可移植、快速启动"的核心需求。可以说,Go 的语言设计与云原生的工程需求之间形成了一种正向共振。

# 性能表现

再来说说性能表现方面,JDK 21引入Virtual Threads后,Java在IO密集型场景的并发表现已接近Go的goroutine。笔者在自己的Mac M4(10C32G)上做过一个压测:请求耗时100ms、并发1000的场景下,Go和Java的吞吐量(throughput)均在9000左右,性能差距不大:

理论最大吞吐量计算公式如下:

最大吞吐量 = 并发数/ 请求耗时
				 = 1000 / 0.1s 
				 = 10,000 req/s
1
2
3

实际9000约为理论值的90%,剩余10%的损耗来自框架开销、调度开销、GC以及启动时的预热等,属于合理范围。但在冷启动速度和内存占用方面,Go毫秒级启动和MB级的内存占用远小于Java秒级启动+百MB级别的内存:

而在并发编程这一方面,Go语言更着重强调简单易用,无需手动管控并发线程池、阻塞队列等并发队列存取逻辑处理,只需一个go关键字即可快速启动配合,channel和select天然表达并发协调。

我们以一个多生产者和单消费者的案例为例,该需要我们高效并发写入元素到队列中让消费者准确的消费:

go语言的写法就比较简单,快速构建channel只有,用go关键字启动协程投递即可:

// 创建一个缓冲通道
	ch := make(chan string, 3)

	// 一行启动三个并发任务
	go func() { ch <- "api1" }()
	go func() { ch <- "api2" }()
	go func() { ch <- "api3" }()

	// select天然协调:谁先返回先处理,超时兜底
	for i := 0; i < 3; i++ {
		select {
		case result := <-ch:
			fmt.Println(result)
		case <-time.After(2 * time.Second):
			fmt.Println("超时")
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

相比之下,Java则稍显啰嗦,需要显示指定线程池和阻塞队列,同时还需要结合try-with-resource等机制优雅关闭资源:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            BlockingQueue<String> queue = new LinkedBlockingQueue<>(3);
            // 提交到阻塞队列
            executor.submit(() -> queue.add("api1"));
            executor.submit(() -> queue.add("api2"));
            executor.submit(() -> queue.add("api3"));

            // 等待所有任务完成后再消费,避免 poll 非阻塞导致提前结束
            Thread.sleep(100);
            String item;
            while ((item = queue.poll()) != null) {
                System.out.println(item);
            }
        }  
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# GoRoot 和 GoPath 有什么用

无论是初学者还是有编程经验的开发者,每次提及这个问题的时候还是会有所疑惑。当然,你也可以认为这是一道无用的八股文,但笔者所要强调的是这道题目之后,我们对于技术理解的能力。

结合二者的语义,GOROOT强调的是"根"的理念,即Go语言一切的根源,所以GOROOT代表Go的安装路径,包含了标准库和工具链。我们可以通过命令来验证:

$ go env GOROOT
/usr/lib/go-1.22

$ ls /usr/lib/go-1.22
VERSION  api  bin  go.env  misc  pkg  src  test
1
2
3
4
5

其中 bin/ 存放工具链(go、gofmt)等:

src/ 存放标准库源码(fmt、net/http、os等),这也印证了GOROOT确实是Go语言一切的"根":

同理,GOPATH强调的是"路径"的概念,它是一个工作空间(workspace),用于管理项目源码和第三方依赖。GOPATH可以设置多个目录,按照Go官方的要求,GOPATH下需要包含三个文件夹:

  1. src:存放源代码文件
  2. pkg:存放编译后的库文件
  3. bin:存放可执行文件

⚠️ 注意:自Go 1.16默认启用Go Modules后,项目不再强制要求放在GOPATH/src下,开发者可以在任意目录管理项目。GOPATH目前主要用于go install安装工具的默认存放位置和模块缓存路径。简单来说,GOROOT告诉Go去哪里找标准库,GOPATH告诉Go去哪里找你的代码和第三方依赖。

从设计演进的角度看,GOPATH → vendor → Go Modules 的变迁,本质上是一个从"全局共享"到"项目隔离"的依赖管理演进过程。GOPATH 时代所有项目共享同一个依赖目录,无法实现项目级别的版本隔离;vendor 机制通过在项目中拷贝依赖实现了隔离,但引入了重复存储和更新困难的问题;Go Modules 通过 go.mod 文件声明依赖版本,配合最小版本选择(MVS)算法,最终在隔离性和可维护性之间找到了平衡。理解这个演进过程,有助于在面试中展示对 Go 工程化能力的深层理解。

# 介绍一下 defer关键字作用

回答一个关键字的原理,最好的方式就是去官网说明其思想和理念,按照官方的说法,defer关键字将在对应函数返回时执行,所以,它非常适用于各种流程执行的事后清理工作:

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

举个最经典的文件清理的例子,就像Java最佳实践一样,从Java 7开始可以通过try-with-resources语法特性自动处理实现了AutoCloseable接口的IO工具类:

public class IODemo {
    public static void copyFile(String src, String dest) {
        // 多个资源用分号分隔,按声明的逆序关闭
        try (
            InputStream in = new FileInputStream(src);
            OutputStream out = new FileOutputStream(dest)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // in 和 out 都会自动关闭,先关 out,再关 in(逆序)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

结合go语言简单的理念,对于"事后清理"工作,它并没有像java那样给出过多的约束条件和语法糖,取而代之的则是直接通过defer关键字。

按照官方建议,对于资源释放工作,应该在资源创建之后附近补充defer处理语句而非在函数末尾编写,避免遗漏。

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close() // 最佳实践:资源获取后立即 defer 释放,避免遗漏

	dst, err := os.Create(dstName)
	if err != nil {
		return // 即使这里错误返回,src.Close() 也会被执行
	}
	defer dst.Close() 

	return io.Copy(dst, src) // 函数返回前,自动执行 dst.Close() 和 src.Close()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

当然,这也是古法编程时的最佳实践,读者可以在日常开发时结合AI编程助手的输出质量,在rules中补充相关规范来约束这一点,降低后续复杂工程排错的上下文复杂度。

# 多个 defer 的顺序

在明确了解defer关键字的使用理念之后,我们再来聊聊多个defer关键字的执行顺序和时机,对于这类技术类特定的问题,笔者一向推荐主动到官网检索答案,对于defer关键字的工作机制,在defer-panic-and-recover一文中作者也说明,defer关键字调用是严格按照LIFO也就是栈的工作机制执行:

Deferred function calls are executed in Last In First Out order after the surrounding function returns

所以我们在一段循环中,利用defer关键字延迟输出数字i时,对应的执行过程为:

  1. 依次遍历数字0、1、2、3压入栈中
  2. 函数返回
  3. 不断从栈顶抛出并输出3、2、1、0
func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Print(i) // defer is LIFO => 3210
	}
}
1
2
3
4
5

需要补充说明的是,上述特性进针对Go 1.22 版本及其之后,在之前的版本Go语言对于defer闭包传值则是仅创建一次变量i,则使得输出结果以最终结果为主,也就是3、3、3、3

# defer 在什么时机会修改返回值

接下来是 defer 修改返回值的问题,这是面试中的高频考点。关键在于理解 return 语句并不是一个原子操作,它实际上分为三步:赋值返回值 → 执行 defer → 真正返回。因此,defer 有机会在函数真正返回之前修改命名返回值:

func foo() (result int) {
    defer func() { result++ }()
    return 1
}

func main() {
    fmt.Println(foo()) // 输出 2,而非 1
}
1
2
3
4
5
6
7
8

执行过程:return 1 先将 result 赋值为 1 → defer 将 result 自增为 2 → 函数真正返回 2。

需要注意的是,defer 只能修改命名返回值(named return value)。如果返回值没有命名,defer 无法访问和修改它:

func bar() int {
    result := 1
    defer func() { result++ }()
    return result // 返回 1,defer 修改的是局部变量 result,不影响返回值
}
1
2
3
4
5

# go出现panic的场景

结合官方的说法,Panic是一个内置函数,常用于运行时异常的流程终止工作。当方法调用panic时,后续流程全部中断不再执行(除了defer函数),然后往更上层的堆栈执行,直到协程返回,触发程序崩溃。

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

结合这段说法,我们可以知晓,panic内置函数有点类似于Java的RuntimeException,但有一个重要区别:Java的RuntimeException可以被catch捕获后继续运行,而Go的panic如果没有被recover捕获,就会导致整个程序崩溃。所以针对这题的考察,我们也可以着重针对软件运行时异常分析。

首当其冲的肯定是空指针异常:

type Person struct {
	name string
}

func main() {
	var p *Person
	fmt.Println(p.name) //panic: runtime error: invalid memory address or nil pointer dereference
}
1
2
3
4
5
6
7
8

然后就是一些非法的算术异常,对应代码如下,请读者务必注意笔者的写法,显式声明变量并指定0值,而非直接除0,原因也很简单,Go在针对算术异常时也做了一定的健壮性处理,即编译器看到被除数字面量为0时,直接编译失败:

	num := 100
	divisor := 0
	fmt.Println(num / divisor) // panic: runtime error: integer divide by zero
1
2
3

还有一些标准库函数在参数不合法时也会抛出panic,例如:rand.Intn在入参值小于等于0时会panic:

fmt.Println(rand.Intn(0)) //panic: invalid argument to Intn
1

总而言之,针对大部分运行时异常,Go语言默认都是通过调用panic触发,对应常见场景总结如下:

  • 空指针调用
  • 数组/切片越界
  • 除 0 算术异常
  • 未能在正确的状态访问资源,最经典就是channel的处理:关闭未初始化的 channel、向已经关闭的 channel 发送数据、重复关闭 channel
  • 访问未初始化 map
  • 并发读写 map(Go的map不是并发安全的,需要加锁或使用sync.Map)
  • sync 计数为负数,即sync.WaitGroup重复执行done导致计数逻辑异常
var wg sync.WaitGroup

	wg.Add(1)
	wg.Done()
	wg.Done() // panic: sync: negative WaitGroup counter
1
2
3
4
5
  • 类型断言不匹配,即将接口指向的类型转为其他类型并使用,建议使用 s, ok := i.(int) 进行前置检查:
var i interface{} = "hello"

// 不安全:直接断言,类型不匹配会panic
s := i.(int) // panic: interface conversion: interface {} is string, not int

// 安全:使用 comma-ok 模式进行前置检查
s, ok := i.(int) // ok 为 false,不会panic
if !ok {
    fmt.Println("类型断言失败")
}
1
2
3
4
5
6
7
8
9
10

关于defer和panic更多知识点,可以看到官网的文档:https://go.dev/blog/defer-panic-and-recover (opens new window)

# go是否支持while循环,如何实现这种机制

Go为了保证语法的简洁、高效和可读性,仅提供for循环控制能力,通过循环参数的调整来实现while和do...while的效果,从而避免语言关键字的膨胀和概念的重复。

模拟while循环最直接的方式是使用 for condition {},即只保留条件判断:

i := 0
for i < 5 {
    fmt.Println(i)
    i++
}
1
2
3
4
5

也可以使用不带条件的for循环,配合 if + break 来控制退出:

for {
    // do something
    if someCondition {
        break
    }
}
1
2
3
4
5
6

对应 do {} while (条件) 的效果(至少执行一次),只需在无限for循环的逻辑末尾补充条件判断:

i := 0
for {
    fmt.Printf("i = %d\n", i)
    i++
    if i >= 5 {
        break
    }
}
1
2
3
4
5
6
7
8

# Go 允许多个返回值吗

Go 支持多返回值,是语言规范层面的设计决策,而非底层实现细节。Go 的函数签名允许声明多个返回值,这使得错误处理成为函数签名的一部分——调用方必须显式处理每一个返回值,错误不会被静默忽略。

从底层实现角度看,Go 通过调用约定传递返回值在 Go 1.17 之前,参数和返回值主要通过栈传递;从 Go 1.17 开始,在 amd64 架构上引入了基于寄存器的调用约定(register-based ABI),参数和返回值优先通过寄存器传递,超出寄存器容量时才回退到栈,由此保证程序执行的高效性(CPU读写远高于内存)

多返回值配合命名返回值,还带来了几个工程上的优势:

  • 灵活的多返回值:Go 没有在语言规范中硬性限制返回值数量,实际上限取决于编译器和调用约定
  • 适配 defer:defer 函数可以在外围函数返回时直接修改命名返回值,因为返回值在函数栈帧中有明确的地址(栈传递场景下)或通过约定的寄存器/栈位置可访问
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Println("result:", result) // result: 3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 写go单元测试的规范

Go 语言将测试工具内置到工具链中,体现了前面提到的"工具链统一"的设计哲学——所有 Go 项目的测试方式完全一致,不存在 JUnit vs TestNG 的选型争议。这种统一性降低了团队协作的认知成本,也让测试成为开发流程中自然而然的一部分,而非额外的负担。

Go语言内置单元测试工具,只需严格按照规范即可快速编写单元测试,对应规则为:

  1. 文件名必须以 _test.go 结尾
  2. 测试函数必须以 Test 开头,参数为 t *testing.T
  3. 测试文件与被测文件在同一个包(同一目录下,package声明相同)

例如:我们的main包下有个calc.go的计算工具类:

package main

// Add 两数相加
func Add(a, b int) int {
	return a + b
}

// IsPositive 判断是否为正数
func IsPositive(n int) bool {
	return n > 0
}

1
2
3
4
5
6
7
8
9
10
11
12

对应的编写步骤为:

  1. 声明 calc_test.go 文件
  2. 测试加法的函数为 TestAdd,入参为 t *testing.T
  3. 测试文件与被测文件在同一目录

对应代码示例如下:

import "testing"

func TestAdd(t *testing.T) {
	result := Add(1, 2)
	expected := 3

	if result != expected {
		// t.Errorf 报告失败但继续执行后续测试
		t.Errorf("Add(1, 2) = %d, want %d", result, expected)
	}
}

func TestIsPositive(t *testing.T) {
	if !IsPositive(1) {
		t.Errorf("IsPositive(1) = false, want true")
	}

	// t.Fatalf 报告失败并立即停止当前测试函数
	if IsPositive(-1) {
		t.Fatalf("IsPositive(-1) = true, want false")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

运行测试:

$ go test -v ./...
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestIsPositive
--- PASS: TestIsPositive (0.00s)
PASS
1
2
3
4
5
6

# Go 有异常类型吗?为什么?

Go中没有"异常(Exception)"的概念,只有"错误(Error)"。Go本着简单的原则,对于错误的处理一贯通过判断逻辑来处理,从而避免Java那种try-catch-finally不同类型捕获的繁琐逻辑:

func main() {
    err := foo()
    if err != nil {
        fmt.Println(err)
    }
}

func foo() error {
    return nil
}
1
2
3
4
5
6
7
8
9
10

当然,Go语言也支持自定义错误类型,对应步骤为:

  1. 声明错误结构体
  2. 实现error接口的 Error() string 方法
// 自定义错误类型,只需实现 Error() string 方法即可
type MyError struct {
    Code int
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("[错误码:%d] %s", e.Code, e.Msg)
}
1
2
3
4
5
6
7
8
9

使用时,直接创建返回即可。例如这段年龄判断的逻辑,若小于0要返回自定义错误,则直接通过 &MyError 创建错误对象并返回指针:

func checkAge(age int) error {
    if age < 0 {
        return &MyError{Code: 400, Msg: "年龄不能为负数"}
    }
    return nil
}
1
2
3
4
5
6

后续的错误处理逻辑,我们可以在if逻辑中通过类型断言判断错误类型,进行个性化处理:

func main() {
    err := checkAge(-1)
    if err != nil {
        fmt.Println(err) // [错误码:400] 年龄不能为负数

        // 类型断言获取自定义字段
        if e, ok := err.(*MyError); ok {
            fmt.Println("code:", e.Code, "msg:", e.Msg)
        }
    }

    // 简单错误:errors.New 和 fmt.Errorf
    fmt.Println(errors.New("简单错误"))
    fmt.Println(fmt.Errorf("用户 %s 不存在", "张三"))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

⚠️ 注意:Go 1.13引入了 errors.Is 和 errors.As,这是目前官方推荐的错误判断方式。当错误被 %w 包装后(如 fmt.Errorf("xxx: %w", err)),传统的 == 比较和类型断言会失效,必须使用这两个函数才能正确匹配。

// errors.Is:判断错误链中是否包含某个特定的错误值
var ErrNotFound = errors.New("not found")

func query() error {
    return fmt.Errorf("query failed: %w", ErrNotFound)
}

err := query()
if errors.Is(err, ErrNotFound) {
    fmt.Println("找到了") // ✅ 支持错误链解包
}

// errors.As:从错误链中提取特定类型的错误
var myErr *MyError
err = checkAge(-1)
if errors.As(err, &myErr) {
    fmt.Println("code:", myErr.Code) // ✅ 支持错误链解包
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
方式 旧写法 新写法(Go 1.13+) 区别
判断错误值 err == target errors.Is(err, target) 支持错误链解包
判断错误类型 err.(*MyError) errors.As(err, &target) 支持错误链解包

# 函数、方法的区别

大部分Java开发接触的都是面向对象开发,而Go语言作为一门以C理念衍生出来的语言,就涉及面向过程和面向对象两种编程范式。理解函数和方法的区别,是可以判断一个Go语言开发者能否在不同的应用场景采用不同的方式编写可长期维护且高效代码的基准。

# 具名函数

函数可理解为一组操作序列,是程序基本组成部分,又可分为具名函数和匿名函数,具名函数即带有参数、返回值和函数名,如下所示:

/*
*
	入参:a b
	函数名:swap
	返回值:int int
*/
func swap(a, b int) (int, int) {
	return b, a
}
1
2
3
4
5
6
7
8
9

具名函数基础之上,结合面向对象的概念又衍生了方法的概念,与函数唯一的区别就是方法在函数名前面带有类型标识,即将方法依托于类型上,通过静态编译完成绑定。

例如:我们上文的交换函数,若希望该方法被封装为对象的一个行为,则需要在方法名前面声明类型:


type NumUtil struct {
}

func (nu *NumUtil) swap(a, b int) (int, int) {
	return b, a
}
1
2
3
4
5
6
7

使用示例如下所示:

nu := NumUtil{}
	a, b := nu.swap(1, 2) //2 1
	fmt.Println(a, b)
1
2
3

# 匿名函数

匿名函数即没有函数名的函数,常用于启动一个轻量、简单的并发任务,只需一个go关键字+匿名函数即可快速启动:

go func() {
		fmt.Println("在后台运行的任务")
	}()
1
2
3

需要注意的是,当匿名函数引用了外部变量时,它捕获的是该变量的地址而非值拷贝,所有对变量的修改都会直接影响外部,这种函数我们称为闭包函数:

wg := sync.WaitGroup{}
	wg.Add(1)
	//声明一个变量num
	num := 1
	//执行一个闭包函数,修改num的值
	go func() {
		num *= 10
		wg.Done()
	}()

	wg.Wait()
	fmt.Println("num:", num)//num: 10
1
2
3
4
5
6
7
8
9
10
11
12

# 二者区别

来简单小结一下二者的区别,通过是否带有类型标识符我们可以拆解出函数和方法,函数作为面向过程的编程范式,不带有任何类型标识符,可直接声明使用。而方法则是面向对象思想下的产物,为保证以对象维度复用和修改内部状态,方法名前带有类型标识符。

匿名函数则是函数的最简形式,它本着轻量、简单的理念,常用于协程的快速启动,根据是否捕获外部变量进而衍生出闭包函数的概念:

# 如何理解Go语言中的方法

C语言作为Go的祖先,没有直接给出面向对象的使用方式,但其底层一些设计上已经体现了面向对象编程的思想。

我们以文件操作打开和关闭为例,可以看到,其函数签名上是带有操作的文件对象的FILE,这种基于FILE结构体的函数操作,已经有了C++中构造和析构方法的雏形:

FILE *fopen( const char *filename, const char *mode );


int fclose( FILE *stream );
1
2
3
4

与之对应Go其实和C语言也很像,为了做到对象维度的复用,即带有面向对象的操作,它仅仅是将方法所操作的对象提到了函数签名前面作为receiver:

func (f *File) Close() error {
	if f == nil {
		return ErrInvalid
	}
	return f.file.close()
}

// read reads up to len(b) bytes from the File.
// It returns the number of bytes read and an error, if any.
func (f *File) read(b []byte) (n int, err error) {
	n, err = f.pfd.Read(b)
	runtime.KeepAlive(f)
	return n, err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 切片传参是引用传参吗

这是大部分Java开发者的通病,初次接触下面这段函数,大部分会认为该函数传入的是切片的引用,所以能够修改切片内部的元素:

// resetFirst 将切片第一个元素改为0
func resetFirst(nums []int) {
	if len(nums) == 0 {
		return
	}
	nums[0] = 0
}
1
2
3
4
5
6
7

实际上,Go语言中一切都是值传递,切片传参时传入的是SliceHeader的值拷贝。只不过SliceHeader中的Data字段是一个指针,拷贝后的新SliceHeader与原切片指向同一个底层数组,所以通过Data指针修改数组元素会影响原切片:

所以上述的函数我们完全可以理解为这样:

func resetFirst(s SliceHeader) {
	// s 是 SliceHeader 的值拷贝,但 s.Data 与原切片指向同一底层数组
	*(*int)(unsafe.Pointer(s.Data)) = 0
}
1
2
3
4

# Go 编译相关的命令详解

# go编译常见指令有哪些?

就像计算机专业必修的编译原理课程一样,本题主要考察候选人对于 Go 语言底层机制的理解深度。掌握编译过程和相关指令,有助于从更底层的视角处理和优化日常研发工作,例如理解逃逸分析对性能的影响、读懂编译器给出的优化提示等。

总的来说,Go语言有3个比较常见的指令分别是:

  • go build:编译包及其依赖,对main包生成可执行文件,对非main包只检查编译是否通过
  • go install:编译并安装包及其依赖,将生成的可执行文件放到 $GOPATH/bin(或 $GOBIN),将编译后的包文件放到缓存中
  • go run:编译并直接运行Go源文件,适用于开发调试阶段,不会在当前目录留下可执行文件

# Go 编译链接过程概述

大部分使用Go语言的开发者一般只会在八股文中学习了解这一点,实际上,掌握一个语言的编译和链接步骤,会让你以更底层的视角处理和优化日常研发工作。

对于Go语言的编译链接过程,我们从一个开发好的代码说起,假设我们现在存在如下加法代码段:

x := 1 + 2
1

首先是进行词法分析,词法分析会将一条代码段拆分为数个token:

IDENT("x"), ASSIGN(":="), INT("1"), ADD("+"), INT("2"), SEMICOLON(";")

1
2

然后是语法分析,将上述的token尝试构建为一棵AST语法树,确保语法是否正确:


    := (赋值)
   / \
  x   + (加法)
     / \
    1   2

1
2
3
4
5
6
7

接下来是语义分析(type checking):

  • 类型检查:是否存在不同类型的错误运算,例如:int+String
  • 未使用变量导入检查
  • 闭包变量捕获分析

然后是中间代码生成,该步骤会在正式执行代码之前,进行一些可控的优化工作,例如:

  • 逃逸分析:确定对象分配是位于堆还是位于栈(逃逸分析发生在 SSA 构建阶段,而非语义分析阶段)
  • 方法内联:将简单的函数调用直接替换为函数体本身,避免函数调用的跳转开销
  • 常量折叠:将编译期可计算的常量表达式直接计算出结果,避免运行时重复计算
  • 死代码消除:移除永远不会被执行到的代码分支,减小最终二进制文件的体积

明确完成上述所有校验和优化工作之后,最后就是生成可执行的机器码。然后将编译产物与标准库(runtime、OS相关包等)和第三方依赖全部链接起来,生成最终的可执行程序。

总结一下,Go语言编译过程为:

  • 词法分析
  • 语法分析
  • 语义分析
  • 中间代码生成
  • 链接并生成可执行文件

# 基本数据类型

# uint8 类型溢出问题

基本类型问题主要是考察候选人,特定场景对不同业务数据精度的把控能力,是一种针对性能和准确性能力的综合体现。

针对uint的溢出问题,我们还是要从其底层存储的视角出发。

与Java不同,对于整数类型,Go将其分为不同的精度,其中uint8即无符号8位整数,范围为0~255。当值达到255时,其二进制表示为 11111111,此时若再加1,结果为 100000000(9位),但uint8只有8位,最高位被截断,剩下 00000000 = 0,这就是溢出:

对应我们也给出代码示例印证:

var num uint8
	num = 255
	fmt.Println(num + 1)//0
1
2
3

# 能介绍下 rune 类型吗

针对rune类型的考察,是查看候选人对于go语言源码的掌握程度,以及对于不同编码和字符串管理的理念,按照现有主流的编码范围:

  • ascii:仅仅占用7bit,但只能表示英文的128个字符
  • unicode:则是ascii的超集,包含世界上书写系统的存在的所有字符
  • utf8:变长编码(1~4 字节),中文字符通常占 3 个字节

在Go语言中默认编码为UTF-8,为更好地表示Unicode范围内的所有字符,故采用int32来表示,同时为了语义化字符串的4字节字符类型,设计者将这种类型称之为rune,对应也可以在源代码builtin.go中印证这一点:

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
1
2
3

将字符串转为 []rune 后,每个中文字符会被解码为一个独立的 int32 值,由此保证字符串索引以字符为一个原子,确保语义化遍历的高效性:

s := "Go语言"
	fmt.Println(len([]rune(s))) // 4:按字符计数,G + o + 语 + 言

	// 按字符遍历
	for i, r := range s {
		fmt.Printf("索引%d: %c (Unicode: U+%04X)\n", i, r, r)
	}
1
2
3
4
5
6
7

输出结果如下,可以看到按照rune准确的将字符串按照字符进行划分,同时编历时也是严格按照字符进行逐个遍历:

4
索引0: G (Unicode: U+0047)
索引1: o (Unicode: U+006F)
索引2: 语 (Unicode: U+8BED)
索引5: 言 (Unicode: U+8A00)
1
2
3
4
5

同时,为了印证Go语言字符默认采用UTF-8编码也这一说法,我们也可以通过len函数印证:

fmt.Println(len(s)) //8:按字节计数,G(1) + o(1) + 语(3) + 言(3)


1
2
3

# 单引号,双引号,反引号的区别?

这题同样是考察候选对于字符串运用场景的使用经验:

  • 单引号:存放一个字符
  • 双引号:字符串,我们最常用的
  • 反引号:不解析字符串中的转义字符,保留原始内容,常用于打印原生格式的文本(如SQL语句、多行模板等)

对应我们也给出代码印证这一点:

// 【1】单引号 → rune (int32),单个 Unicode 字符
	ch := '语'
	fmt.Printf("单引号: %c  类型: %T  值: %d\n", ch, ch, ch)

	// 【2】双引号 → string,解析转义字符
	s1 := "双引号\t换行\n第二行"
	fmt.Println(s1)

	// 【3】反引号 → raw string,原样输出,不解析转义
	s2 := `反引号\t换行\n第二行`
	fmt.Println(s2)
1
2
3
4
5
6
7
8
9
10
11

输出结果如下:

单引号: 语  类型: int32  值: 35821
双引号	换行
第二行
反引号\t换行\n第二行
1
2
3
4

需要注意的是,因为反引号保留原始打印的特性,对于一点特定的格式化输出字符,我们就可以很好的借助该符号进行格式化存储:

sql := `
		SELECT id, name
		FROM users
		WHERE age > 18`
	fmt.Println(sql)
1
2
3
4
5

输出结果如下:


		SELECT id, name
		FROM users
		WHERE age > 18
1
2
3
4

# 如何高效地拼接字符串

考察候选人对于字符串底层实现理解,无论是Java还是Go,字符串底层的存储都是采用字节数组管理,一般情况下字符串拼接有如下几种方式:

  1. 符号"+":每次 + 都创建一个全新字符串,将左右两侧内容拷贝进去。N次拼接的内存分配和拷贝次数为 O(n²),大量拼接时性能最差
  2. fmt.Sprintf:通过占位符界定模板,入参经过 interface{} 装箱后利用反射解析类型,再完成格式化拼接。除了和 + 一样的 O(n²) 拷贝开销,还额外承担了反射解析的开销。
  3. strings.Builder:底层维护一个 []byte 切片,通过 Grow() 预分配足够的容量,拼接时直接 append 到切片末尾,几乎不需要扩容。最终转字符串时直接基于底层 byte 数组构造,零拷贝
  4. []byte 拼接:手动管理 byte 切片,预分配足够容量后直接 append。本质上 strings.Builder 就是对 []byte 的封装,因此裸 []byte 的性能是理论天花板
  5. strings.Join:底层同样通过 byte 切片拼接,适合将一个字符串切片一次性合并为单个字符串。内部会先计算总长度再预分配,避免了多次扩容

对应我们也给出测试的源代码:


func main() {
	const n = 10000 // 拼接次数
	parts := make([]string, n)
	for i := range parts {
		parts[i] = fmt.Sprintf("str%d", i)
	}

	// 1. + 拼接:每次都创建新字符串,O(n²)内存分配和拷贝,大量拼接性能最差
	start := time.Now()
	var s1 string
	for _, p := range parts {
		s1 += p
	}
	fmt.Printf("+ 拼接:           %v\n", time.Since(start))

	// 2. fmt.Sprintf:通过占位符拼接,参数经interface{}装箱+反射解析,与+同属O(n²)量级
	start = time.Now()
	var s2 string
	for _, p := range parts {
		s2 = fmt.Sprintf("%s%s", s2, p)
	}
	fmt.Printf("fmt.Sprintf:      %v\n", time.Since(start))

	// 3. strings.Builder:底层[]byte切片预分配,O(n)拼接,零拷贝转字符串
	start = time.Now()
	var builder strings.Builder
	builder.Grow(n * 10) // 预分配容量,减少扩容次数
	for _, p := range parts {
		builder.WriteString(p)
	}
	_ = builder.String()
	fmt.Printf("strings.Builder:  %v\n", time.Since(start))

	// 4. []byte 拼接:strings.Builder的底层实现,预分配后直接append,理论最快
	start = time.Now()
	buf := make([]byte, 0, n*8)
	for _, p := range parts {
		buf = append(buf, p...)
	}
	_ = string(buf)
	fmt.Printf("[]byte 拼接:       %v\n", time.Since(start))

	// 5. strings.Join:底层也是[]byte切片预分配拼接,适合将字符串切片一次性合并
	start = time.Now()
	_ = strings.Join(parts, "")
	fmt.Printf("strings.Join:     %v\n", time.Since(start))

	// 防止编译器优化掉未使用的变量
	_ = s1
	_ = s2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

关键结论:

  • 第一梯队(微秒级):[]byte、strings.Builder、strings.Join,三者底层都是基于 byte 切片预分配,O(n) 线性增长,差距在封装开销
  • 第二梯队(毫秒级):fmt.Sprintf、+,每次拼接都创建新字符串,O(n²) 增长,10000次拼接慢了约 1000 倍
  • []byte 是 strings.Builder 的底层实现,裸用略快,但实际开发中推荐使用 strings.Builder,可读性更好
  • fmt.Sprintf 和 + 属于同一量级,多次运行排名会交替,本质上都是 O(n²) 的暴力拷贝

# 如何交换 2 个变量的值

在大学时期两数交换,本质通过一个中间值完成,虽然考察问题比较简单,但是出题者针对这题主要是考察候选人对于Go语言理念的理解。要知道,Go语言本质的理念就是简单,用最小的语义完成尽可能多的事情,所以,对于两数交换,Go语言提供了直接交换的语法:

num1 := 1
	num2 := 2
	
	num1, num2 = num2, num1
	fmt.Println(num1, num2)// 2 1
1
2
3
4
5

# go里面的int和int32是同一个概念吗

可能很多人认为int和int32都是4字节,实际上这题考察的是候选人对于操作系统的理解。Go语言中的int是一种平台相关的类型,其大小取决于底层系统的位数:

  • 32位系统:int = 4字节,与int32相同
  • 64位系统:int = 8字节,与int64相同

而int32是固定4字节的,与系统位数无关。所以在64位系统上,int和int32并不是同一个概念,int能表示的数值范围比int32更大。在需要明确整数大小的场景(如网络协议、文件格式等),建议使用int32/int64等固定大小的类型,而非int。

对应我们的也给出int在64位系统的字节数的印证代码段:

var x int = 1     
 fmt.Println(unsafe.Sizeof(x)) // 输出 8
1
2

# 参考

GOALNG_INTERVIEW_COLLECTION: https://github.com/mao888/golang-guide/blob/main/golang/go-Interview/GOALNG_INTERVIEW_COLLECTION.md interview-go: https://github.com/lifei6671/interview-go Go语言的多返回值是如何实现的?: https://blog.csdn.net/2301_78841354/article/details/156361479 Go出现panic的场景: https://www.cnblogs.com/paulwhw/p/15585467.html Go 中 while 和 do..while 的实现: https://blog.csdn.net/chengqiuming/article/details/115573947 golang常见面试题: https://blog.csdn.net/qq_67503717/article/details/136386099 Why did you create a new language? - Go FAQ: https://go.dev/doc/faq#creating_a_new_language 《Go语言高级编程(第2版)》

编辑 (opens new window)
基于 Claude Code 复刻 Redis 慢查询指令实践

← 基于 Claude Code 复刻 Redis 慢查询指令实践

最近更新
01
基于快照合并修复Seata AT回滚补偿与Flink批攒导致StarRocks数据不一致最佳实践
05-20
02
AI 写的企业级组件不敢用?我替你验过了
05-19
03
感觉你因为AI退化了?用Redis SCAN源码打脸AI时代的孔乙己
05-17
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×