Java 开发者的 Go 语法基础:从 0 开始快速上手 Go

一、为什么 Java 开发者学 Go 会觉得“简单又陌生”?

Java 和 Go 都是静态类型、编译型语言,都支持并发、接口和垃圾回收。因此,Java 开发者学习 Go 的难点通常不是看不懂代码,而是要适应几种不同的设计:

  1. Go 没有 class、继承和构造器,更多使用 struct、组合和接口。
  2. Go 强调显式处理错误,不使用 Java 那样的受检异常体系。
  3. Go 的并发是语言级能力,goroutinechannel 比创建线程更轻量。
  4. Go 的代码格式、未使用变量、未使用 import 都由工具严格约束。

可以先记住一句话:

Java 更强调面向对象的类型体系,Go 更强调组合、接口和简单直接。

二、先写出第一个 Go 程序

1. Java 与 Go 的 Hello World

Java:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, 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:

int age = 18;
String name = "Tom";

Go 有两种常用写法:

var age int = 18
var name string = "Tom"

// 编译器自动推断类型
score := 95

:= 同时完成变量声明和赋值,但只能在函数内部使用。

例如:

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. 常量

const Pi = 3.1415926
const AppName string = "demo"

Go 的常量在编译期确定,不能重新赋值,使用方式类似 Java 的 static final

4. 常用类型对照

JavaGo说明
bytebyte / uint88 位无符号整数
intint整数,位数与平台相关
longint6464 位整数
floatfloat3232 位浮点数
doublefloat6464 位浮点数
booleanbool布尔值
StringstringUTF-8 字节序列
Objectanyinterface{} 的别名

Go 不支持隐式数值类型转换:

var age int = 18
var total int64 = int64(age)

intint64 之间必须显式转换。


四、流程控制:Go 只有一个循环关键字

1. if 语句

Java:

if (age >= 18) {
    System.out.println("adult");
}

Go:

if age >= 18 {
    fmt.Println("adult")
}

Go 的条件表达式不需要括号,但大括号不能省略。

还可以在条件前声明临时变量:

//获取字符串 name 的长度,并赋值给变量 n
//判断n长度是否大于0
if n := len(name); n > 0 {
    fmt.Println("字符串长度:", n)
}

2. for 循环

Go 没有 whiledo...while统一使用 for

普通循环:

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

相当于 Java 中的 while

for count < 10 {
    count++
}

无限循环:

for {
    fmt.Println("持续执行")
    break
}

3. switch 语句

Go 的 switch 默认不会自动贯穿,因此通常不需要写 break

switch day {
case 1:
    fmt.Println("Monday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Weekday")
}

多个条件可以写在同一个 case 中:

case 6, 7:
    fmt.Println("周末")

Go 还支持不写表达式的 switch

switch {
case score >= 90:
    fmt.Println("A")
case score >= 60:
    fmt.Println("B")
default:
    fmt.Println("C")
}

这种写法类似 Java 中的多组 if...else if


五、函数:Go 支持多个返回值

1. 基本函数写法

// func  方法名称(参数类型)返回值
func add(a int, b int) int {
    return a + b
}

如果多个参数类型相同,可以简写:

func add(a, b int) int {
    return a + b
}

调用函数:

//相当于 var int result;
//result = add(10,20)
result := add(10, 20)
fmt.Println(result)

2. 多返回值

这是 Java 开发者需要重点适应的语法:

//返回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. 可变参数

//nums ...int 表示:可以接收 0 个或多个 int 类型的参数
func sum(nums ...int) int {
    total := 0
    //_ 是索引,这里用不上就写_这个
    for _, n := range nums {
        total += n
    }

    return total
}

调用:

result := sum(1, 2, 3, 4)
fmt.Println(result)

4. defer

defer 表示函数结束时执行某段代码:

func demo() {
    defer fmt.Println("函数结束时执行")

    fmt.Println("执行主要逻辑")
}

defer 常用于关闭文件、释放连接等操作,使用场景类似 Java 中的 finally

多个 defer 的执行顺序是后进先出:

func demo() {
    defer fmt.Println("第一个 defer")
    defer fmt.Println("第二个 defer")
}

执行结果:

第二个 defer
第一个 defer

六、数组、切片和 map

1. 数组

Go 的数组长度是类型的一部分:

var arr [3]int = [3]int{1, 2, 3}

