目录
[1.1 Go安装与配置](#1.1 Go安装与配置)
[1.2 GOPATH vs Go Modules](#1.2 GOPATH vs Go Modules)
[1.3 快速体验:Hello World](#1.3 快速体验:Hello World)
[2.1 变量声明](#2.1 变量声明)
[2.2 基本类型](#2.2 基本类型)
[2.3 常量与枚举](#2.3 常量与枚举)
[2.4 流程控制](#2.4 流程控制)
[3.1 函数定义](#3.1 函数定义)
[3.2 方法与接收者](#3.2 方法与接收者)
[3.3 指针接收者 vs 值接收者](#3.3 指针接收者 vs 值接收者)
[4.1 数组](#4.1 数组)
[4.2 切片](#4.2 切片)
[4.3 append机制](#4.3 append机制)
[5.1 Map的基本使用](#5.1 Map的基本使用)
[5.2 并发安全问题](#5.2 并发安全问题)
[6.1 Go指针 vs Java引用](#6.1 Go指针 vs Java引用)
[6.2 指针的使用场景](#6.2 指针的使用场景)
[6.3 避免指针陷阱](#6.3 避免指针陷阱)
写在前面
云原生时代,Go 语言已然成为容器编排、微服务、Service Mesh 等领域的事实标准。对 Java 工程师来说,掌握 Go 不只是多掌握一门语言,更是面向云原生架构的关键能力升级。
Go 的设计理念与 Java 差异显著:它崇尚极简、高效与工程友好,舍弃了 Java 中诸多繁复的语法与特性。为了让你用最短时间、最低成本从 Java 平滑过渡到 Go,本系列博客将全程以 Java 工程师的视角,通过大量对比、对标学习的方式,带你快速上手 Go。
作为系列第一篇,本文将从 Go 基础语法入手,帮你快速建立对 Go 的直观认知,轻松迈出从 Java 转向 Go 的第一步。
一、开发环境与工具链
1.1 Go安装与配置
Go的安装比JDK更简单。从官网下载对应平台的安装包,一路"下一步"即可完成。安装后需要配置两个环境变量:
GOROOT:Go的安装路径(类似JAVA_HOME)GOPATH:Go的工作空间(类似Maven的本地仓库)
不过,现代Go开发已经不再强制要求配置这些环境变量,Go会使用默认值。
对比Java:
- Java需要安装JDK,配置JAVA_HOME、CLASSPATH
- Go只需要安装Go,配置更简单
- Go的编译速度远快于Java,这是Go的一大优势
1.2 GOPATH vs Go Modules
Go的依赖管理经历了两个阶段:
GOPATH时代:
- 所有项目共享一个工作空间
- 依赖下载到
$GOPATH/src下 - 没有版本管理,类似早期的Java项目
Go Modules时代(Go 1.11+):
- 每个项目独立管理依赖
go.mod文件定义依赖(类似pom.xml)go.sum记录依赖校验(类似Maven的依赖锁定)
对比Maven/Gradle:
|------|------------------|----------------------------|---------------|
| 特性 | Go Modules | Maven | Gradle |
| 配置文件 | go.mod | pom.xml | build.gradle |
| 依赖格式 | module@version | groupId:artifactId:version | 同Maven |
| 仓库 | proxy.golang.org | Maven Central | Maven Central |
| 传递依赖 | 自动管理 | 自动管理 | 自动管理 |
Go Modules的配置更简洁,依赖格式也更直观。
1.3 快速体验:Hello World
Go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
对比Java:
java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
差异一目了然:
- Go不需要类,直接定义函数
- Go的
package main表示可执行程序 - Go的
import语法更简洁 - Go不需要分号(编译器自动添加)
二、基础语法对比
2.1 变量声明
Go的变量声明有多种方式:
Go
// 完整声明
var name string = "Go"
// 类型推断
var name = "Go"
// 短变量声明(最常用)
name := "Go"
// 多变量声明
var (
name string = "Go"
age int = 10
active bool = true
)
对比Java:
java
String name = "Java";
var name = "Java"; // Java 10+
核心差异:
- Go的类型在变量名之后(
var name string),Java在之前(String name) - Go的
:=语法糖可以省略var和类型 - Go有
var()批量声明语法
2.2 基本类型
Go的基本类型非常简洁:
Go
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8的别名
rune // int32的别名,表示Unicode码点
float32 float64
complex64 complex128
对比Java:
|---------|---------|---------------------|
| Go类型 | Java类型 | 说明 |
| int | int | Go的int根据平台可能是32或64位 |
| int32 | int | Java的int固定32位 |
| float64 | double | Go默认float64 |
| string | String | Go的string是不可变的 |
| bool | boolean | 相同 |
关键差异:
- Go没有包装类型(Integer、Long等),只有基本类型
- Go的int根据平台自适应,Java的int固定32位
- Go有复数类型(complex),Java没有
2.3 常量与枚举
Go的常量:
Go
const Pi = 3.14159
const (
StatusOK = 200
StatusError = 500
)
Go没有enum关键字,但可以用iota实现枚举:
Go
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
对比Java:
java
enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}
Go的iota更灵活,可以定义复杂的枚举表达式,但不如Java的enum类型安全。
2.4 流程控制
if语句:
Go
// Go
if age >= 18 {
fmt.Println("成年")
}
// if语句可以包含初始化语句
if age := getAge(); age >= 18 {
fmt.Println("成年")
}
java
// Java
if (age >= 18) {
System.out.println("成年");
}
for循环:
Go只有for,没有while:
Go
// 标准for循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 类似while
for i < 10 {
fmt.Println(i)
i++
}
// 无限循环
for {
// break退出
}
// for-range遍历
for index, value := range slice {
fmt.Println(index, value)
}
对比Java:
java
// 标准for循环
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// while循环
while (i < 10) {
System.out.println(i);
i++;
}
// 增强for循环
for (String value : list) {
System.out.println(value);
}
switch语句:
Go
// Go的switch默认break
switch day {
case "Monday":
fmt.Println("周一")
case "Friday":
fmt.Println("周五")
default:
fmt.Println("其他")
}
// 需要穿透使用fallthrough
switch num {
case 1:
fmt.Println("1")
fallthrough
case 2:
fmt.Println("2")
}
对比Java:
java
// Java的switch默认穿透
switch (day) {
case "Monday":
System.out.println("周一");
break;
case "Friday":
System.out.println("周五");
break;
default:
System.out.println("其他");
}
核心差异:
- Go的if不需要括号,但必须有花括号
- Go只有for,没有while
- Go的switch默认break,Java默认穿透
- Go的switch可以不写表达式,当作if-else使用
三、函数与方法
3.1 函数定义
Go的函数定义:
Go
// 单返回值
func add(a, b int) int {
return a + b
}
// 多返回值(Go的特色)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
// 命名返回值
func calculate(a, b int) (sum, product int) {
sum = a + b
product = a * b
return // 自动返回sum和product
}
对比Java:
java
// 单返回值
public int add(int a, int b) {
return a + b;
}
// 需要返回多个值时,Java需要定义类或使用数组
public class Result {
public int sum;
public int product;
}
public Result calculate(int a, int b) {
Result result = new Result();
result.sum = a + b;
result.product = a * b;
return result;
}
核心优势:
- Go的多返回值避免了定义额外的类
- 命名返回值让代码更清晰
- 错误处理更直观(返回error)
3.2 方法与接收者
Go没有类,但可以为类型定义方法:
Go
type Person struct {
Name string
Age int
}
// 值接收者
func (p Person) GetName() string {
return p.Name
}
// 指针接收者
func (p *Person) SetAge(age int) {
p.Age = age
}
对比Java:
java
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
}
核心差异:
- Go的方法在结构体外部定义
- Go有值接收者和指针接收者之分
- 指针接收者可以修改结构体,类似Java的引用传递
3.3 指针接收者 vs 值接收者
Go
// 值接收者:不修改原对象
func (p Person) ModifyAge(age int) {
p.Age = age // 修改的是副本
}
// 指针接收者:修改原对象
func (p *Person) SetAge(age int) {
p.Age = age // 修改的是原对象
}
对比Java:
- Java的方法默认是引用传递(对于对象)
- Go需要显式选择值接收者或指针接收者
- 指针接收者更接近Java的行为
四、数组与切片
4.1 数组
Go的数组是固定长度的:
Go
// 声明数组
var arr [5]int
// 初始化
arr := [5]int{1, 2, 3, 4, 5}
// 数组长度是类型的一部分
var arr1 [5]int
var arr2 [10]int
// arr1和arr2是不同类型
对比Java:
java
int[] arr = new int[5];
int[] arr = {1, 2, 3, 4, 5};
核心差异:
- Go的数组长度是类型的一部分(
[5]int和[10]int是不同类型) - Go的数组是值类型,传递时会复制
- Java的数组是引用类型
4.2 切片
切片是Go中最常用的数据结构,是动态数组的实现:
Go
// 创建切片
slice := []int{1, 2, 3, 4, 5}
// 使用make创建
slice := make([]int, 5, 10) // 长度5,容量10
// 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2, 3, 4]
// append追加元素
slice = append(slice, 6)
slice = append(slice, 7, 8, 9)
切片的内部结构:
┌─────────────────────────────────────────┐
│ 切片结构 │
├─────────────────────────────────────────┤
│ │
│ ptr ──────▶ 指向底层数组 │
│ len 切片长度 │
│ cap 切片容量 │
│ │
└─────────────────────────────────────────┘
对比Java的ArrayList:
|------|-------|----------------|
| 特性 | Go切片 | Java ArrayList |
| 底层结构 | 数组 | 数组 |
| 扩容机制 | 2倍扩容 | 1.5倍扩容 |
| 类型安全 | 编译时检查 | 编译时检查 |
| 内存布局 | 连续内存 | 连续内存 |
核心差异:
- 切片是对底层数组的引用,多个切片可以共享底层数组
- 切片的扩容会创建新数组,原切片不受影响
- 切片更轻量,传递时只传递三个字段(ptr、len、cap)
4.3 append机制
Go
slice := make([]int, 0, 5)
// 容量足够,不扩容
slice = append(slice, 1, 2, 3, 4, 5)
// 容量不足,扩容
slice = append(slice, 6) // 创建新数组,容量翻倍
扩容策略:
- 容量小于1024:翻倍
- 容量大于等于1024:增长25%
五、Map与集合
5.1 Map的基本使用
Go
// 创建map
m := make(map[string]int)
// 初始化
m := map[string]int{
"apple": 5,
"banana": 3,
}
// 增删改查
m["orange"] = 10 // 增/改
delete(m, "apple") // 删
value := m["banana"] // 查
// 检查key是否存在
value, exists := m["grape"]
if exists {
fmt.Println(value)
}
对比Java:
java
Map<String, Integer> m = new HashMap<>();
m.put("apple", 5); // 增/改
m.remove("apple"); // 删
Integer value = m.get("banana"); // 查
// 检查key是否存在
if (m.containsKey("grape")) {
System.out.println(m.get("grape"));
}
核心差异:
- Go的map通过返回两个值来判断key是否存在
- Java需要调用
containsKey方法 - Go的map是无序的,Java的HashMap也是无序的
5.2 并发安全问题
Go的map不是并发安全的:
java
// 错误:并发读写map会panic
var m = make(map[int]int)
go func() {
m[1] = 100 // 写
}()
go func() {
_ = m[1] // 读
}()
解决方案:
- 使用
sync.RWMutex加锁 - 使用
sync.Map(Go 1.9+)
对比Java:
- Java的
ConcurrentHashMap是线程安全的 - Go需要手动处理并发安全
六、指针详解
6.1 Go指针 vs Java引用
Go有指针,但没有指针运算:
Go
// 取地址
x := 10
p := &x // p是指向x的指针
// 解引用
fmt.Println(*p) // 10
// 通过指针修改
*p = 20
fmt.Println(x) // 20
对比Java:
java
// Java只有引用,没有指针
Person p1 = new Person();
Person p2 = p1; // p2和p1指向同一个对象
p2.setName("Go");
System.out.println(p1.getName()); // "Go"
核心差异:
- Go的指针可以获取变量的地址
- Go的指针可以解引用获取值
- Java的引用不能获取地址,也不能解引用
- Go的指针不能进行算术运算(比C安全)
6.2 指针的使用场景
场景1:修改函数参数
Go
func increment(num *int) {
*num++
}
x := 10
increment(&x)
fmt.Println(x) // 11
对比Java:
java
// Java无法直接修改基本类型参数
public void increment(int num) {
num++; // 只修改了副本
}
int x = 10;
increment(x);
System.out.println(x); // 仍然是10
// 需要使用包装类或数组
public void increment(int[] num) {
num[0]++;
}
场景2:避免大对象复制
Go
type BigStruct struct {
Data [1000000]int
}
func process(s *BigStruct) {
// 传递指针,避免复制
}
对比Java:
- Java对象默认是引用传递,不存在复制问题
- Go需要显式使用指针来避免复制
6.3 避免指针陷阱
陷阱1:循环变量地址
Go
// 错误:所有指针指向同一个地址
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i)
}
// 正确:每次循环创建新变量
for i := 0; i < 3; i++ {
i := i // 创建新变量
ptrs = append(ptrs, &i)
}
陷阱2:nil指针
Go
var p *int
fmt.Println(*p) // panic: nil pointer dereference
// 正确:检查nil
if p != nil {
fmt.Println(*p)
}
七、总结
Go语言的基础语法相比Java更加简洁:
设计哲学差异:
- Go追求简洁,Java追求严谨
- Go只有25个关键字,Java有50个
- Go的编译速度远快于Java
核心语法差异:
- 变量声明:Go的类型在变量名之后
- 函数:Go支持多返回值
- 数组:Go的数组长度是类型的一部分
- 切片:Go的动态数组实现
- 指针:Go有指针,Java只有引用
思维转换:
- 从面向对象思维转向组合式设计
- 从异常处理转向错误返回值
- 从继承转向接口组合
掌握这些基础语法后,你已经可以编写简单的Go程序了。下一篇,我们将深入Go的高级特性,包括面向对象、并发编程、错误处理等,理解Go的设计哲学。