Go 语言学习总结(9)—— Go 与 Java 全面对比总结

基本语法格式

Golang: 编码风格相对统一,简单,没有太多的语法糖等,Java层次清晰,全面面向对象。

变量相关

  • 变量的声明及使用

在Java或者PHP、Python中,声明了变量,可以不使用,也不报错。

private static String CToString(int n) {
  int data = n; 
  return String.valueOf(n);
} 

在Golang中,变量申明以后,必须被使用,或者使用_标明,否则编译会报错,一般来讲,我们都不会申明不使用的变量了。

func CToStringPanic(n int) string {
	newData := n     // newData编译无法通过
	return strconv.Itoa(n)
}

func CToStringOk(n int) string {
	_ := n	      // ok
	return strconv.Itoa(n)
}
  • 变量声明及初始化

在Java中,如果内部申明变量,但是没有初始化,有时候会编译报错,需要注意,例如以下代码;

public void Cpainc() {
        int salary;
        Object object;
        System.out.println(salary); // 编译错误
        System.out.println(object); // 编译错误
}

在Golang中:对于基本类型来讲,声明即初始化,对于引用类型,声明则初始化为nil。

结构体、类、变量的作用范围的标准与规则

Java: 对方法、变量及类的可见域规则是通过private、protected、public关键字来控制的,类似PHP

Golang: 控制可见域的方式只有一个,当字段首字母开头是大写时说明其是对外可见的、小写时只对包内成员可见。

//公开
type Public struct{
	Name string    //公开 
  Age uint8      //公开
  salary uint    //私有
}


//私有
type private struct{
	name string    //私有 
  age uint8      //私有
  salary uint    //私有
}

类、结构体、方法、函数

结构体声明及使用

  • Java:Java是一种面向对象的编程语言,它使用类来组织数据和行为。类是对象的蓝图,它定义了对象的属性和方法。Java中的继承和接口实现允许创建层次结构,支持多态性。
  • Go:Go没有类的概念,而是使用结构体(structs)来组织数据。结构体是一种用户自定义的数据类型,它可以包含字段(fields)和方法(methods)。Go不支持继承,而是使用组合来复用代码。方法可以在结构体上定义,但与结构体本身没有紧密关联。

先看Golang的:

// 定义people结构体
type People struct {
	 Title string
   Age uint8
}

// 使用
func main() {
	P := new(People) // 通过new方法创建结构体指针
	person1 := People{}        
	person2 := People{
		Title: "xiaoHong",
		Age:  18,
	}
  ...........
}

再Java的:

方法和函数是有区别的

在Java中:所有的“函数”都是基于“类”这个概念构建的,也就是只有在“类”中才会包含所谓的“函数”,这里的“函数”被称为“方法”,可见上方声明实体并使用。

在Golang中:“函数”和“方法”的最基本区别是:函数不基于结构体而是基于包名调用,方法基于结构体调用。如下实例:

值类型、引用类型以及指针

Java中的指针和数据类型:

  1. 指针操作: 在Java中,不存在显式的指针操作。Java采用引用类型,但不允许直接访问内存地址。对象在Java中通过引用进行传递,而不是通过值。
  2. 基本数据类型和引用类型: Java有8种基本数据类型,它们是值类型,包括int、float、boolean等。除了基本数据类型外,Java的数组和对象属于引用类型。引用类型的变量存储的是对象的引用,而不是对象本身的值。

Go中的指针和数据类型:

  1. 指针操作: Go中存在显式的指针操作,但与C相比,Go的指针更加安全和简化。Go不支持指针运算,这有助于减少指针错误的发生。
  2. 基本数据类型和引用类型: Go中的所有基本类型都是值类型,包括int、float、bool等。但有几个类型(slice、map、channel、interface)表现出引用类型的特征,它们可以被看作引用类型。这些类型在传递时不会复制整个值,而是传递引用,因此可以直接使用变量本身而无需使用指针。
  3. slice和数组的区别: 在Go中,数组是值类型,其长度是固定的。而slice是一种动态数组,它没有固定长度,因此在使用上更加灵活。
  4. 类型的一致性: 在Go中,同类型的变量是可比较的,但只有同样长度和类型的数组才被视为同一类型。例如,[]int和[3]int被认为是不同的类型,这在参数传递和类型断言时需要注意。

