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: メソッド、変数、およびクラスの表示可能なドメイン ルールは、PHP と同様に、private、protected、および public キーワードによって制御されます。

Golang: 表示フィールドを制御する方法は 1 つだけです。フィールドの最初の文字が大文字で始まる場合は、外部から表示されることを意味します。小文字の場合は、パッケージ内のメンバーのみに表示されます。

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


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

クラス、構造体、メソッド、関数

構造体の宣言と使用法

  • Java: Java は、クラスを使用してデータと動作を編成するオブジェクト指向プログラミング言語です。クラスはオブジェクトの設計図であり、そのプロパティとメソッドを定義します。Java での継承とインターフェイスの実装により、階層の作成が可能になり、ポリモーフィズムがサポートされます。
  • Go: Go にはクラスの概念がありませんが、構造体 (struct) を使用してデータを整理します。構造体は、フィールドとメソッドを含めることができるユーザー定義のデータ型です。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 には、int、float、boolean などの値型である 8 つの基本データ型があります。基本的なデータ型に加えて、Java 配列とオブジェクトも参照型です。参照型変数には、オブジェクト自体の値ではなく、オブジェクトへの参照が格納されます。

Go のポインターとデータ型:

  1. ポインタ操作: Go には明示的なポインタ操作が存在しますが、C と比較すると、Go のポインタはより安全で単純です。Go はポインター演算をサポートしていないため、ポインター エラーの発生を減らすことができます。
  2. 基本データ型と参照型: Go のすべての基本型は、int、float、bool などを含む値型です。ただし、参照型の特性を示し、参照型とみなせるタイプ (スライス、マップ、チャネル、インターフェイス) がいくつかあります。これらの型は、渡されたときに値全体をコピーするのではなく、参照を渡すため、ポインターを使用せずに変数自体を直接使用できます。
  3. スライスと配列の違い: Go では、配列は値型であり、その長さは固定です。スライスは固定長のない動的配列であるため、より柔軟に使用できます。
  4. 型の一貫性: Go では、同じ型の変数は比較できますが、同じ長さと型の配列のみが同じ型とみなされます。たとえば、[]int と [3]int は異なる型とみなされ、パラメータの受け渡しや型のアサーションの際に注意が必要です。

Java と Go には、ポインタとデータ型の処理方法に明らかな違いがあります。Java は参照型を使用し、ポインター操作をサポートしませんが、Go は安全性を維持しながら明示的なポインター操作を許可し、基本データ型と特定の参照型を異なる方法で扱います。これらの違いは、データとメモリ管理を適切に処理するためにプログラミング時に注意する必要があります。

配列の比較

Java の場合: 配列がメソッドに渡されると、元の配列の内部値は、渡された配列 (浅いコピー) を通じて直接変更できます。Golang には 2 つの状況があります: 配列の長さが制限されていない場合 (スライス)、元の配列の値が直接変更されます; 配列の長さが制限されている場合、変更のために完全なコピーが作成されます (ディープ コピー)。

ジャワ:

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;
}

ゴラン:

対象物も違います。

Go では、オブジェクトを関数にパラメータとして渡すと、実際には、独自の独立したメモリ アドレスを持つ、元のオブジェクトの新しいコピーが作成されます。Go では、オブジェクト間の代入操作によりオブジェクトのメモリの内容がコピーされます。そのため、globalUser が変更される前後でもアドレスは変更されませんが、オブジェクトの内容は変更されます。

対照的に、Java では、オブジェクトが関数に渡されるとき、元のオブジェクトへの参照のコピーが渡され、このコピーは依然として同じメモリ アドレスを指します。したがって、Java オブジェクト間の代入操作では、実際にはオブジェクトの内容ではなく、オブジェクトの参照がコピーされます。参照が別のアドレスを指している場合、オブジェクトの内容も変更されます。

ゴラン:

//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 パターン」または「エラー値パターン」が一般的な例外処理方法です。

