Java vs Go:语法对比

打开 .go 文件,第一眼看到这些,脑子直接宕机:

func getUser(id int) (*User, error) {
    if user, ok := cache.Load(id); ok {
        return user.(*User), nil
    }
    defer func() { metrics.Record("getUser") }()
    // ...
}

:= 是什么? *Usererror 为什么挤在返回值里? defer 又是什么鬼? ok 从哪冒出来的?

写了 5 年 Spring Boot,习惯了 var user = new User()try-catch-finallypublic class 之后,Go 的语法看起来像是故意反着来。这篇文章的目的就是一句话:把所有 Java 里习以为常的语法点,在 Go 里找到对应写法 。没有废话,全是代码对比。

📌 前置知识:本文假定读者有 Java 基础(Java 8+),了解基本的编程概念(变量、函数、循环、异常)。Go 版本为 1.22。

变量声明: var / := 对比 Java 的类型声明

Java 声明变量的方式从"啰嗦"式进化到了 var

// Java —— 三种写法,都在用
String name = "foo";           // 传统
var name = "foo";              // Java 10+ 类型推断
final String NAME = "foo";     // 常量

Go 只有两种,但多了关键特性——短变量声明

// Go —— 只有两种
var name string = "foo"        // 完整写法(较少用)
name := "foo"                  // 短变量声明,类型自动推断

// 常量
const NAME = "foo"             // Go 的 const,不是全大写也行

// 多变量声明
var x, y int = 1, 2
a, b := 3, 4

:= 是 Go 里使用频率最高的符号之一。它等价于 var name = "foo" 但更简洁。 := 只能在函数内使用,包级别变量必须用 var

package main

var globalVar = "包级别用 var"   // ✅

func main() {
    localVar := "函数内用 :="    // ✅
}

⚠️ 新手提示: := 左边的变量如果已经声明过,会报编译错误。Go 不允许声明了变量却不使用——编译器直接拒绝。Java 里 int x = 1; 然后不用,只是一个 warning。

下面用一张表快速对照 Java 和 Go 的基本类型:

分类JavaGo
整数(有符号)int (32 位)/ long (64 位)int (平台相关,64 位机器 64 位)/ int32 / int64
整数(无符号)uint / uint32 / uint64
浮点float (32 位)/ double (64 位)float32 / float64
布尔booleanbool
字符串String (不可变,引用类型)string (不可变,值类型行为)
字节bytebyte (即 uint8
字符char (16 位 Unicode)rune (即 int32 ,Unicode 码点)
零值null每种类型有默认零值: int0string""boolfalse ,指针是 nil

⚠️ 新手提示:Go 的 int 大小取决于机器——64 位机器上就是 64 位。如果你存一个极大的数,在本地 Mac 上跑没问题,部署到 32 位 ARM 上就溢出。精确控制大小用 int32 / int64

flowchart TD
    JavaVar(["☕ Java 变量声明"]) --> J1["显式类型:String s = #quot;hello#quot;"]
    JavaVar --> J2["类型推断:var s = #quot;hello#quot;(Java 10+)"]
    JavaVar --> J3["常量:final String S = #quot;hello#quot;"]
    
    GoVar(["🐹 Go 变量声明"]) --> G1["完整声明:var s string = #quot;hello#quot;"]
    GoVar --> G2["短声明:s := #quot;hello#quot;(仅函数内)"]
    GoVar --> G3["常量:const S = #quot;hello#quot;"]
    GoVar --> G4["多变量:a, b := 1, 2"]
    
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
    
    class JavaVar,GoVar root
    class J1,J2,J3,G1,G2,G3,G4 leaf

循环: for 统治一切

Java 里有 for (三段式)、 for-each (增强 for)、 whiledo-whilestream.forEach 。Go 里 只有 for ——它包揽了所有循环场景:

// Java —— 五种循环
for (int i = 0; i < 10; i++) { }          // 三段式
for (var item : list) { }                  // for-each
while (condition) { }                      // while
do { } while (condition);                  // do-while
list.stream().forEach(item -> { });        // stream
// Go —— 只有 for,全包了
for i := 0; i < 10; i++ { }               // 三段式(注意没有括号!)
for _, item := range list { }              // range 遍历 = Java for-each
for condition { }                          // while(连关键字都省了)
for { }                                    // 死循环
for i := range 10 { }                      // Go 1.22:遍历 0 ~ 9

Go 的三段式 for 没有小括号,大括号 { 必须在同一行——这是硬性规定。

range 返回两个值:索引和元素。不需要索引用 _ 丢弃(Go 不允许未使用的变量,但 _ 除外)。

// Go 1.22 新特性:range over int
for i := range 5 {
    fmt.Println(i) // 0 1 2 3 4
}
// 等价于 Java 的:
// for (int i = 0; i < 5; i++) { System.out.println(i); }

条件与 Switch:惊喜连连

if —— 居然能带初始化语句

// Java
int result = compute();
if (result > 0) {
    System.out.println(result);
}
// Go —— if 可以带一个初始化语句
if result := compute(); result > 0 {
    fmt.Println(result)
}
// result 的作用域仅限于 if-else 块

这个特性在 Go 里非常常用——尤其是在处理 error 返回值的时候(后面会详细讲)。

switch —— break 消失了

// Java —— 每个 case 必须写 break,不然会穿透
switch (day) {
    case "MONDAY":
    case "TUESDAY":
        System.out.println("工作日");
        break;
    default:
        System.out.println("其他");
}
// Go —— 自动 break,不需要手写
switch day {
case "MONDAY", "TUESDAY":    // 多值匹配
    fmt.Println("工作日")
default:
    fmt.Println("其他")
}

Go 的 switch 自动 break。如果需要 Java 的 fall-through 行为,显式写 fallthrough 关键字。而且 switch 后面可以没有表达式——直接替代一堆 if-else:

// Go —— 无表达式 switch,等于一串 if-else
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

函数:Go 最"反 Java 直觉"的地方

多返回值

Java 里函数只能返回一个值,要返回多个只能包装成对象。Go 原生支持多返回值:

// Go —— 多返回值
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为 0")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    // 处理错误
}

对比 Java 的实现方式——要么抛异常,要么包装成 Result 对象:

// Java —— 类似效果需要包装类或异常
public class DivisionResult {
    private final int result;
    private final String error;
    // constructor, getters...
}

// 或者直接抛异常
public static int divide(int a, int b) throws ArithmeticException {
    if (b == 0) throw new ArithmeticException("除数不能为 0");
    return a / b;
}

函数是一等公民

Go 里函数可以赋值给变量、作为参数传递、作为返回值:

// Go —— 函数赋值给变量
add := func(a, b int) int {
    return a + b
}
result := add(3, 5) // 8

// Go —— 函数作为参数
func apply(nums []int, op func(int) int) []int {
    result := make([]int, len(nums))
    for i, v := range nums {
        result[i] = op(v)
    }
    return result
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// doubled = [2, 4, 6]

Java 里类似的功能用 Lambda + Function<T, R> 接口实现:

// Java —— Lambda 表达式(Java 8+)
Function<Integer, Integer> add = (a, b) -> a + b; // 编译错误!Function 只接受一个参数
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
var result = add.apply(3, 5); // 8

⚠️ 新手提示:Java 的 Function<T, R> 只接受一个参数,两个参数就得换 BiFunction ,三个参数得自己定义接口。Go 没有这个问题——函数签名是什么就是什么,不需要适配 Function 家族。

defer:资源清理终极方案

Java 里清理资源用 try-with-resources (Java 7+)或 try-finally

// Java —— try-with-resources
try (var reader = new BufferedReader(new FileReader("file.txt"))) {
    // 读取文件
} // 自动调用 close()

// Java —— 手动 finally
var conn = dataSource.getConnection();
try {
    // 操作数据库
} finally {
    conn.close();
}

Go 用 defer ——在函数返回前执行,而且 后进先出(LIFO, Last In First Out,后进先出)

// Go —— defer,资源申请和释放写在一起
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // 函数返回前一定执行

    // 读取文件...
    return nil
}

defer 的强大之处在于 先申请资源,紧接着写释放代码,它们挨在一起,不会忘记:

func processDB() error {
    conn, err := db.Connect()
    if err != nil {
        return err
    }
    defer conn.Close()

    tx, err := conn.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback() // 如果 commit 了,Rollback 是 no-op

    // 执行 SQL...
    return tx.Commit()
}

对比 Java 里 try-finally 嵌套的情况——释放和申请离得很远。defer 的执行顺序是后进先出(像叠盘子),多个 defer 时按声明的 逆序 执行。

flowchart TD
    JavaClean(["☕ Java 资源清理"]) --> JTR["try-with-resources\n(Java 7+,自动 close)"]
    JavaClean --> JTF["try-finally\n(手动关闭,嵌套多)"]
    JTF --> JTFN["嵌套3层 try-finally\n释放和申请离得很远"]
    
    GoClean(["🐹 Go defer"]) --> GD["申请+释放写在一起\ndefer f.Close()"]
    GD --> GLIFO["多个 defer:后进先出\ndefer A → defer B → defer C\n执行:C → B → A"]
    
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
    
    class JavaClean,GoClean root
    class JTR,JTF,GD,GLIFO leaf
    class JTFN reject

异常处理:最大的世界观冲击

这是 Java 程序员转 Go 最不适应的地方。先看一段代码对比:

// Java —— 面向 try-catch 编程
public User getUser(Long id) {
    try {
        return userRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("用户不存在"));
    } catch (NotFoundException e) {
        log.error("用户未找到: {}", id, e);
        throw e;
    } catch (DataAccessException e) {
        log.error("数据库异常", e);
        throw new ServiceException("查询用户失败", e);
    }
}
// Go —— error 就是返回值
func (s *UserService) GetUser(id int64) (*User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("查询用户失败: %w", err)
    }
    if user == nil {
        return nil, ErrNotFound
    }
    return user, nil
}