Java和Go在指针和数据类型的处理方式上存在明显差异。Java采用引用类型,不支持指针操作,而Go在保持安全性的同时,允许显式的指针操作,并将基本数据类型和特定引用类型区分对待。这些差异在编程时需要注意,以确保正确处理数据和内存管理。

数组对比

在Java中:当向方法中传递数组时,可以直接通过该传入的数组修改原数组内部值(浅拷贝)。在Golang中:则有两种情况:在不限定数组长度(为slice)时也直接改变原数组的值,当限定数组长度时会完全复制出一份副本来进行修改(深拷贝):

Java:

public static void main(String[] args) {
        int[] arr = {11, 21, 31};
        c(arr);
        System.out.println(Arrays.toString(arr)); // 2,21,31
    }

    private static void c(int[] a r r) {
        arr[0] = 2;
}

Golang:

对象也不太一样哦

在Go中,当将对象传递给函数作为参数时,实际上会创建原对象的一个全新拷贝,这个拷贝具有自己独立的内存地址。而在Go中,对象之间的赋值操作会复制对象内存中的内容,这就是为什么即使在修改globalUser之前后,其地址保持不变,但对象的内容却发生了变化。

相比之下,在Java中,当将对象传递给函数时,传递的是原对象的引用的拷贝,这个拷贝仍然指向同一块内存地址。因此,Java对象之间的赋值操作实际上是复制对象的引用,而不是对象的内容。当引用指向不同的地址时,对象的内容也会发生变化。

Golang:

//User 定义User结构体
type User struct {
   Name string
   Age int
}


var globalUser = User {
   "zz",
   33,
}

func modifyUser(user User) {
   fmt.Printf("user的addr = %p\n",&user) 
   fmt.Printf("globalUser修改前的addr = %p\n",&globalUser)
   fmt.Println("globalUser修改前 = ",globalUser)
   // 修改指向
   globalUser = user
   fmt.Printf("globalUser修改后的addr = %p\n",&globalUser)
   fmt.Println("globalUser修改后 = ",globalUser)
}

func main() {
   var u User = User {
      "kx",
      32,
   }
   fmt.Printf("传递的参数u的addr = %p\n",&u)
   modifyUser(u)
}

指针的差异常

在Java中,当传递引用类型(例如对象或数组)作为函数参数时,实际上传递的是引用的副本,也可以说是引用的拷贝。这意味着传递的引用仍然指向相同的对象或数组,因此对对象或数组的修改将在原对象或数组上反映出来。

在Go中,确实需要显式传递指针才能操作原对象。如果不传递指针,将只传递对象的副本,对副本的修改不会影响原对象。这是Go语言的设计决策,旨在提供更明确的内存控制和避免意外的副作用。因此,在Go中,需要特别注意传递对象的指针,以确保对原对象的修改能够生效。

Golang的指针:

Java的指针:

面向对象编程,不太一样,这里的不同,很多人都不习惯

Java的面向对象与Golang的结构体组合模式

在Java中,一般使用抽象类和继承

// 定义抽象类 Animal
abstract class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // 抽象方法 bark,子类需要实现
    public abstract void bark();
}

// 定义 Dog 类,继承自 Animal
class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }

    // 实现抽象方法 bark
    @Override
    public void bark() {
        System.out.println("狗在汪汪叫!");
    }
}

在Go中,可以使用结构体和方法接收者来实现这个场景,以下是一个示例:

package main

import "fmt"

// 定义 Animal 结构体
type Animal struct {
    Name string
    Age  int
}

// 定义 Animal 结构体的方法
func (a Animal) GetName() string {
    return a.Name
}

func (a Animal) GetAge() int {
    return a.Age
}

// 定义 Dog 结构体,嵌套 Animal
type Dog struct {
    Animal // 嵌套 Animal 结构体,获得其属性和方法
}

// 定义 Dog 结构体的方法
func (d Dog) Bark() {
    fmt.Println("狗在汪汪叫!")
}

func main() {
    // 创建 Dog 实例
    myDog := Dog{
        Animal: Animal{
            Name: "旺财",
            Age:  3,
        },
    }

    // 使用嵌套的 Animal 方法
    fmt.Printf("狗的名字:%s\n", myDog.GetName())
    fmt.Printf("狗的年龄:%d岁\n", myDog.GetAge())

    // 调用 Dog 自身的方法
    myDog.Bark()
}