このモードでは、例外をスローする可能性のあるすべてのメソッドまたはコードは 2 番目の戻り値としてエラーを返します。プログラムは通常、エラーが空でない場合に戻り値を判断する必要があります (通常は 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: 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. パニックの役割: パニックは、実行時パニック (パニック) を引き起こし、現在の関数の実行フローを停止するために使用されます。ただし、実行を停止する前に、現在の関数で defer ステートメントが実行され、その関数の実行が停止されます。次に、それを呼び出した関数の defer ステートメントの実行が続行され、ゴルーチンの呼び出しスタックがクリアされるまで同様に実行されます。最終的には、プログラム全体が中止される可能性があります。パニックは通常、配列の範囲外や null ポインター参照などの重大なエラーを処理し、プログラムがさらに深刻な問題を引き起こす可能性のあるコードを実行するのを防ぐために使用されます。
  2. リカバリの役割: リカバリは、パニックによって引き起こされたランタイム パニックをキャプチャするために使用され、それによってプログラムが中止されずに実行を継続できるようになります。通常、recover は defer と一緒に使用されます。Recovery が defer 内で呼び出されると、パニック エラー情報が取得され、エラーが返されます。パニックが発生していない場合、recover は nil を返します。リカバリを使用すると、エラーのログ記録、リカバリの実行、クリーンアップ操作の実行など、パニック発生時に適切なアクションを実行できます。リカバリは遅延内でのみ使用でき、パニックが発生した場合にのみ有効であることに注意してください。

リカバリの機能は、パニックによってスローされたエラーをキャプチャして処理することですが、Java の catch コード ブロックと同様に、defer と組み合わせて使用​​する必要があります。

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

注: Recovery を使用してパニック命令を処理するには、パニックの前に defer を宣言する必要があります。そうしないと、パニックが発生したときに、Recover はパニックをキャプチャできません。

同時プログラミングも異なります

同時実行性の観点からは、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 は、同時タスクを非常に効率的に実行できる 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 はゴルーチンとチャネルの同時実行モデルを使用します。Goroutine は軽量のユーザーレベルのスレッドであり、Go 言語ランタイム システムがそれらのスケジュールを担当します。チャネルは、異なるゴルーチン間でデータを受け渡すためのメカニズムです。よりシンプルで使いやすく、より安全なモデルです。

スレッド管理:

  • Java のスレッド管理は比較的複雑です。開発者は、スレッド オブジェクトを明示的に作成し、スレッドを開始、停止し、スレッドの同期と調整を処理する必要があります。Java は、マルチスレッド同期を処理するために、synchronized キーワードとさまざまなロックを提供します。
  • Golang の goroutine 管理はランタイム システムによって自動的に処理されるため、開発者は go キーワードを使用するだけで関数を goroutine として開始することができ、スレッドを手動で作成して管理する必要はありません。Golang は、ゴルーチン間の通信と同期のためのチャネルを提供し、同時プログラミングを大幅に簡素化します。

メモリモデル:

  • Java は Java メモリ モデル (JMM) を使用して、マルチスレッド プログラムのメモリ アクセスを管理します。JMM は、共有変数の可視性と操作の順序を指定します。
  • Golang は、よりシンプルで予測可能なメモリ モデルを使用します。Golang のメモリ モデルは Sequential Consistency です。ゴルーチン間の通信はチャネルを通じて実行され、共有データを保護するために明示的なロックが必要ないため、開発者は複雑なメモリの可視性の問題について心配する必要がありません。

同時実行ツール:

  • Java は、スレッド プール、ロック、キュー、アトミック操作などのさまざまな同時実行の問題に対処するために使用される java.util.concurrent パッケージなど、多数の同時実行ツールとクラス ライブラリを提供します。
  • Golang は、同期パッケージのロック、チャネル操作を選択するための select ステートメント、ゴルーチン、チャネル自体など、いくつかの組み込み同時実行ツールも提供します。これらのツールは比較的少数ですが、ほとんどの同時実行シナリオを処理するには十分です。

一般に、Golang の同時プログラミング モデルはよりシンプルかつ安全であり、同時性の高い分散システムやネットワーク サービスの構築に適しています。Java は、従来のエンタープライズ レベルのアプリケーションや複雑なマルチスレッド シナリオにより適していますが、より多くのスレッド管理と同期作業が必要になります。開発者がプロ​​グラミング言語を選択するときは、プロジェクトのニーズと複雑さに基づいて、どの同時実行モデルがより適しているかを検討する必要があります。

JavaとGo公式ライブラリの同期メソッドの対応

Java 同期と Golang Mutex

Java の synchronized キーワードと Golang の Mutex (ミューテックス) はどちらもマルチスレッド同期を実現するために使用されるツールですが、いくつかの違いがあります。以下に、主な違いと、その違いを説明するサンプル コードを示します。Java の同期キーワード:

  1. synchronized キーワードは Java の組み込み同期メカニズムであり、メソッドまたはコード ブロック レベルの同期に使用できます。
  2. synchronized キーワードは、メソッド レベルの同期を実装するために使用でき、コード ブロックの同期にも使用できます。
  3. synchronized キーワードは、synchronized ブロックに入る前にロックを取得し、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 は、同期パッケージの Mutex タイプを使用して同期を実装します。
  2. Mutex はコード ブロック レベルの同期を実装するために使用できますが、通常はメソッド レベルの同期には使用されません (Golang の同時実行モデルは同時実行を処理するためにゴルーチンとチャネルを使用することを好むため)。
  3. Golang では、クリティカル セクション内の 1 つの goroutine だけが共有リソースにアクセスできるようにするために、Mutex を明示的にロックおよびロック解除する必要があります。
  4. ミューテックスのロックとロック解除は明示的であり、コード内で 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 では、複数のスレッドまたはゴルーチンが共有リソースに安全にアクセスできるようにするために、条件変数はロック (ミューテックスまたは同期ブロック) とともに使用されることがよくあります。条件変数は、スレッドまたはゴルーチン間の調整のための待機および通知メカニズムを提供します。
  2. 複数条件の待機: Java では、複数の条件変数 (Lock と Condition によって実装) を使用して、さまざまな条件を待機することができ、それによってスレッドの待機と通知をより正確に制御できます。Golang では、sync.Cond 構造を使用して、複数のゴルーチンを待機し、通知することができます。

違い:

  1. 条件変数の作成:
  • Java では、条件変数は通常、Lock インターフェイスの newCondition メソッドを通じて作成されます。
  • Golang では、条件変数は sync.Cond 構造体で表されますが、sync.Mutex と組み合わせて使用​​する必要があります。条件変数は sync.NewCond(&mutex) を通じて作成されます。ここで、mutex はミューテックス ロックです。
  1. サスペンドとウェイクアップのタイミング:
  • Java では、通常、条件変数の待機や通知はロックの保護下で行われます。オブジェクトの wait() メソッド、notify()、および NoticeAll() メソッドは、ロックを取得した後にのみ呼び出すことができます。
  • G olang では、sync.Cond の Wait() メソッドはロックを取得した後に呼び出されますが、通知メソッド Broadcast() と Signal() はロックが解除された後に実行されます。つまり、通知が送信された後、待機goroutine は再度競合する必要があります。ロックします。

一般に、Java と Golang の条件変数の概念は似ており、両方ともスレッドまたはゴルーチン間の同期と通信に使用されます。ただし、特定の使用法とタイミングにはいくつかの違いがあります。特に Golang では、条件変数の通知はロックの保護下で実行する必要はありません。これにより、潜在的なデッドロックの問題を回避できますが、開発者はより多くのことを行う必要があります。競合状態には慎重に対処してください。

CAS/アトミック

Javaの場合:

  • CAS 操作は、volatile キーワードと、AtomicInteger、AtomicLong などの java.util.concurrent.atomic パッケージ内のクラスによってサポートされています。これらのクラスは、整数および長整数に対してアトミック操作を実行するために使用できるアトミック操作を提供します。
  • java.util.concurrent パッケージは、スレッド セーフを実現するために CAS 操作を使用する、ConcurrentHashMap、ConcurrentLinkedQueue などのいくつかのロックフリー データ構造も提供します。
  • Java 9 では、整数や長整数に限定されない、より一般的なアトミック操作を提供する VarHandle が導入されました。

Golang の場合:

  • Golang の sync/atomic パッケージは、atomic.AddInt32、atomic.CompareAndSwapInt64 など、整数型でアトミック操作を実行するために使用できる一連のアトミック操作関数を提供します。これらの関数を使用すると、マルチゴルーチン環境でアトミックな操作を保証できます。
  • atomic.Value 型は、任意の型の値を格納し、それらに対してアトミックなロードおよびストア操作を実行できるアトミック操作メカニズムを提供します。
  • Golang の組み込みマップ タイプはスレッド セーフではありませんが、sync.Map タイプを使用して、CAS 操作を使用してスレッド セーフを確保するスレッド セーフ マッピングを実装できます。

つまり、Java と Golang は両方とも CAS 操作とアトミック操作をサポートしていますが、具体的な実装と使用法にはいくつかの違いがあります。マルチスレッドまたはマルチゴルーチン環境では、これらの操作により共有変数のアトミック性が保証され、競合状態やデータ競合の問題が回避されます。マルチスレッド プログラミングに使用する言語とライブラリを選択するときは、特定のニーズと言語の機能に基づいて決定できます。

シングルトンパターン

Java 遅延シングルトン (遅延初期化):

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;
    }
}