Go 没有 try-catch-finally ,没有 throw ,没有 throws 声明。 error 就是一个普通的返回值 。Go 的 error 本质上是一个接口:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为 error 返回。这比 Java 的 Throwable 继承体系简单太多了。

下面是 Java 异常调用栈和 Go error 向上传播的对比:

flowchart LR
    subgraph Java["☕ Java 异常链路"]
        direction TB
        J1(["Controller → Service"])
        J2["throws → ControllerAdvice"]
        J3["Service → Repository"]
        J4["throw new Exception ↻"]
        J5["catch + log.error()"]
        J6["Repository → DB"]
        J7(["❌ SQLException"])
        J1 --> J2 --> J3 --> J4 --> J5 --> J6 --> J7
    end
    subgraph Go["🐹 Go Error 链路"]
        direction TB
        G1(["Handler → Service"])
        G2["if err != nil { return }"]
        G3["Service → Repository"]
        G4["if err != nil { return }"]
        G5["fmt.Errorf('...: %w', err)"]
        G6["Repository → DB"]
        G7(["❌ sql.ErrNoRows"])
        G1 --> G2 --> G3 --> G4 --> G5 --> G6 --> G7
    end

关键差异:

  • Java:异常打断正常控制流,沿着调用栈向上弹,谁 catch 谁处理。可能被中间某层的全局异常处理器截获,很难追踪。
  • Go:error 就是返回值,每一层显式检查、显式传递、显式包装。调用链上一层一个 if err != nil ,没有隐藏的控制流。

panic/recover —— 最后的保险

Go 也有类似异常的东西—— panic ,但它 不是用来处理业务错误的

// panic 用于真正的不可恢复错误
func mustCompileRegex(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(err) // 正则写错了,程序没法继续,直接挂
    }
    return re
}

// recover 只在 defer 里有效
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复自 panic:", r)
        }
    }()
    panic("出大事了") // 不会让程序崩溃
}

⚠️ 新手提示:Go 的 panic 不要当成 Java 的 throw 来用。业务错误用 error 返回值, panic 只给真正的不可恢复错误(数组越界、配置文件不存在等)。你写了一百次 if err != nil 很烦,但这正是 Go 的设计意图——错误处理是正常的控制流,不是"异常"。

struct vs class:没有类的面向对象

Java 的世界观是 class。Go 的世界观是 struct。

// Java —— class
public class User {
    private Long id;
    private String name;
    private int age;