隐式与显式实现接口的实现

在Java中,接口主要用于定义不同组件之间的契约或规范。Java中的接口是一种侵入性接口,这意味着实现接口的类必须显式声明它们实现了特定接口。以下是一个示例,演示如何在Java中管理狗的行为:

// 定义一个 Animal 接口,描述动物的基本行为
interface Animal {
    void eat();
    void sleep();
}

// 定义一个 Dog 类,实现 Animal 接口
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("狗正在吃东西");
    }

    @Override
    public void sleep() {
        System.out.println("狗正在睡觉");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建 Dog 实例
        Dog myDog = new Dog();

        // 调用实现的接口方法
        myDog.eat();
        myDog.sleep();
    }
}

在Go中,可以定义一个接口(Factory)并为其定义一些方法,然后创建一个结构体(CafeFactory)并实现这些方法,以满足接口的要求。以下是一个示例代码,演示如何实现这个场景:

package main

import "fmt"

// 定义 Factory 接口
type Factory interface {
    Produce() string
    Consume() string
}

// 定义 CafeFactory 结构体,实现 Factory 接口
type CafeFactory struct {
    Product string
}

// 实现 Produce 方法
func (cf CafeFactory) Produce() string {
    return "生产了 " + cf.Product
}

// 实现 Consume 方法
func (cf CafeFactory) Consume() string {
    return "消费了 " + cf.Product
}

func main() {
    // 创建 CafeFactory 实例
    coffeeFactory := CafeFactory{Product: "咖啡"}

    // 使用 Factory 接口来调用方法
    factory := Factory(coffeeFactory)

    // 调用 Produce 和 Consume 方法
    fmt.Println(factory.Produce())
    fmt.Println(factory.Consume())
}

Golang的非侵入式接口的优势在于其简洁、高效、以及按需实现的特性。在Go语言中,不存在类的继承概念,而是只需知道一个类型实现了哪些方法以及每个方法的行为是什么。在实现类型时,我们只需考虑自己应该提供哪些方法,而不必担心接口需要分解得多细才算合理。接口是由使用方根据需要进行定义的,而不是事先规划好的。这种方式减少了包的引入,因为多引入外部包意味着更多的耦合。接口是由使用方根据其自身需求来定义的,使用方无需担心是否已经有其他模块定义了类似的接口。相比之下,Java的侵入式接口优势在于其清晰的层次结构以及对类型的行为有严格的管理。

异常处理,相差很大

在Java中,异常处理是通过try-catch块来实现的

public class ExceptionHandlingDemo {
    public static void main(String[] args) {
        try {
            // 可能会引发异常的代码
            int result = divide(10, 0);
            System.out.println("结果是: " + result);
        } catch (ArithmeticException e) {
            // 捕获并处理异常
            System.out.println("发生了算术异常: " + e.getMessage());
        } finally {
            // 不管是否发生异常,都会执行的代码块
            System.out.println("这里是finally块,无论如何都会执行");
        }
    }

    public static int divide(int dividend, int divisor) {
        // 尝试执行除法操作
        return dividend / divisor;
    }
}

Golang的异常处理:

在Golang中,"ok模式"或者叫做"错误值模式"(error value pattern)是一种常见的异常处理方式。

在这种模式中,所有可能引发异常的方法或代码将错误作为第二个返回值返回,程序通常需要对返回值进行判断,如果错误不为空(通常用 if err != nil {} 判断),则进行相应的错误处理,并可能中断程序的执行。这种方式相对于传统的异常处理机制如Java的异常捕获和处理机制来说,更为简洁和直观。

package main

import (
	"fmt"
	"io/ioutil"
)

// 一个用于读取文件内容的函数,可能会返回错误
func readFileContent(filename string) (string, error) {
	// 尝试打开文件
	content, err := ioutil.ReadFile(filename)
	if err != nil {
		// 如果发生错误,返回错误信息
		return "", err
	}
	// 如果没有错误,返回文件内容
	return string(content), nil
}

func main() {
	// 调用 readFileContent 函数,并检查是否有错误发生
	content, err := readFileContent("example.txt")
	if err != nil {
		// 处理错误,例如打印错误信息
		fmt.Println("发生错误:", err)
		return // 可以选择中断程序的执行
	}
	// 如果没有错误,打印文件内容
	fmt.Println("文件内容:")
	fmt.Println(content)
}