積極的な初期化:

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、CMS (同時マークスイープ) GC、G1 (ガベージファースト) GC などの複数の 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. ガベージ コレクター: Java のガベージ コレクターには、G1 (ガベージ ファースト)、CMS (同時マークスイープ)、シリアル、パラレル (パラレル スカベンジおよびパラレル オールド) などを含む多くの実装があります。各ガベージ コレクターには独自の特性と適用可能なシナリオがあります。
  2. ガベージ コレクション アルゴリズム: Java は、主にマーク アンド スイープ (マーク アンド スイープ)、マーク アンド コンパクト (マーク アンド コンパクト)、コピー (コピー)、世代別コレクション (世代コレクション) など、さまざまなガベージ コレクション アルゴリズムを使用してメモリを管理します。さまざまなメモリ領域のガベージ コレクションを処理するには、さまざまなアルゴリズムが使用されます。
  3. 到達可能性アルゴリズム: Java は到達可能性分析 (到達可能性分析) を使用して、どのオブジェクトが到達可能か、どのオブジェクトが到達不可能かを判断します。これは、物品がリサイクル可能かどうかを判断するための重要な基準です。
  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() などのランタイム パッケージ内の関数を使用して 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) コンパイル戦略は 2 つの異なるコンパイルおよび実行戦略であり、コンパイルとパフォーマンスにおいていくつかの重要な違いがあります。比較すると次のようになります。

1. コンパイルのタイミング:

  • JIT コンパイル: Java では、通常、コードは最初にバイトコード (バイトコード) にコンパイルされ、次に実行時に 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 は多くのエンタープライズ レベルのアプリケーションにとって推奨される開発言語の 1 つになりました。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 の使用に適している場合があります。どちらにも、特定の使用例に応じて、独自の利点と機能があります。

簡単な概要

おすすめ

転載: blog.csdn.net/u012562943/article/details/133026010