    public User(Long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public boolean isAdult() {
        return age >= 18;
    }
}

var user = new User(1L, "张三", 25);
System.out.println(user.isAdult());
// Go —— struct
type User struct {
    ID   int64
    Name string
    age  int // 小写开头 = 包内私有
}

// 构造函数惯例:New + 类型名
func NewUser(id int64, name string, age int) *User {
    return &User{ID: id, Name: name, age: age}
}

// 方法绑定在 struct 上(receiver)
func (u *User) IsAdult() bool {
    return u.age >= 18
}

user := NewUser(1, "张三", 25)
fmt.Println(user.IsAdult())

几个关键差异:

概念JavaGo
类型定义classtype xxx struct
构造new Xxx() / Builder 模式惯例 NewXxx() 工厂函数
方法是类型的一部分方法定义在类内部方法通过 receiver 绑定在 struct 外部
访问控制public / private / protected首字母大小写(大写 = 公开)
this / selfthis 关键字receiver 参数名(惯例用类型首字母小写)
私有属性private小写开头 + 包级可见(同包内可见)
继承extends无!用 struct 嵌入代替

访问控制:大写 = public

Go 的访问控制极其简单——首字母大写就是公开,小写就是包内私有

type User struct {
    ID   int64  // 大写 = 其他包可以访问
    Name string // 大写 = 公开
    age  int    // 小写 = 只在 user 包内可访问
}

func NewUser() *User { // 大写 = 公开函数
    return &User{}
}

func (u *User) validate() error { // 小写 = 包内方法
    // ...
}

没有 protected ,没有 package-private 关键字——所有小写开头的标识符在同一个包内互相可见

包管理与构建:从 Maven 到 Go Modules

Java 程序员最熟悉的两个命令大概是 mvn clean installmvn spring-boot:run 。Go 的对等命令:

操作MavenGradleGo
初始化项目手写 pom.xmlgradle initgo mod init github.com/user/project
添加依赖编辑 pom.xml → mvn install编辑 build.gradlego get github.com/gin-gonic/gin@v1.9
下载所有依赖mvn dependency:resolvegradle buildgo mod download
清理无用依赖手动删手动删go mod tidy
编译mvn compilegradle compileJavago build
运行测试mvn testgradle testgo test ./...
打包mvn packagegradle buildgo build -o app
依赖文件pom.xmlbuild.gradlego.mod
校验和锁定pom.xml<dependencyManagement>gradle.lockfilego.sum
查看依赖树mvn dependency:treegradle dependenciesgo mod graph

go.mod 相当于 pom.xml ,但 简洁到令人感动

module github.com/example/myapp

go 1.22

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/go-sql-driver/mysql v1.7.1
)

go.sum 记录了每个依赖的 SHA-256 哈希,确保你下载的代码和上次构建时一模一样——类似 Maven 的 <dependencyManagement> 锁定版本号的效果,但更安全(直接校验内容而非版本号)。

Go 没有中央仓库的概念——依赖直接从 Git 仓库拉。 github.com/gin-gonic/gin 既是一个 import 路径,也是真实的代码仓库地址。 不需要发布到 Maven Central,打个 tag 就发布了。

flowchart TD
    Maven(["☕ Maven 依赖流"]) --> MC["1. 编辑 pom.xml\n添加 groupId/artifactId/version"]
    MC --> MC2["2. mvn install\n从 Maven Central 下载 jar"]
    MC2 --> MC3["3. ~/.m2/repository\n本地缓存"]
    
    GoMod(["🐹 Go Modules"]) --> GM1["1. go get github.com/xx/yy@v1.0"]
    GM1 --> GM2["2. 直接从 Git 仓库拉代码"]
    GM2 --> GM3["3. $GOPATH/pkg/mod\n本地缓存"]
    GM2 --> GM4["4. go.sum 锁定 SHA-256"]
    
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
    
    class Maven,GoMod root
    class MC,MC2,MC3,GM1,GM2,GM3,GM4 leaf

总结

下表覆盖了 Java 开发者最容易困惑的 Go 语法点:

场景JavaGo
变量声明var name = "foo"name := "foo"
常量final String X = "x"const X = "x"
for 循环for (int i=0; i<10; i++)for i := 0; i < 10; i++ (无括号)
for-eachfor (var v : list)for _, v := range list
whilewhile (cond)for cond
死循环while (true)for {}
switch每个 case 需 break自动 break, fallthrough 穿透
函数public int add(int a, int b)func add(a, b int) int
多返回值需包装类或异常func div(a,b int) (int, error)
资源清理try-with-resources / finallydefer f.Close()
异常try-catch-finallyif err != nil { return err }
类型定义class Usertype User struct
构造new User(1L, "张三")NewUser(1, "张三")
公开/私有public / private首字母大小写
依赖管理pom.xml + Maven Centralgo.mod + 直接 Git 拉取

一句话总结:Go 的语法不是删了 Java 的东西,是用更少的关键字提供了同样的表达能力 。多返回值替代了 try-catch 和包装类,defer 替代了 finally 和 try-with-resources,首字母大小写替代了四个访问修饰符。

📖 下一步阅读:语法对比看完了,下一篇进入 Go 最核心的武器——Go 并发编程:Goroutine、Channel 与 CSP 模型。goroutine 和 Java 虚拟线程到底差在哪?channel 如何替代你的 BlockingQueue?select 怎么写超时控制?用写了 5 年线程池的经验来理解 Go 并发。


参考资源