go 和 java 两者同属静态类型、编译型语言,核心工程理念相通。但Go以"简洁、高效、轻量工程化"为核心设计,剥离了Java中复杂的语法糖与冗余设计。本篇聚焦入门核心知识点,通过"Java对比+可运行Go代码"的模式,快速建立Go语言认知。
一、Go语言核心定位与Java核心差异
对比Java,Go的核心差异集中在三点
- 语法风格:Go摒弃了Java的类继承、复杂泛型(1.18后引入泛型但用法极简)、注解等厚重特性,采用"结构体+接口"实现组合式编程,代码更简洁直观;也就是显式编程代替 AOP、IOC等隐式方式
- 编译方式:Go直接编译为机器码,无需JVM依赖,启动速度快、内存占用低;Java需先编译为字节码,再通过JVM运行,跨平台性强但存在一定运行时开销;
- 工程理念:Go内置go mod、go build等工程化工具,无需额外依赖(类似Java的Maven/Gradle),实现"开箱即用";Java则需依赖构建工具和大量第三方库完成工程化配置。
总结: JAVA 的生态和扩展性比 go 强大,但是 go由于舍弃掉了这些隐式编程以及外部构建工具等,因此性能上会更加强大,其中的协程也是 go 语言的立足根本
二、Go保留关键字:精简语法与Java对标
Go仅含25个关键字(Java有53个),高频常用关键字可直接对标Java语法,核心差异与对应用法如下表,方便快速对照理解:
|-----------|-----------------------------|---------------------------------|---------------------------------------------|------------------------------------------------------------------------------|
| Go关键字 | Java对应语法 | 作用说明 | Go代码示例 | Java对应代码 |
| var | 显式类型声明(如int a) | 声明变量,支持类型后置 | var name string = "GoLang" | String name = "GoLang"; |
| func | 方法/函数定义(如public void xxx()) | 定义函数或结构体方法,无访问修饰符(靠包控制访问) | func sayHello() { fmt.Println("Hello Go") } | public void sayHello() { System.out.println("Hello Go"); } |
| struct | POJO类(无继承的简单JavaBean) | 定义结构体,用于封装数据(类似Java的无继承类) | type User struct { Name string; Age int } | public class User { private String name; private int age; // getter/setter } |
| if | if条件判断 | 逻辑一致,但Go无需括号包裹条件表达式 | if age > 18 { fmt.Println("成年") } | if (age > 18) { System.out.println("成年"); } |
| for | for循环 | Go用for替代Java的for、while、do-while | for i := 0; i < 3; i++ { fmt.Println(i) } | for (int i = 0; i < 3; i++) { System.out.println(i); } |
说明:Go没有public/private/protected关键字,通过"首字母大小写"控制访问权限------首字母大写可跨包访问(类似public),小写仅包内可见(类似private)。
三、Go数据类型:熟悉基础与关键差异
Go的数据类型体系和Java高度重叠,但在声明语法、使用规则上有明确差异,重点关注以下核心类型:
3.1 基础类型(int/string/bool等)
核心差异:Go的数值类型更精细(区分位数),string类型不可变且拼接更简洁,无自动类型转换。
package main
import "fmt"
func main() {
// 1. 字符串:不可变(和Java String一致),拼接直接用+
str1 := "Java"
str2 := "转Go"
fmt.Println(str1 + str2) // 输出:Java转Go
// 对比Java:String str1 = "Java"; String str2 = "转Go"; System.out.println(str1 + str2);
// 2. 数值类型:区分int8/int16/int32/int64(对应Java的byte/short/int/long)
var num1 int32 = 100 // 明确32位整数
var num2 int64 = 200 // 明确64位整数
// 注意:Go无自动类型转换,num1 + num2会编译报错(需显式转换)
fmt.Println(int64(num1) + num2) // 输出:300
// 对比Java:int num1 = 100; long num2 = 200; System.out.println(num1 + num2);(自动提升为long)
// 3. 布尔类型:仅bool,无0/1替代(Java可通过0/1间接表示)
var flag bool = true
fmt.Println(flag) // 输出:true
}
3.2 复合类型:数组(固定长度)
Go的数组和Java数组核心一致------都是固定长度、连续内存,但声明语法相反(Go是"长度+类型",Java是"类型+[]")。
package main
import "fmt"
func main() {
// Go数组声明:[长度]类型,初始化后长度不可变
var arr [3]int = [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出:1
fmt.Println(len(arr)) // 输出:3(len()是Go内置函数,获取长度)
// 简化初始化:省略长度,用...自动推导
arr2 := [...]int{4, 5, 6}
fmt.Println(len(arr2)) // 输出:3
// 对比Java:
// int[] arr = new int[]{1,2,3};
// System.out.println(arr[0]);
// System.out.println(arr.length);
}
四、Go操作符:重点差异与实用用法
Go的算术运算符(+、-、*、/)、逻辑运算符(&&、||、!)和Java完全一致,重点关注2个特有/差异操作符:
4.1 短变量声明符 :=
替代var的简化写法,自动推导变量类型,仅能在函数内使用(最常用的Go语法之一)。
package main
import "fmt"
func main() {
// 短变量声明::= 自动推导类型
name := "张三" // 等价于 var name string = "张三"
age := 25 // 等价于 var age int = 25
fmt.Println(name, age) // 输出:张三 25
// 对比Java:必须显式声明类型
// String name = "张三";
// int age = 25;
// System.out.println(name + " " + age);
}
4.2 比较运算符 ==:支持结构体/数组直接比较
Go的==除了基础类型比较,还支持数组、结构体的"值比较"(Java需重写equals方法才能实现)。
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
// 1. 数组比较:长度和元素完全一致则返回true
arr1 := [3]int{1,2,3}
arr2 := [3]int{1,2,3}
fmt.Println(arr1 == arr2) // 输出:true
// 2. 结构体比较:所有字段值一致则返回true
u1 := User{Name: "张三", Age: 25}
u2 := User{Name: "张三", Age: 25}
fmt.Println(u1 == u2) // 输出:true
// 对比Java:
// 数组:new int[]{1,2,3} == new int[]{1,2,3} → false(比较地址)
// 结构体(类):new User("张三",25) == new User("张三",25) → false(需重写equals)
}
注意:Go的切片(slice)、字典(map)不支持直接用==比较(编译报错),需手动遍历元素判断。
五、Go错误处理:与Java try-catch的本质区别
Go没有try-catch-finally异常机制,而是通过"error接口"和"if err != nil"的显式判断处理错误,核心思路是"将错误视为普通返回值"。
核心逻辑
-
Go的error是一个接口(类似Java的Exception),任何类型只要实现Error() string方法,就是一个error;
-
函数通过"多返回值"返回结果和错误(Go支持多返回值,Java需封装为对象);
-
调用函数后,优先判断err是否为nil(nil等价于Java的null),非nil则表示有错误。
package main
import (
"errors"
"fmt"
)// 模拟用户查询:返回用户信息和错误
func getUserById(id int) (User, error) {
if id <= 0 {
// 生成错误对象(errors.New是Go内置方法)
return User{}, errors.New("用户ID必须大于0")
}
// 正常返回:错误为nil
return User{Name: "张三", Age: 25}, nil
}type User struct {
Name string
Age int
}func main() {
// 调用函数,接收结果和错误
user, err := getUserById(-1)
// 优先判断错误
if err != nil {
fmt.Println("查询失败:", err) // 输出:查询失败: 用户ID必须大于0
return
}
// 无错误则处理结果
fmt.Println("查询成功:", user)// 对比Java try-catch: // public static User getUserById(int id) throws Exception { // if (id <= 0) { // throw new Exception("用户ID必须大于0"); // } // return new User("张三", 25); // } // // public static void main(String[] args) { // try { // User user = getUserById(-1); // System.out.println("查询成功:" + user); // } catch (Exception e) { // System.out.println("查询失败:" + e.getMessage()); // } // }}
六、Go字典(map):对标HashMap与并发安全方案
Go的map是"键值对"集合,核心特性和Java的HashMap一致(无序、键唯一、线程不安全),但声明和操作语法更简洁。
核心操作:定义、CRUD
package main
import "fmt"
func main() {
// 1. 定义map:map[键类型]值类型
// 方式1:先声明,后初始化
var userMap map[int]User
userMap = make(map[int]User, 10) // make是Go内置函数,用于初始化map(指定初始容量10)
// 方式2:声明+初始化一步到位
userMap2 := map[int]User{
1: {Name: "张三", Age: 25},
2: {Name: "李四", Age: 30},
}
// 2. 新增/修改(键存在则修改,不存在则新增)
userMap[1] = User{Name: "张三", Age: 25}
userMap[1] = User{Name: "张三_更新", Age: 26} // 修改
// 3. 查询:返回值+是否存在(Go特有,避免键不存在时返回零值的歧义)
user, exists := userMap[1]
if exists {
fmt.Println("查询到用户:", user) // 输出:查询到用户: {张三_更新 26}
} else {
fmt.Println("用户不存在")
}
// 4. 删除:delete( map, 键 )
delete(userMap, 1)
_, exists = userMap[1]
fmt.Println("删除后是否存在:", exists) // 输出:false
// 对比Java HashMap:
// Map<Integer, User> userMap = new HashMap<>(10);
// userMap.put(1, new User("张三",25));
// userMap.put(1, new User("张三_更新",26));
//
// User user = userMap.get(1);
// if (user != null) {
// System.out.println("查询到用户:" + user);
// }
//
// userMap.remove(1);
// System.out.println("删除后是否存在:" + userMap.containsKey(1));
}
说明:Go的map初始化必须用make(或直接字面量赋值),否则为nil,无法直接新增元素(会panic,类似Java的NullPointerException)。
6.2 并发场景下map的数据安全保障
Go的原生map并非线程安全(类似Java的HashMap),在并发读写时会触发panic。针对并发场景,有两种主流解决方案,可对标Java的ConcurrentHashMap:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 方案1:使用sync.Mutex加锁(类似Java的Collections.synchronizedMap)
var mu sync.Mutex
safeMap := make(map[int]string)
// 并发写入
for i := 0; i < 5; i++ {
go func(idx int) {
mu.Lock() // 加锁保护临界区
defer mu.Unlock() // 延迟解锁,确保函数退出时释放
safeMap[idx] = fmt.Sprintf("value%d", idx)
}(i)
}
time.Sleep(100 * time.Millisecond)
mu.Lock()
fmt.Println("sync.Mutex保护的map:", safeMap)
mu.Unlock()
// 方案2:使用sync.Map(Go1.9+内置,类似Java的ConcurrentHashMap)
var syncMap sync.Map
// 并发写入
for i := 0; i < 5; i++ {
go func(idx int) {
syncMap.Store(idx, fmt.Sprintf("syncValue%d", idx)) // 内置线程安全存储方法
}(i)
}
time.Sleep(100 * time.Millisecond)
// 遍历sync.Map
syncMap.Range(func(key, value interface{}) bool {
fmt.Printf("sync.Map: key=%d, value=%s\n", key, value)
return true
})
}
核心对比:Java中需显式使用ConcurrentHashMap保证并发安全,而Go提供"锁+原生map"和"sync.Map"两种选择------sync.Map针对"读多写少"场景做了优化,无需初始化容量,性能更贴近ConcurrentHashMap;锁方案则更灵活,适配复杂并发逻辑。
七、Go切片(slice):对标ArrayList与并发安全保障
Go的切片是"动态数组",核心特性和Java的ArrayList一致(自动扩容、有序、可动态增删),是Go中最常用的集合类型(比数组更灵活)。
核心认知:切片本质是"数组的视图",包含三个属性------指向底层数组的指针、长度(len)、容量(cap),扩容时会自动创建新的底层数组。
核心操作:定义、扩容、CRUD
package main
import "fmt"
func main() {
// 1. 定义切片:[]类型(无长度,区别于数组)
// 方式1:make初始化(指定长度和容量,容量可选)
slice1 := make([]int, 3, 5) // 长度3,容量5(底层数组长度5)
slice1[0] = 1
slice1[1] = 2
slice1[2] = 3
// 方式2:字面量初始化(长度=容量=元素个数)
slice2 := []int{1, 2, 3}
// 2. 新增元素:append(切片, 元素)(返回新切片,必须接收)
slice2 = append(slice2, 4, 5)
fmt.Println("slice2新增后:", slice2) // 输出:[1 2 3 4 5]
fmt.Println("slice2长度:", len(slice2)) // 输出:5
fmt.Println("slice2容量:", cap(slice2)) // 输出:6(扩容后,默认扩容策略:容量<1024时翻倍,否则增50%)
// 3. 查询:和数组一致,通过索引
fmt.Println("slice2[0]:", slice2[0]) // 输出:1
// 4. 删除元素:Go无内置delete,需通过切片截取实现
// 删除索引2的元素(3):slice = append(slice[:index], slice[index+1:]...)
slice2 = append(slice2[:2], slice2[3:]...)
fmt.Println("slice2删除后:", slice2) // 输出:[1 2 4 5]
// 5. 切片截取:slice[start:end](左闭右开,不包含end)
subSlice := slice2[1:3] // 从索引1到2(元素2、4)
fmt.Println("子切片:", subSlice) // 输出:[2 4]
// 对比Java ArrayList:
// List<Integer> list = new ArrayList<>(5);
// list.add(1);
// list.add(2);
// list.add(3);
// list.add(4);
// list.add(5);
// System.out.println("list新增后:" + list);
// System.out.println("list长度:" + list.size());
// System.out.println("list.get(0):" + list.get(0));
// list.remove(2);
// System.out.println("list删除后:" + list);
}
关键区别:Java的ArrayList通过size()获取长度、ensureCapacity()手动扩容;Go的切片通过len()获取长度、cap()获取容量,append时自动扩容,无需手动干预。
7.2 并发场景下slice的数据安全保障
切片本身也非线程安全,并发读写(尤其是写操作)会导致数据竞争、元素错乱。核心解决方案是通过同步锁(sync.Mutex/sync.RWMutex)保护切片操作,类似Java中用Collections.synchronizedList包装ArrayList:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 用sync.RWMutex保护切片(读多写少场景推荐,读锁可共享,写锁排他)
var rwMu sync.RWMutex
safeSlice := make([]int, 0, 5)
// 并发写入
for i := 0; i < 5; i++ {
go func(idx int) {
rwMu.Lock() // 写操作加写锁
defer rwMu.Unlock()
safeSlice = append(safeSlice, idx)
}(i)
}
time.Sleep(100 * time.Millisecond)
// 并发读取
for i := 0; i < 3; i++ {
go func(idx int) {
rwMu.RLock() // 读操作加读锁
defer rwMu.RUnlock()
fmt.Printf("goroutine%d 读取切片:%v\n", idx, safeSlice)
}(i)
}
time.Sleep(100 * time.Millisecond)
}
补充说明:Java的CopyOnWriteArrayList通过"写时复制"实现并发安全,适合读多写少场景;Go中无内置的"并发安全切片",需手动用锁实现,或基于切片封装自定义安全结构,灵活性高于Java的封装类。