作为一名Java开发者,我早已习惯了其严谨的对象模型、丰富的API和强大的跨平台能力。然而,当我踏入Go语言的世界时,我被其简洁、高效和直接的风格深深吸引。Go语言,这个由Google精心打造的开源编程语言,不仅为我带来了全新的编程体验,也让我对编程有了更深刻的理解。在学习完Go的基本语法后,我深感两种语言在设计理念、语法特性和应用场景上的巨大差异。Go的简洁语法、强大的并发支持和静态类型安全,让我对编程有了全新的认识。与此同时,我也发现Java在面向对象编程、内存管理和生态系统方面的优势。在这篇博客中,我将分享我从Java转向Go的学习过程,并总结Go和Java之间的主要区别。我将从语法、并发、内存管理、性能以及生态系统等多个方面进行比较,希望能为同样对这两种语言感兴趣的读者提供一些有价值的参考。
基础语法区别
1.数据类型
Java支持byte、short、int、long、double、float、boolean、char等基本数据类型和String、类、数组、枚举、接口等引用数据类型
go支持bool、int、int16、int32,uint .....、byte、float32,float64、complex64、uintptr等基本数据类型,支持string、数组、切片、map、struct等符合数据类型还支持指针、函数、接口、通道、错误等引用类型。
2.变量
Java
//局部变量
int a; //类型 变量名
a = 10;
int a = 10;
//类变量
private int b; //通过private等关键字控制变量的可见性
//常量
public static final String PATH = "xxxx";
public final String HELLO = "Hello World";
go
//局部变量
a int //变量名 数据类型
a = 10 //可以省略类型
a := 10
//go声明变量必须使用否则会报错
//全局变量
var b int
var Name string //通过首字母大小写控制外部包对变量的可见性
//常量
const PATH string = "xxx"
const NAME = "xxxx"
3.枚举
Java
public enum Color {
RED,
GREEN,
BLUE;
// 还可以为枚举类型添加方法
public static void printAllColors() {
for (Color color : Color.values()) {
System.out.println(color);
}
}
// 还可以为枚举值添加构造器和方法
private Color() {
// 私有构造器,防止外部实例化枚举
}
// 示例:添加一个返回颜色描述的方法
public String getDescription() {
switch (this) {
case RED:
return "Red color";
case GREEN:
return "Green color";
case BLUE:
return "Blue color";
default:
throw new IllegalArgumentException("Unknown color");
}
}
}
public class EnumExample {
public static void main(String[] args) {
Color myColor = Color.RED;
System.out.println(myColor); // 输出: RED
System.out.println(myColor.getDescription()); // 输出: Red color
Color.printAllColors(); // 输出所有颜色
}
}
Go
package main
import "fmt"
type Color int
const (
Red Color = iota // Red == 0
Green // Green == 1
Blue // Blue == 2
)
//iota在每个const声明块中都是从头开始计数的,所以Red被赋值为0,Green被赋值为1,Blue被赋值为2
func main() {
var c Color = Green
fmt.Println("The color is:", c) // 输出:The color is: 1
switch c {
case Red:
fmt.Println("Red")
case Green:
fmt.Println("Green")
case Blue:
fmt.Println("Blue")
default:
fmt.Println("Unknown color")
}
}
4.函数
Java
public class MyClass {
// 这是一个无参数、无返回值的方法(也称为void方法)
public void sayHello() {
System.out.println("Hello, World!");
}
// 这是一个有参数、无返回值的方法
public void printNumber(int number) {
System.out.println("The number is: " + number);
}
// 这是一个有参数、有返回值的方法
public int addNumbers(int num1, int num2) {
int sum = num1 + num2;
return sum;
}
// 主方法,程序的入口点
public static void main(String[] args) {
MyClass myObject = new MyClass(); // 创建一个MyClass对象
// 调用无参数方法
myObject.sayHello();
// 调用有参数方法
myObject.printNumber(42);
// 调用有参数、有返回值的方法,并打印返回值
int result = myObject.addNumbers(5, 3);
System.out.println("The sum is: " + result);
}
}
go
package main
import "fmt"
// 这是一个带有两个整数参数并返回一个整数的函数
func add(x int, y int) int {
return x + y
}
// 这是一个没有参数也没有返回值的函数
func sayHello() {
fmt.Println("Hello, World!")
}
//多返回值函数
func get()(x,y int){
return 10,20
}
// 主函数,程序的入口点
func main() {
// 调用带有参数的函数,并将结果存储在变量中
sum := add(42, 13)
fmt.Println("The sum is:", sum)
// 调用没有参数的函数
sayHello()
x,y := get()
x1,_:= get() //使用_拒收返回值
}
go中函数也是一种数据类型,因此函数可以用变量接收,可做为参数,可作为返回值
package main
import "fmt"
// 定义一个简单的函数
func greet(name string) string {
return "Hello, " + name
}
// 定义一个接受函数作为参数的函数
func applyFunction(f func(string) string, name string) {
fmt.Println(f(name))
}
// 定义一个返回函数的函数
func getGreetingFunction(prefix string) func(string) string {
return func(name string) string {
return prefix + ", " + name
}
}
func main() {
// 函数赋值给变量
var greetFunc func(string) string
greetFunc = greet
fmt.Println(greetFunc("World")) // 输出: Hello, World
// 函数作为参数
applyFunction(greet, "Go") // 输出: Hello, Go
// 函数作为返回值
anotherGreetFunc := getGreetingFunction("Hi")
fmt.Println(anotherGreetFunc("Universe")) // 输出: Hi, Universe
}
5.方法
Java中类中定义的函数称为该类的方法
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String print() {
return "{name = " + name + ",age = " + age + "}";
}
}
public class Demo {
public static void main(String[] args) {
User user = new User("xiaohong", 29);
System.out.println("user信息:" + user.print());
}
}
//执行结果
user信息:{name = xiaohong,age = 29}
go中有接收者的函数称为方法
package entity
import "fmt"
type User struct {
Name string
Age int
}
// User结构体/指针可调用的"方法",属于User结构体
func (user *User) Solve() {
fmt.Println(user)
}
// 任何地方都可调用的"函数",不属于任何结构体,可通过entity.Solve调用
func Solve(user *User) {
fmt.Println(user)
}
go中的结构体就类似于Java中发方法
6.指针
在Java中不存在显式的指针操作;8种基本数据类型是值类型,数组和对象属于引用类型。
而 Golang 中存在显式的指针操作,但是 Golang 的指针不像C那么复杂,不能进行指针运算。
var ptr *int // 声明一个指向int类型的指针变量
var num int = 42
ptr := &num // 将num的地址赋值给ptr
fmt.Println(*ptr) // 输出: 42
*ptr = 100 // 修改ptr指向的值为100
fmt.Println(num) // 输出: 100,因为ptr指向num的内存地址
面向对象
1.继承
在 Java 中,继承是面向对象编程(OOP)的四个基本特性之一(抽象、封装、继承和多态)。通过继承,一个类(称为子类或派生类)可以获取另一个类(称为父类或基类)的属性和方法。子类可以覆盖父类的方法(即提供自己的实现),这被称为方法重写(Overriding)。
public class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Dog extends Animal {
@Override
void makeSound() {
System.out.println("The dog barks");
}
}
Go 语言并不直接支持传统意义上的类继承和面向对象模型。相反,它采用了一种不同的方法来支持代码复用和组织。Go 通过组合(Composition)和接口(Interface)来实现类似继承的功能。
-
组合:在 Go 中,你可以通过将一个类型的值嵌入到另一个类型中来实现组合。这样,内部类型的方法就可以直接在外部类型上使用,类似于继承的效果。
-
接口:Go 的接口是一种类型,它定义了一组方法的集合。任何包含这些方法实现的类型都隐式地实现了该接口。通过这种方式,Go 实现了类似多态的行为,而不需要显式的继承关系。
package main
import "fmt"
type Animal struct{}
func (a *Animal) makeSound() {
fmt.Println("The animal makes a sound")
}
type Dog struct {
Animal // 嵌入 Animal 类型,实现组合
}
// 重写(或称为覆盖)Animal 的 makeSound 方法
func (d *Dog) makeSound() {
fmt.Println("The dog barks")
}
func main() {
animal := &Animal{}
dog := &Dog{}
animal.makeSound() // 输出: The animal makes a sound
dog.makeSound() // 输出: The dog barks
}
// 注意:在上面的示例中,Dog 类型的 makeSound 方法实际上覆盖了 Animal 类型的同名方法,
// 但在 Go 中没有像 Java 那样的显式 @Override 注解。
2.多态
Java通过继承和接口来实现多态
在继承关系中,子类可以重写(Override)父类的方法。当通过父类引用指向子类对象时,如果调用被重写的方法,则会执行子类中的版本,这就是多态的体现。
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // Animal对象
myAnimal.makeSound(); // 输出: The animal makes a sound
Animal myDog = new Dog(); // Dog对象,但引用类型是Animal
myDog.makeSound(); // 输出: The dog barks,多态的体现
}
}
在Java中,接口定义了方法的契约,但不提供实现。任何类只要实现了接口,就必须提供接口中所有方法的具体实现。通过接口引用指向实现了该接口的类的对象,也可以实现多态。
interface Speaker {
void speak();
}
class Person implements Speaker {
@Override
public void speak() {
System.out.println("Hello, I'm a person.");
}
}
class Robot implements Speaker {
@Override
public void speak() {
System.out.println("Hello, I'm a robot.");
}
}
public class Main {
public static void main(String[] args) {
Speaker speaker1 = new Person(); // Person对象
speaker1.speak(); // 输出: Hello, I'm a person.
Speaker speaker2 = new Robot(); // Robot对象
speaker2.speak(); // 输出: Hello, I'm a robot.,多态的体现
}
}
虽然Go语言没有显式的继承机制,但它通过接口和类型嵌入(组合)的方式实现了类似多态的效果。在Go中,一个类型只要实现了接口中定义的所有方法,就被认为实现了该接口。通过接口引用,可以调用实现了该接口的任何类型的对象的方法。
package main
import "fmt"
type Speaker interface {
Speak()
}
type Person struct{}
func (p Person) Speak() {
fmt.Println("Hello, I'm a person.")
}
type Robot struct{}
func (r Robot) Speak() {
fmt.Println("Hello, I'm a robot.")
}
func main() {
var speaker1 Speaker = Person{} // Person对象
speaker1.Speak() // 输出: Hello, I'm a person.
var speaker2 Speaker = Robot{} // Robot对象
speaker2.Speak() // 输出: Hello, I'm a robot.,类似多态的效果
}
异常处理
在 Java 中: 通过 try..catch..finally 的方式进行异常处理,有可能出现异常的代码会被 try 块给包裹起来,在 catch 中捕获相关的异常并进行处理,最后通过finally块来统一执行最后的结束操作(释放资源)。
public class ExceptionTest {
public static void main(String[] args) {
FileInputStream fileInputStream = null;
try{
fileInputStream = new FileInputStream("test.txt");
}catch (IOException e){
System.out.println(e.getMessage());
e.printStackTrace();
return;
}finally {
if(fileInputStream!=null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("回收资源");
}
}
}
在Golang中:错误处理方式有两种方式:, ok 模式 与 defer、panic及 recover 的组合。
Golang 的, ok 模式。所有可能出现异常的方法或者代码直接把错误当作第二个响应值进行返回,程序中对返回值进行判断,非空则进行处理并且立即中断程序的执行。
优点:这种比 Java 的简单很多,是 Golang 在异常处理方式上的一大特色。
缺点:代码冗余,所有的异常都需要通过 if err != nil {}
去做判断和处理,不能做到统一捕捉和处理,容易遗漏
func main() {
value, err := Bark()
if err != nil {
// 返回了异常,进行处理
log.error("...异常:", err)
return err
}
// Bark方法执行正确,继续执行后续代码
Process(value)
}
defer 是 Golang 错误处理中常用的关键字, pannic 及 recover 是 Golang 中的内置函数,通常与 defer 结合进行错误处理,它们各自的用途为:
defer的作用是延迟执行某段代码,一般用于关闭资源或者执行必须执行的收尾操作,无论是否出现错误defer代码段都会执行,类似于 Java 中的 finally 代码块的作用;defer 也可以执行函数或者是匿名函数:
defer func() {
// 清理工作
} ()
// 这是传递参数给匿名函数时的写法
var num := 1
defer func(num int) {
// 做你复杂的清理工作
} (num)
panic 的作用是抛出错误,制造系统运行时恐慌。当在一个函数执行过程中调用 panic ()函数时,正常的函数执行流程将立即终止。但函数中之前使用 defer 关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行 panic 流程,直至所属的 goroutine 中所有正在执行的函数被终止, panic 和 Java 中的 throw 关键字类似:用于抛出错误,阻止程序执行。
recover 的作用是捕捉 panic 抛出的错误并进行处理,需要联合 defer 来使用,类似于 Java 中的 catch 代码块:
func main() {
fmt.Println("main begin")
// 必须要先声明defer,否则不能捕获到panic异常
defer func() {
fmt.Println("defer begin")
if err := recover(); err != nil {
// 这里的err其实就是panic传入的内容
fmt.Println(err)
}
fmt.Println("defer end")
}()
test()
// test中出现错误,这里开始下面代码不会再执行
fmt.Println("main end")
}
func test() {
fmt.Println("test begin")
panic("error")
//这里开始下面代码不会再执行
fmt.Println("test end")
}
//执行结果
main begin
test begin
defer begin
error
defer end
并发
Java语言上没有协成的概念,Java的线程模型依然用的内核级线程模型,多线程开发依然需要复杂的实现,而且实现方式有很多种而不用拖着一个像,你需要了解每种实现方式的优缺点才能写出高性能的代码,除了这些还需要了解各种锁,来保障你写的线程是安全的。
Go 作为一种为现代多核计算机设计的语言,简单优雅的并发,并且具有强大的并发模型,其设计基于两级线程模型改进的GMP模型,这样能大大减少并发切换线程的性能开销,而且这些模型统一封装到语言级别的调度层,只需通过关键字 go
就可以开启协成,提高了易用性。
在 Golang 中,则需要将代码包装成函数。使用 go
关键字调用函数之后,便创建了一个可以运行代码单元的 goroutine 。一旦 CPU 资源就绪,对应的代码单元便会在 goroutine 中执行:
go func() {
fmt.Println("test task running")
}()
GC
Java 基于 JVM 完成了垃圾收集的功能,其体系很庞大,包括了垃圾回收器( G1、CMS、Serial、ParNew 等)、垃圾回收算法(标记-清除、标记-整理、复制、分代收集)、可达性算法(可达性分析、引用计数法)、引用类型、JVM内存模型等内容。
经过多代发展, Java 的垃圾回收机制较为完善,Java划分新生代、老年代来存储对象。对象通常会在新生代分配内存,多次存活的对象会被移到老年代,由于新生代存活率低,产生空间碎片的可能性高,通常选用“标记-复制”作为回收算法,而老年代存活率高,通常选用“标记-清除”或“标记-整理”作为回收算法,压缩整理空间。
Go语言主要使用并发标记-清除(mark-and-sweep)算法,并在此基础上进行了优化。
- 标记阶段:从根对象开始,对内存对象进行遍历,对所有可达的对象进行标记。
- 清除阶段:对标记阶段未被标记的内存对象进行回收,回收完毕后重置所有的内存对象的标记以便下轮“标记-清除”。
三色标记
- 白色:潜在垃圾,表示还未搜索到的对象,其内存可能会被垃圾收集器回收。
- 灰色:活跃对象,表示正在搜索还未搜索完的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象。
- 黑色:活跃对象,表示搜索完成的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象。
GC优化
- STW(Stop The World):在早期的Go版本中,垃圾回收器在执行垃圾回收时需要暂停整个应用程序的运行,这被称为STW。但随着版本的更新,Go通过引入三色标记法和其他优化技术,减少了STW的时间,提高了程序的并发性能。
- 写屏障(Write Barrier):Go语言引入了插入写屏障和删除写屏障机制,用于解决三色标记法在某些情况下的问题。写屏障确保了在并发环境下,对象的引用关系能够正确地被垃圾回收器感知和处理。
- 辅助GC(Helper GC):Go语言的垃圾回收器还使用了辅助GC的技术,通过在程序运行过程中主动触发垃圾回收,以减少STW的时间并提高程序的响应性。
开发方向
Go 最适合开发以下类型的应用程序
- 分布式网络服务:Go 的内置并发特性,主要是 goroutine 和通道,非常适合开发一些网络服务。因此,许多 Go 项目用于网络、分布式功能和云服务:
Web 服务器
、API
、应用框架
、分布式存储服务
等。 - 云原生开发:它的并发和网络能力,以及高度的移动性,使其适合构建云应用程序。事实上,Go 已被用于构建云计算的多个流行项目,包括
Docker
、Kubernetes
和Istio
等。 - 区块链: 由于Go有着非常好的性能,并且简洁的语法,现在它是区块链开发的首选语言。
- 其他:一些基础设施和独立的工具等,比如SSH服务器,
Java开发的方向
- 后端服务:Java spring 有着完善的微服务体系,常用于后台服务的开发。
- Android应用:尽管Kotlin已上升为Google的亲儿子,但他和Java很相似,都是JVM语言,而且Android的SDK依然是JAVA。
- 大数据程序:Hadoop 是用 Java 编写的。Scala、Kafka 和 Spark 都使用 JVM。此外,Java 使您可以访问许多经过验证的库、调试器和监控工具。
- 金融项目:作为金融行业最受欢迎的语言技能之一,Java 被用于服务器端和客户端来构建可靠、快速和简单的网站。它也是建模和数据模拟的首选语言。