[3]int[4]int 是两种不同的类型。

2. 切片

实际开发中,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])

输出:

[1 2 3 4]
[2 3]

切片可以理解成一个动态数组,使用体验类似 Java 中的 ArrayList,但它不是一个类。

创建指定长度的切片:

//创建一个长度为3的int切片
// make([]int, 3) 会分配一个底层数组,包含 3 个元素,值默认为0
nums := make([]int, 3)
nums[0] = 10

3. map

Go 中的 map 类似 Java 的 HashMap

ages := map[string]int{
    "Tom":  18,
    "Lucy": 20,
}

新增或修改元素:

ages["Tom"] = 19

读取元素:

// 返回两个值:
//   - age: 键对应的值(如果存在),否则为值类型的零值
//   - exists: bool 类型,true 表示键存在,false 表示不存在
age, exists := ages["Tom"]

if exists {
    fmt.Println("Tom 的年龄:", age)
}

删除元素:

delete(ages, "Lucy")

也可以使用 make 创建 map:

//初始化map
scores := make(map[string]int)
scores["Go"] = 100

需要注意,直接向 nil map 写入会触发 panic(程序崩溃):

//var 声明的 map 默认值是 nil,它只声明了变量,但没有分配内存空间
// scores 是 nil(空指针),没有初始化
var scores map[string]int

// 错误:会 panic
scores["Go"] = 100

//所以,要么用make进行初始化
//要么scores := map[string]int{}

但是从 nil map 中读取数据是安全的。

4. range 遍历

遍历切片:

for index, value := range nums {
    fmt.Println(index, value)
}

遍历 map:

for key, value := range ages {
    fmt.Println(key, value)
}

如果不需要某个返回值,可以使用下划线 _ 忽略:

for _, value := range nums {
    fmt.Println(value)
}

七、struct、方法与指针

Go 没有 Java 中的 class,但可以使用 struct 组织数据。

1. 定义 struct

type User struct {
    Name string
    Age  int
}

创建对象:

user := User{
    Name: "Tom",
    Age:  18,
}

fmt.Println(user.Name)

也可以使用位置初始化:

user := User{"Tom", 18}

不过实际项目中更推荐使用字段名初始化,这样可读性更好。

2. 模拟构造器

Go 没有构造器关键字,通常使用普通函数创建对象:

func NewUser(name string, age int) User {
    return User{
        Name: name,
        Age:  age,
    }
}

调用:

user := NewUser("Tom", 18)

3. 定义方法

//定义Intro方法名,接收参数为User,返回参数为String
func (u User) Intro() string {
    // Sprintf 格式化字符串,但不输出到控制台,而是返回
    return fmt.Sprintf("我叫 %s,今年 %d 岁", u.Name, u.Age)
}

调用:

user := User{Name: "Tom", Age: 18}
fmt.Println(user.Intro())

4. 指针接收者

func (u *User) Birthday() {
    //如果有指针,说明这个值是会影响原来的对象
    u.Age++
}

调用:

user := User{Name: "Tom", Age: 18}
user.Birthday()

fmt.Println(user.Age) // 19

User 是值接收者,方法拿到的是对象副本;*User 是指针接收者,可以修改原对象。

Go 会自动完成取地址和解引用,因此通常可以直接写:

user.Birthday()

而不需要手动写 &user

5. Go 的可见性规则

Go 不使用 publicprivateprotected

标识符首字母大写,表示对其他包可见;首字母小写,表示包内私有。

type User struct {
    Name string // 对外可见
    age  int    // 包内私有
}

八、接口:Go 的实现是隐式的

Java 中实现接口需要显式写 implements

class Dog implements Animal {
    // ...
}

Go 不需要显式声明实现接口:

type Speaker interface {
    Speak() string
}

type Dog struct{}

func (Dog) Speak() string {
    return "wang"
}

定义一个接收接口参数的函数:

func say(s Speaker) {
    fmt.Println(s.Speak())
}

调用:

dog := Dog{}
say(dog)

只要 Dog 拥有接口要求的全部方法,它就自动实现了 Speaker

这和 Java 最大的区别是:

Java 是“声明我实现了哪个接口”,Go 是“只要方法匹配,就自动实现接口”。

Go 的接口通常比较小,例如:

type Reader interface {
    Read(p []byte) (n int, err error)
}

