一、为什么 Java 开发者学 Go 会觉得"简单又陌生"?
Java 和 Go 都是静态类型、编译型语言,都支持并发、接口和垃圾回收。因此,Java 开发者学习 Go 的难点通常不是看不懂代码,而是要适应几种不同的设计:
- Go 没有
class、继承和构造器,更多使用struct、组合和接口。 - Go 强调显式处理错误,不使用 Java 那样的受检异常体系。
- Go 的并发是语言级能力,
goroutine和channel比创建线程更轻量。 - Go 的代码格式、未使用变量、未使用
import都由工具严格约束。
可以先记住一句话:
Java 更强调面向对象的类型体系,Go 更强调组合、接口和简单直接。
二、先写出第一个 Go 程序
1. Java 与 Go 的 Hello World
Java:
java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, Go");
}
}
Go:
Go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
}
package main 表示这是一个可执行程序,func main() 是程序入口,fmt.Println 相当于 Java 中的 System.out.println。
2. 运行 Go 程序
安装 Go 后,可以直接运行:
go run main.go
初始化项目:
go mod init example.com/hello
go build
Go 使用 go.mod 管理模块和依赖,定位上类似 Maven 的 pom.xml,但文件更加简洁。
三、变量、常量与基本类型
1. 变量声明
Java:
java
int age = 18;
String name = "Tom";
Go 有两种常用写法:
Go
var age int = 18
var name string = "Tom"
// 编译器自动推断类型
score := 95
**:=**同时完成变量声明和赋值,但只能在函数内部使用。
例如:
Go
func main() {
name := "Tom"
age := 18
fmt.Println(name, age)
}
Go 要求局部变量必须使用,否则会编译失败。这可以避免很多无用代码遗留在项目中。
2. 零值
Go 声明变量后会自动获得零值:
var i int // 0
var ok bool // false
var text string // ""
var p *int // nil
Java 中对象引用默认是
null,而 Go 对不同类型都有明确的零值。
3. 常量
Go
const Pi = 3.1415926
const AppName string = "demo"
Go 的常量在编译期确定,不能重新赋值,使用方式类似 Java 的 static final。
4. 常用类型对照
| Java | Go | 说明 |
|---|---|---|
byte |
byte / uint8 |
8 位无符号整数 |
int |
int |
整数,位数与平台相关 |
long |
int64 |
64 位整数 |
float |
float32 |
32 位浮点数 |
double |
float64 |
64 位浮点数 |
boolean |
bool |
布尔值 |
String |
string |
UTF-8 字节序列 |
Object |
any |
interface{} 的别名 |
Go 不支持隐式数值类型转换:
var age int = 18
var total int64 = int64(age)
int 和 int64 之间必须显式转换。
四、流程控制:Go 只有一个循环关键字
1. if 语句
Java:
java
if (age >= 18) {
System.out.println("adult");
}
Go:
Go
if age >= 18 {
fmt.Println("adult")
}
Go 的条件表达式不需要括号,但大括号不能省略。
还可以在条件前声明临时变量:
Go
//获取字符串 name 的长度,并赋值给变量 n
//判断n长度是否大于0
if n := len(name); n > 0 {
fmt.Println("字符串长度:", n)
}
2. for 循环
Go 没有 while 和 do...while,统一使用 for。
普通循环:
Go
for i := 0; i < 5; i++ {
fmt.Println(i)
}
相当于 Java 中的 while:
java
for count < 10 {
count++
}
无限循环:
Go
for {
fmt.Println("持续执行")
break
}
3. switch 语句
Go 的 switch 默认不会自动贯穿,因此通常不需要写 break:
Go
switch day {
case 1:
fmt.Println("Monday")
case 6, 7:
fmt.Println("Weekend")
default:
fmt.Println("Weekday")
}
多个条件可以写在同一个 case 中:
Go
case 6, 7:
fmt.Println("周末")
Go 还支持不写表达式的 switch:
Go
switch {
case score >= 90:
fmt.Println("A")
case score >= 60:
fmt.Println("B")
default:
fmt.Println("C")
}
这种写法类似 Java 中的多组 if...else if。
五、函数:Go 支持多个返回值
1. 基本函数写法
Go
// func 方法名称(参数类型)返回值
func add(a int, b int) int {
return a + b
}
如果多个参数类型相同,可以简写:
Go
func add(a, b int) int {
return a + b
}
调用函数:
Go
//相当于 var int result;
//result = add(10,20)
result := add(10, 20)
fmt.Println(result)
2. 多返回值
这是 Java 开发者需要重点适应的语法:
Go
//返回float64,和erroe
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
//对应返回
return a / b, nil
}
调用:
result, err := divide(10, 2)
if err != nil {
fmt.Println("计算失败:", err)
return
}
fmt.Println(result)
Java 通常需要返回一个对象、使用异常,或者通过其他方式传递多个结果;Go 习惯把结果和错误一起返回。
3. 可变参数
Go
//nums ...int 表示:可以接收 0 个或多个 int 类型的参数
func sum(nums ...int) int {
total := 0
//_ 是索引,这里用不上就写_这个
for _, n := range nums {
total += n
}
return total
}
调用:
Go
result := sum(1, 2, 3, 4)
fmt.Println(result)
4. defer
defer 表示函数结束时执行某段代码:
Go
func demo() {
defer fmt.Println("函数结束时执行")
fmt.Println("执行主要逻辑")
}
defer 常用于关闭文件、释放连接 等操作,使用场景类似 Java 中的 finally。
多个 defer 的执行顺序是后进先出:
Go
func demo() {
defer fmt.Println("第一个 defer")
defer fmt.Println("第二个 defer")
}
执行结果:
Go
第二个 defer
第一个 defer
六、数组、切片和 map
1. 数组
Go 的数组长度是类型的一部分:
Go
var arr [3]int = [3]int{1, 2, 3}
[3]int 和 [4]int 是两种不同的类型。
2. 切片
实际开发中,Go 更常使用切片:
Go
nums := []int{1, 2, 3}
//append是GO的内置函数,而不需要像java一样,需要xx.append()
nums = append(nums, 4)
fmt.Println(nums)
//左闭右开,取1、2索引
fmt.Println(nums[1:3])
输出:
Go
[1 2 3 4]
[2 3]
切片可以理解成一个动态数组,使用体验类似 Java 中的 ArrayList,但它不是一个类。
创建指定长度的切片:
Go
//创建一个长度为3的int切片
// make([]int, 3) 会分配一个底层数组,包含 3 个元素,值默认为0
nums := make([]int, 3)
nums[0] = 10
3. map
Go 中的 map 类似 Java 的 HashMap:
Go
ages := map[string]int{
"Tom": 18,
"Lucy": 20,
}
新增或修改元素:
Go
ages["Tom"] = 19
读取元素:
Go
// 返回两个值:
// - age: 键对应的值(如果存在),否则为值类型的零值
// - exists: bool 类型,true 表示键存在,false 表示不存在
age, exists := ages["Tom"]
if exists {
fmt.Println("Tom 的年龄:", age)
}
删除元素:
Go
delete(ages, "Lucy")
也可以使用 make 创建 map:
Go
//初始化map
scores := make(map[string]int)
scores["Go"] = 100
需要注意,直接向 nil map 写入会触发 panic(程序崩溃):
Go
//var 声明的 map 默认值是 nil,它只声明了变量,但没有分配内存空间
// scores 是 nil(空指针),没有初始化
var scores map[string]int
// 错误:会 panic
scores["Go"] = 100
//所以,要么用make进行初始化
//要么scores := map[string]int{}
但是从 nil map 中读取数据是安全的。
4. range 遍历
遍历切片:
Go
for index, value := range nums {
fmt.Println(index, value)
}
遍历 map:
Go
for key, value := range ages {
fmt.Println(key, value)
}
如果不需要某个返回值,可以使用下划线**_**忽略:
Go
for _, value := range nums {
fmt.Println(value)
}
七、struct、方法与指针
Go 没有 Java 中的 class,但可以使用 struct 组织数据。
1. 定义 struct
Go
type User struct {
Name string
Age int
}
创建对象:
Go
user := User{
Name: "Tom",
Age: 18,
}
fmt.Println(user.Name)
也可以使用位置初始化:
Go
user := User{"Tom", 18}
不过实际项目中更推荐使用字段名初始化,这样可读性更好。
2. 模拟构造器
Go 没有构造器关键字,通常使用普通函数创建对象:
Go
func NewUser(name string, age int) User {
return User{
Name: name,
Age: age,
}
}
调用:
Go
user := NewUser("Tom", 18)
3. 定义方法
Go
//定义Intro方法名,接收参数为User,返回参数为String
func (u User) Intro() string {
// Sprintf 格式化字符串,但不输出到控制台,而是返回
return fmt.Sprintf("我叫 %s,今年 %d 岁", u.Name, u.Age)
}
调用:
Go
user := User{Name: "Tom", Age: 18}
fmt.Println(user.Intro())
4. 指针接收者
Go
func (u *User) Birthday() {
//如果有指针,说明这个值是会影响原来的对象
u.Age++
}
调用:
Go
user := User{Name: "Tom", Age: 18}
user.Birthday()
fmt.Println(user.Age) // 19
User 是值接收者,方法拿到的是对象副本;*User 是指针接收者,可以修改原对象。
Go 会自动完成取地址和解引用,因此通常可以直接写:
Go
user.Birthday()
而不需要手动写 &user。
5. Go 的可见性规则
Go 不使用 public、private、protected。
标识符首字母大写,表示对其他包可见;首字母小写,表示包内私有。
Go
type User struct {
Name string // 对外可见
age int // 包内私有
}
八、接口:Go 的实现是隐式的
Java 中实现接口需要显式写 implements:
java
class Dog implements Animal {
// ...
}
Go 不需要显式声明实现接口:
Go
type Speaker interface {
Speak() string
}
type Dog struct{}
func (Dog) Speak() string {
return "wang"
}
定义一个接收接口参数的函数:
Go
func say(s Speaker) {
fmt.Println(s.Speak())
}
调用:
Go
dog := Dog{}
say(dog)
只要 Dog 拥有接口要求的全部方法,它就自动实现了 Speaker。
这和 Java 最大的区别是:
Java 是"声明我实现了哪个接口",Go 是"只要方法匹配,就自动实现接口"。
Go 的接口通常比较小,例如:
Go
type Reader interface {
Read(p []byte) (n int, err error)
}
小接口有利于降低耦合,让代码更容易测试和扩展。
九、错误处理:if err != nil 是日常代码
Go 通常把可预期的失败作为普通返回值:
Go
//读配置文件,返回data和err
data, err := os.ReadFile("config.json")
//如果err不为空,则返回失败
if err != nil {
return fmt.Errorf("读取配置失败: %w", err)
}
fmt.Println(string(data))
Go 不鼓励通过异常处理普通业务错误,而是显式判断:
Go
result, err := doSomething()
if err != nil {
return err
}
常见经验:
- 能恢复、能提示用户的失败,使用
error返回。 - 真正不可恢复的程序错误,才考虑使用
panic。 - 不要为了少写几行
if而滥用panic。 - 可以使用
%w包装错误,保留原始错误信息。
例如:
Go
return fmt.Errorf("查询用户失败: %w", err)
十、goroutine 与 channel:Go 的并发入门
Java 中常见的并发方式是创建线程或使用线程池;Go 可以使用 go 关键字启动一个 goroutine:
Go
func work() {
fmt.Println("执行任务")
}
func main() {
go work()
time.Sleep(time.Second)
}
上面的代码只是演示语法,生产环境不建议使用 Sleep 等待任务完成。
1. 使用 channel 通信
func main() {
done := make(chan string)
go func() {
done <- "任务完成"
}()
message := <-done
fmt.Println(message)
}
done <- "任务完成" 表示向 channel 发送数据。
message := <-done 表示从 channel 接收数据。
channel 可以用于 goroutine 之间传递数据和同步。可以把它理解为一种类型安全的并发队列,但它不仅能传递数据,也能表达任务是否完成。
2. 使用 WaitGroup
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("任务执行完成")
}()
wg.Wait()
}
WaitGroup 类似于等待多个线程执行完成。
十一、Java 开发者最容易踩的坑
1. 分号不是必须的
Go 会自动插入分号,通常不要手写:
Go
fmt.Println("Hello")
2. 大括号不能单独换行
推荐写法:
Go
if age > 18 {
fmt.Println("adult")
}
不要写成:
Go
if age > 18
{
fmt.Println("adult")
}
3. 未使用变量会导致编译失败
Go
func main() {
name := "Tom"
}
上面的代码无法编译,因为 name 声明后没有使用。
4. 未使用 import 也会导致编译失败
import (
"fmt"
"os"
)
如果代码没有使用 os,程序同样无法编译。
5. 字符串不是字符数组
Go 的字符串底层是 UTF-8 字节序列:
Go
text := "你好"
fmt.Println(len(text))
len(text) 返回的是字节数,不是字符数。
如果需要按 Unicode 字符遍历:
Go
for _, r := range text {
fmt.Println(r)
}
6. range 中的 value 是副本
Go
nums := []int{1, 2, 3}
for _, value := range nums {
value = value * 2
}
fmt.Println(nums) // [1 2 3]
因为 value 是元素副本,所以不会修改原切片。
正确写法:
Go
for i := range nums {
nums[i] = nums[i] * 2
}
7. 不要把 Go 当成"没有 class 的 Java"
Go 的设计重点不是模拟 Java 的继承体系,而是:
- 使用 struct 表示数据
- 使用方法表示行为
- 使用组合代替继承
- 使用小接口降低耦合
- 使用函数和错误返回值简化流程
十二、Java 与 Go 常用语法对照
| 功能 | Java | Go |
|---|---|---|
| 程序入口 | public static void main |
func main() |
| 输出 | System.out.println() |
fmt.Println() |
| 字符串 | String |
string |
| 动态数组 | ArrayList |
slice |
| 哈希表 | HashMap |
map |
| 类 | class |
struct |
| 继承 | extends |
不支持,使用组合 |
| 接口实现 | implements |
隐式实现 |
| 异常处理 | try-catch |
返回 error |
| 延迟执行 | finally |
defer |
| 线程 | Thread、线程池 |
goroutine |
| 并发通信 | 阻塞队列等 | channel |
| 包管理 | Maven、Gradle | Go Modules |
十三、建议的学习顺序
可以按照下面的顺序学习 Go,并尝试实现一个命令行 Todo 项目:
- 变量、基础类型、
if、for、switch - 函数、多返回值、
error、defer - slice、map、struct 和方法
- 包、
go.mod、文件读写和 JSON - 接口、单元测试
- goroutine、channel 和并发控制
- HTTP 服务、数据库操作和项目实战
对于 Java 开发者来说,最好的学习方式不是只看语法,而是选择一个熟悉的小项目,用 Go 重新实现一遍。
例如:
- 用 Go 实现一个 Todo 命令行工具
- 用 Go 实现一个简单 HTTP 接口
- 用 Go 读取 JSON 文件并保存到数据库
- 用 Go 编写一个并发任务处理器
总结
对于 Java 开发者来说,Go 的语法并不复杂,真正需要改变的是编程思维:
- 少一些继承层次,多一些组合
- 少一些异常跳转,多一些显式错误
- 少一些重量级线程管理,多一些 goroutine 和 channel
- 少一些复杂框架配置,多一些简单直接的代码
当你掌握以下五组核心语法后,就已经具备阅读大多数 Go 项目的基础:
struct + method + interface + error + goroutine
接下来可以选择一个熟悉的 Java 小项目,用 Go 重新实现一遍。相比单纯背诵语法,这种方式更容易真正掌握 Go。