Golang的defer、panic及recover

defer:defer是用于延迟执行一段代码的关键字。无论函数是否正常返回或者是否发生了恐慌(panic),defer代码都会在函数退出时执行。这通常用于执行一些必须在函数结束时进行的清理工作,例如关闭文件、释放资源等。defer也可以用来调用函数或匿名函数。它确保在函数结束前执行。

//defer
func someFunction() {
    defer fmt.Println("This will be executed last")
    fmt.Println("This will be executed first")
}

//painc
func someFunction() {
    panic("Something went terribly wrong!")
}

//recover
func someFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    panic("Something went terribly wrong!")
}

需要注意的是,defer使用一个栈来维护需要执行的代码,所以defer函数所执行的顺序是和defer声明的顺序相反的。

defer fmt.Println(1)   
defer fmt.Println(2)
defer fmt.Println(3)

执行结果:
3
2
1
  1. panic的作用:panic用于引发运行时恐慌(panic),并停止当前函数的执行流程。但是,在停止执行之前,它会执行当前函数中的defer语句,然后停止该函数的执行。然后,它会继续执行调用该函数的函数的defer语句,以此类推,直到goroutine的调用堆栈被清理。最终,整个程序可能会中止。panic通常用于处理严重错误,例如数组越界或空指针引用等,以避免程序进一步运行可能导致更严重问题的代码。
  2. recover的作用:recover用于捕获panic引发的运行时恐慌,从而允许程序继续执行而不中止。通常,recover与defer一起使用。当在defer内部调用recover时,它会捕获panic的错误信息,并返回该错误。如果没有panic,recover返回nil。你可以使用recover来在发生恐慌时采取适当的措施,例如记录错误、进行恢复或执行清理操作。需要注意的是,recover只能在defer内部使用,并且仅在发生panic时才有效。

recover的作用是捕捉panic抛出的错误并进行处理,需要联合defer来使用,类似于Java中的catch代码块:

func someFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
            // 在这里可以采取适当的措施
        }
    }()
    panic("Something went terribly wrong!")
}

注:利用recover处理panic指令,defer必须在panic之前声明,否则当panic时,recover无法捕获到panic。

并发编程,也不太一样

并发方面,Java 和 Golang 的基本实现

在 Java 中,要获得 CPU 资源并异步执行代码单元,通常需要创建一个实现了 Runnable 接口的类,并将该类的实例传递给一个 Thread 对象来执行。以下是一些示例代码,演示了如何在 Java 中执行异步代码单元:

// 创建一个实现了 Runnable 接口的类
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 在这里编写你的异步代码单元
        for (int i = 0; i < 5; i++) {
            System.out.println("线程执行:" + i);
            try {
                Thread.sleep(1000); // 模拟任务执行时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建一个 Thread 对象,并传递 MyRunnable 的实例
        Thread thread = new Thread(new MyRunnable());
        
        // 启动线程
        thread.start();
        
        // 主线程可以继续执行其他任务
        for (int i = 0; i < 3; i++) {
            System.out.println("主线程执行:" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在 Golang 中,执行异步代码单元通常不需要创建线程,而是通过创建 goroutine 来实现。Goroutine 是 Go 语言的轻量级线程,可以非常高效地执行并发任务。要执行异步代码单元,只需将代码包装成一个函数,并使用 go 关键字调用该函数,这将创建一个新的 goroutine 来执行该函数中的代码。以下是一个简单的示例:

package main

import (
	"fmt"
	"time"
)

// 异步执行的函数
func asyncFunction() {
	for i := 0; i < 5; i++ {
		fmt.Println("异步执行:", i)
		time.Sleep(time.Second) // 模拟任务执行时间
	}
}

func main() {
	// 使用 go 关键字创建一个新的 goroutine 来执行 asyncFunction
	go asyncFunction()

	// 主线程可以继续执行其他任务
	for i := 0; i < 3; i++ {
		fmt.Println("主线程执行:", i)
		time.Sleep(time.Second)
	}

	// 等待一段时间以确保异步任务有足够的时间执行
	time.Sleep(5 * time.Second)
}

并发方面,Java 和 Golang 的区别

Java 和 Golang 在并发编程方面有一些显著的区别,这些区别主要涉及到并发模型、线程管理、内存模型等方面:

并发模型:

  • Java 使用基于线程的并发模型。在 Java 中,线程是基本的并发单元,开发人员需要显式创建和管理线程。Java提供了java.lang.Thread类以及java.util.concurrent包中的各种工具来处理多线程编程。
  • Golang 使用 goroutines 和通道(channels)的并发模型。Goroutines 是轻量级的用户级线程,Go语言的运行时系统负责调度它们。通道是用于在不同的 goroutines 之间传递数据的机制。这种模型更简单、更容易使用和更安全。

线程管理:

  • Java 的线程管理相对复杂。开发人员需要显式创建线程对象、启动线程、停止线程,以及处理线程的同步和协调。Java提供了synchronized关键字和各种锁来处理多线程同步。
  • Golang 的 goroutine 管理由运行时系统自动处理,开发人员只需要使用 go 关键字启动一个函数作为 goroutine,不需要手动创建和管理线程。Golang 提供了通道来进行 goroutine 之间的通信和同步,这大大简化了并发编程。

内存模型:

  • Java 使用 Java Memory Model(JMM)来管理多线程程序中的内存访问。JMM 规定了共享变量的可见性和操作的顺序。
  • Golang 使用了一种更简单和更可预测的内存模型。Golang 的内存模型是顺序一致性(Sequential Consistency)的,不需要开发人员担心复杂的内存可见性问题,因为 goroutines 之间的通信是通过通道进行的,不需要显式的锁来保护共享数据。

并发工具:

  • Java 提供了大量的并发工具和类库,如 java.util.concurrent 包,用于处理各种并发问题,包括线程池、锁、队列、原子操作等。
  • Golang 也提供了一些内置的并发工具,如 sync 包中的锁、select 语句用于选择通道操作、goroutine 和通道本身。这些工具相对较少,但足以处理大多数并发场景。

总的来说,Golang的并发编程模型更简单、更安全,适用于构建高并发的分布式系统和网络服务。Java则更适合传统的企业级应用和复杂的多线程场景,但需要更多的线程管理和同步工作。开发人员在选择编程语言时,应根据项目的需求和复杂性来考虑哪种并发模型更适合。

Java 和 Go 官方库中同步方式的对应关系

Java synchronized 与Golang Mutex

Java 中的 synchronized 关键字和 Golang 中的 Mutex(互斥锁)都是用于实现多线程同步的工具,但它们有一些不同之处。以下是它们的主要区别以及示例代码来说明这些区别。Java 的 synchronized 关键字:

  1. synchronized 关键字是 Java 的内置同步机制,可用于方法或代码块级别的同步。
  2. synchronized 关键字可用于实现方法级别的同步,也可以用于同步代码块。
  3. synchronized 关键字在进入同步块之前要获取锁,在退出同步块后释放锁,这是隐式的。
  4. 可以使用 synchronized 来实现对对象的同步,例如在方法声明上使用 synchronized,或者在代码块中使用 synchronized (object)
class SynchronizedExample {
    private int count = 0;
    
    // 同步方法,锁是该对象实例
    public synchronized void increment() {
        count++;
    }
    
    // 同步代码块,锁是指定的对象
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}

Golang 的 Mutex(互斥锁):

  1. Golang 使用 sync 包中的 Mutex 类型来实现同步。
  2. Mutex 可以用于实现代码块级别的同步,但通常不用于方法级别的同步(因为 Golang 的并发模型更倾向于使用 goroutines 和通道来处理并发)。
  3. 在 Golang 中,需要显式地锁定和解锁 Mutex,以确保在临界区内只有一个 goroutine 可以访问共享资源。
  4. Mutex 的锁定和解锁是显式的,需要在代码中调用 Lock 和 Unlock 方法。

以下是 Golang 中 Mutex 的示例:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	count := 0

	// 同步代码块,锁定 mu
	mu.Lock()
	count++
	// 解锁 mu
	mu.Unlock()

	fmt.Println("Count:", count)
}

修饰结构体:带锁结构体初始化后,直接调用对应的线程安全函数就可以。

条件变量,基本不一样

相似点:

  1. 条件变量和锁的关系:在 Java 和 Golang 中,条件变量通常与锁(互斥锁或同步块)一起使用,以确保多个线程或 goroutines 安全地访问共享资源。条件变量提供了等待和通知机制,用于线程或 goroutines 之间的协调。
  2. 多条件等待:在 Java 中,你可以使用多个条件变量(通过 Lock 和 Condition 实现)来等待不同的条件,从而更精确地控制线程的等待和通知。在 Golang 中,sync.Cond 结构体可以用于等待和通知多个 goroutines。

区别点:

  1. 条件变量的创建:
  • Java 中,条件变量通常通过 Lock 接口的 newCondition 方法创建。
  • Golang 中,条件变量由 sync.Cond 结构体表示,但需要与 sync.Mutex 结合使用。条件变量是通过 sync.NewCond(&mutex) 来创建的,其中 mutex 是一个互斥锁。
  1. 挂起和唤醒的时机:
  • Java 中,条件变量的等待和通知通常是在锁的保护下执行的。Object 的 wait() 方法和 notify()、notifyAll() 方法都在获取锁后才能调用。
  • Golang 中,sync.Cond 的 Wait() 方法是在获取锁后调用的,但是通知方法 Broadcast() 和 Signal() 是在解锁之后执行的,这意味着通知发出后,等待的 goroutines 需要重新竞争锁。

总的来说,条件变量在 Java 和 Golang 中的概念是相似的,都用于线程或 goroutines 之间的同步和通信。然而,它们在具体的使用方式和时机上存在一些区别,尤其是在 Golang 中,条件变量的通知不需要在锁的保护下执行,这可以避免一些潜在的死锁问题,但也需要开发人员更小心地处理竞争条件。

CAS/Atomic

在Java中:

  • CAS 操作由 volatile 关键字和 java.util.concurrent.atomic 包中的类支持,例如 AtomicInteger、AtomicLong 等。这些类提供了原子操作,可用于对整数和长整数进行原子操作。
  • java.util.concurrent 包还提供了一些无锁的数据结构,例如 ConcurrentHashMap、ConcurrentLinkedQueue 等,它们使用 CAS 操作来实现线程安全性。
  • Java 9 引入了 VarHandle,它提供了更通用的原子操作,不仅限于整数和长整数。

在Golang中:

  • Golang 的 sync/atomic 包提供了一组原子操作函数,可以用于对整数类型进行原子操作,例如 atomic.AddInt32、atomic.CompareAndSwapInt64 等。这些函数可以用于保证多goroutine环境下的原子性操作。
  • atomic.Value 类型提供了一种原子操作机制,允许你存储任意类型的值,并对其进行原子性的加载和存储操作。
  • Golang 的内置映射(map)类型是非线程安全的,但你可以使用 sync.Map 类型来实现线程安全的映射,它使用 CAS 操作来保证线程安全性。

总之,Java和Golang都支持CAS操作和原子操作,但在具体的实现和用法上有一些区别。在多线程或多goroutine环境下,这些操作可以确保共享变量的原子性,避免竞态条件和数据竞争问题。在选择哪种语言和库进行多线程编程时,可以根据具体的需求和语言特性来决定。

单例模式

Java懒汉式单例(Lazy Initialization):

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
        // 私有构造函数,防止外部直接实例化
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

饿汉式单例(Eager Initialization):

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
        // 私有构造函数,防止外部直接实例化
    }

    public static EagerSingleton getInstance() {
        return instance;
    }
}

Golang的Once模式

package singleton

import (
    "sync"
)

type Singleton struct {
    // 在这里定义单例的属性
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

垃圾回收机制

  1. GC策略的多样性:JVM支持多种GC策略,如分代GC、CMS(Concurrent Mark-Sweep)GC、G1(Garbage-First)GC等。这允许开发人员根据应用程序的需求选择最适合的策略。
  2. Go的单一GC方案:Go语言采用了一种单一的GC方案,它不分代,也不支持对象移动。这简化了GC的实现和配置,但也限制了对不同工作负载的优化。
  3. GC性能差异:由于Go的GC更频繁地触发(通常是通过并发标记清除算法实现的),因此它在某些情况下可能表现出更低的性能。相比之下,JVM的GC策略和性能更加灵活,可以根据需要进行调优。
  4. 暂停时间优化:Go非常注重减小垃圾回收的暂停时间,这对于需要低延迟的应用程序非常重要。Go的GC设计旨在最小化停顿时间,这在一些情况下可能比JVM更适用。

总之,每种编程语言和运行时环境都有其自己的GC实现和优化目标。Go的GC设计注重简单性和低延迟,而JVM的GC提供了更多的灵活性和优化选项,以适应不同类型的应用程序和工作负载。开发人员在选择编程语言和运行时环境时,需要根据项目的需求来考虑GC的性能和特性。

Java的垃圾回收

  1. 垃圾回收器(Garbage Collectors):Java的垃圾回收器有多种实现,包括G1(Garbage First)、CMS(Concurrent Mark-Sweep)、Serial、Parallel(Parallel Scavenge和Parallel Old)等。每个垃圾回收器都有其自己的特点和适用场景。
  2. 垃圾回收算法:Java使用不同的垃圾回收算法来管理内存,主要有标记-清除(Mark and Sweep)、标记-整理(Mark and Compact)、复制(Copying)和分代收集(Generational Collection)等。不同的算法用于处理不同内存区域的垃圾回收。
  3. 可达性算法:Java使用可达性分析(Reachability Analysis)来确定哪些对象是可达的,哪些对象是不可达的。这是判断对象是否可以被回收的重要依据。
  4. 引用类型:Java提供了不同类型的引用,包括强引用、软引用、弱引用和虚引用,用于更精细地控制对象的生命周期。
  5. JVM内存模型:Java虚拟机(JVM)内存模型定义了不同内存区域的用途和管理策略,例如新生代、老年代、持久代(在Java 7之前)、元空间(在Java 8及更高版本中)等。不同区域用于存储不同类型的对象,并且在垃圾回收时采用不同的算法。
  6. 内存压缩整理:你提到了压缩整理空间,这是垃圾回收过程中的一项重要任务。通过整理内存空间,可以减少内存碎片并提高内存利用率。

总之,Java的垃圾回收机制确实是一个庞大而复杂的体系,它的目标是自动管理内存,减轻开发人员的负担,并提供不同的算法和选项以适应不同类型的应用程序和工作负载。这使得Java成为一个非常强大和灵活的编程语言,适用于各种不同的应用场景。Golang GC特征

以下是一个简单的Java示例,演示了垃圾回收的行为:

public class GarbageCollectionDemo {
    public static void main(String[] args) {
        // 创建一个对象
        MyClass obj1 = new MyClass("Object 1");
        
        // 创建另一个对象
        MyClass obj2 = new MyClass("Object 2");
        
        // 让 obj1 不再被引用
        obj1 = null;
        
        // 强制垃圾回收
        System.gc();
        
        // 在这里等待一段时间,以便观察垃圾回收的效果
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    private String name;

    public MyClass(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(name + " is being finalized");
    }
}

Golang的垃圾回收

Go语言(Golang)的垃圾回收是一种自动管理内存的机制,用于检测和回收不再被引用的对象,从而释放内存资源。Go的垃圾回收器(GC)是Go运行时的一部分,负责管理内存分配和回收。以下是关于Go垃圾回收的一些要点:

  1. GC触发:Go的垃圾回收器是并发的,它会在运行时自动触发。垃圾回收的触发通常是基于内存分配的情况,当分配的内存达到一定阈值时,GC会启动。
  2. 标记-清除算法:Go的垃圾回收器使用标记-清除算法。在标记阶段,它会标记所有仍然被引用的对象,然后在清除阶段,回收所有未标记的对象。
  3. 并发标记:Go的垃圾回收器采用并发标记的方式,这意味着垃圾回收可以与应用程序的执行并行进行。这有助于减小GC对应用程序性能的影响。
  4. 分代垃圾回收:与某些其他语言不同,Go没有严格的分代垃圾回收。Go的GC采用了一种基于内存分配速度的启发式方法,通常将新分配的对象放在新生代,而老年代的对象生命周期更长。
  5. 内存复用:Go的垃圾回收器非常注重内存的高效使用。它会尽量复用被回收的内存,而不是立即释放给操作系统。
  6. Stop-The-World(STW)时间的优化:Go的垃圾回收器努力减小STW的时间。这对于需要低延迟的应用程序非常重要。
  7. 手动GC控制:尽管Go的垃圾回收器通常是自动的,但你也可以使用runtime包中的函数来手动控制GC的行为,如runtime.GC()。

总之,Go语言的垃圾回收是一种自动且并发的机制,它负责管理内存,确保不再被引用的对象被及时回收,从而减少内存泄漏和提高应用程序的性能。Go的GC设计考虑了并发性、低延迟以及内存复用等因素,使其成为编写高性能和高并发应用程序的强大工具。

以下是一个简单的Go示例,演示了垃圾回收的行为:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    var obj *MyObject

    for i := 0; i < 10000; i++ {
        // 创建一个新的对象,并赋值给 obj
        obj = NewObject(i)

        // 手动调用垃圾回收
        runtime.GC()

        // 在这里等待一段时间,以便观察垃圾回收的效果
        time.Sleep(time.Millisecond * 100)
    }

    // 防止 obj 被编译器优化掉
    _ = obj
}

type MyObject struct {
    id int
}

func NewObject(id int) *MyObject {
    return &MyObject{id}
}

func (o *MyObject) String() string {
    return fmt.Sprintf("Object %d", o.id)
}

性能与资源使用情况

Java的JIT策略比Golang的AOT策略

Java的JIT(Just-In-Time)编译策略和Go的AOT(Ahead-Of-Time)编译策略是两种不同的编译和执行策略,它们在编译和性能方面有一些重要的区别。以下是它们的对比:

1. 编译时机:

  • JIT编译:在Java中,代码通常首先被编译成字节码(Bytecode),然后由Java虚拟机(JVM)在运行时动态编译成本地机器代码。这意味着Java应用程序的编译发生在运行时,因此称为JIT编译。
  • AOT编译:Go使用AOT编译策略,Go代码在构建时直接编译成本地机器代码,生成可执行文件。因此,Go应用程序的编译发生在构建时,不需要在运行时进行动态编译。

2. 性能优化:

  • JIT编译:JIT编译允许在运行时进行更多的性能优化,因为编译器可以根据实际运行时数据和环境进行决策。这可以导致更好的性能,尤其是对于长时间运行的应用程序。
  • AOT编译:AOT编译可以在构建时执行更多的静态分析和优化,但它不具备JIT编译的动态性能优化。因此,AOT编译通常适用于需要快速启动和更稳定性能的应用程序。

3. 启动时间:

  • JIT编译:JIT编译的应用程序在启动时可能需要更多时间来进行动态编译。这会导致较长的启动时间。
  • AOT编译:AOT编译的应用程序通常具有更快的启动时间,因为它们在构建时已经被编译成本地机器代码。

4. 开发体验:

  • JIT编译:Java的JIT编译允许更灵活的开发和调试体验,因为代码可以更快地重新编译和运行。
  • AOT编译:Go的AOT编译可能需要更多的构建时间,但在部署和执行时通常具有更好的性能和可预测性。

综上所述,JIT编译和AOT编译都有其适用的场景。JIT编译通常适用于需要动态性能优化的长时间运行的应用程序,而AOT编译适用于需要快速启动和更稳定性能的应用程序。选择哪种策略取决于应用程序的需求和性能目标。

生态环境

Java在生态系统和框架方面的强大是不可否认的。Spring生态系统的广泛应用确实使Java成为了许多企业级应用程序的首选开发语言之一。Spring框架提供了丰富的功能和模块,包括Spring Boot、Spring MVC、Spring Data、Spring Security等,使Java开发更加高效和便捷。相比之下,Go语言虽然在近年来的发展中取得了很大的成功,但在生态系统和框架方面相对较新。Go的成功主要体现在其简洁性、高性能和并发性等方面,这使得它在云原生开发、分布式系统和网络编程等领域表现出色。虽然Go的生态系统不如Java成熟,但Go也有一些知名的框架和库,例如:

  • Gin:一个轻量级的Web框架,适用于构建高性能的Web应用程序。
  • Echo:另一个流行的Web框架,也专注于性能和简洁性。
  • Django:用于构建Web应用程序的全栈框架。
  • Beego:一个全栈的Web框架,提供了一系列的工具和库。
  • gRPC:用于构建分布式系统的高性能RPC(远程过程调用)框架。

尽管Go的生态系统相对较新,但它正在不断发展,并且在一些领域表现出色。在选择编程语言和框架时,通常会根据项目的需求和目标来做出决策。有些项目可能更适合使用Java和Spring,而其他项目可能会更适合使用Go。两者都有自己的优势和特点,取决于你的具体用例。

简单总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一杯甜酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值