小接口有利于降低耦合,让代码更容易测试和扩展。


九、错误处理:if err != nil 是日常代码

Go 通常把可预期的失败作为普通返回值:

//读配置文件,返回data和err
data, err := os.ReadFile("config.json")

//如果err不为空,则返回失败
if err != nil {
    return fmt.Errorf("读取配置失败: %w", err)
}

fmt.Println(string(data))

Go 不鼓励通过异常处理普通业务错误,而是显式判断:

result, err := doSomething()

if err != nil {
    return err
}

常见经验:

  • 能恢复、能提示用户的失败,使用 error 返回。
  • 真正不可恢复的程序错误,才考虑使用 panic
  • 不要为了少写几行 if 而滥用 panic
  • 可以使用 %w 包装错误,保留原始错误信息。

例如:

return fmt.Errorf("查询用户失败: %w", err)

十、goroutine 与 channel:Go 的并发入门

Java 中常见的并发方式是创建线程或使用线程池;Go 可以使用 go 关键字启动一个 goroutine:

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 会自动插入分号,通常不要手写:

fmt.Println("Hello")

2. 大括号不能单独换行

推荐写法:

if age > 18 {
    fmt.Println("adult")
}

不要写成:

if age > 18
{
    fmt.Println("adult")
}

3. 未使用变量会导致编译失败

func main() {
    name := "Tom"
}

上面的代码无法编译,因为 name 声明后没有使用。

4. 未使用 import 也会导致编译失败

import (
    "fmt"
    "os"
)

如果代码没有使用 os,程序同样无法编译。

5. 字符串不是字符数组

Go 的字符串底层是 UTF-8 字节序列:

text := "你好"

fmt.Println(len(text))

len(text) 返回的是字节数,不是字符数。

如果需要按 Unicode 字符遍历:

for _, r := range text {
    fmt.Println(r)
}

6. range 中的 value 是副本

nums := []int{1, 2, 3}

for _, value := range nums {
    value = value * 2
}

fmt.Println(nums) // [1 2 3]

因为 value 是元素副本,所以不会修改原切片。

正确写法:

for i := range nums {
    nums[i] = nums[i] * 2
}

7. 不要把 Go 当成“没有 class 的 Java”

Go 的设计重点不是模拟 Java 的继承体系,而是:

  • 使用 struct 表示数据
  • 使用方法表示行为
  • 使用组合代替继承
  • 使用小接口降低耦合
  • 使用函数和错误返回值简化流程

十二、Java 与 Go 常用语法对照

功能JavaGo
程序入口public static void mainfunc main()
输出System.out.println()fmt.Println()
字符串Stringstring
动态数组ArrayListslice
哈希表HashMapmap
classstruct
继承extends不支持,使用组合
接口实现implements隐式实现
异常处理try-catch返回 error
延迟执行finallydefer
线程Thread、线程池goroutine
并发通信阻塞队列等channel
包管理Maven、GradleGo Modules

十三、建议的学习顺序

可以按照下面的顺序学习 Go,并尝试实现一个命令行 Todo 项目:

  1. 变量、基础类型、ifforswitch
  2. 函数、多返回值、errordefer
  3. slice、map、struct 和方法
  4. 包、go.mod、文件读写和 JSON
  5. 接口、单元测试
  6. goroutine、channel 和并发控制
  7. HTTP 服务、数据库操作和项目实战

对于 Java 开发者来说,最好的学习方式不是只看语法,而是选择一个熟悉的小项目,用 Go 重新实现一遍。

例如:

  • 用 Go 实现一个 Todo 命令行工具
  • 用 Go 实现一个简单 HTTP 接口
  • 用 Go 读取 JSON 文件并保存到数据库
  • 用 Go 编写一个并发任务处理器

总结

对于 Java 开发者来说,Go 的语法并不复杂,真正需要改变的是编程思维:

  • 少一些继承层次,多一些组合
  • 少一些异常跳转,多一些显式错误
  • 少一些重量级线程管理,多一些 goroutine 和 channel
  • 少一些复杂框架配置,多一些简单直接的代码

当你掌握以下五组核心语法后,就已经具备阅读大多数 Go 项目的基础:

struct + method + interface + error + goroutine

接下来可以选择一个熟悉的 Java 小项目,用 Go 重新实现一遍。相比单纯背诵语法,这种方式更容易真正掌握 Go。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值