本文針對Golang與Java的基礎語法、結構體函數、異常處理、併發編程及垃圾回收、資源消耗等各方面的差異進行對比總結,有不准確、不到位的地方還請大家不吝賜教。 ...
本文針對Golang與Java的基礎語法、結構體函數、異常處理、併發編程及垃圾回收、資源消耗等各方面的差異進行對比總結,有不准確、不到位的地方還請大家不吝賜教。
一 、基礎語法
Golang: 編碼風格及可見域規則嚴格且簡單;Java: 來說層次介面清晰、規範,主要表現有以下這些。
1、變數
a、變數聲明及使用
在Java中:變數可以聲明瞭卻不使用
public static String toString(int num) { int data = num; return String.valueOf(num); }
Golang中:聲明的變數必須被使用,否則需要使用_來替代掉變數名,表明該變數不會比使用到
func toString(num int) string {
data := num // data沒有使用者,無法編譯
return strconv.Itoa(num)
}
func toString(num int) string {
_ := num // 正常編譯
return strconv.Itoa(num)
}
b、變數聲明及初始化
在Java中:如果在方法內部聲明一個變數但不初始化,在使用時會出現編譯錯誤;
public void compareVariable() {
int age;
Object object;
System.out.println(age); // 編譯錯誤
System.out.println(object); // 編譯錯誤
}
在Golang中:對於基本類型來講,聲明即初始化;對於引用類型,聲明則初始化為nil。
func compareVariable() {
var age int
var hashMap *map[string]int
fmt.Println(num) // num = 0
fmt.Println(hashMap) // &hashMap== nil
}
2、作用域規則
Java: 對方法、變數及類的可見域規則是通過private、protected、public關鍵字來控制的,具體如下
作用域 | 當前類 | 同一package | 子孫類 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default(無修飾詞) | √ | √ | × | × |
private | √ | × | × | × |
Golang: 控制可見域的方式只有一個,當欄位首字母開頭是大寫時說明其是對外可見的、小寫時只對包內成員可見。
3、逗號 ok 模式
在使用Golang編寫代碼的過程中,許多方法經常在一個表達式返回2個參數時使用這種模式:,ok,第一個參數是一個值或者nil,第二個參數是true/false或者一個錯誤error。在一個需要賦值的if條件語句中,使用這種模式去檢測第二個參數值會讓代碼顯得優雅簡潔。這種模式在Golang編碼規範中非常重要。這是Golang自身的函數多返回值特性的體現。例如:
if _, ok := conditionMap["page"]; ok {
//
}
4、結構體、函數以及方法
a、結構體聲明及使用
在Golang中區別與Java最顯著的一點是,Golang不存在“類”這個概念,組織數據實體的結構在Golang中被稱為結構體。函數可以脫離“類”而存在,函數可以依賴於結構體來調用或者依賴於包名調用。Golang中的結構體放棄了繼承、實現等多態概念,結構體之間可使用組合來達到復用方法或者欄位的效果。
Golang 聲明一個結構體並使用:
// User 定義User結構體
type User struct {
Name string
Age int
}
// 使用一個結構體
func main() {
personPoint := new(User) // 通過new方法創建結構體指針
person1 := User{} // 通過Person{}創建預設欄位的結構體
person2 := User{
Name: "xiaoHong",
Age: 21,
}
fmt.Println(personPoint) // &{ 0 }
fmt.Println(person1) // { 0 }
fmt.Println(person2) // {xiaoHong 21 }
}
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}
b、函數和方法的區別
在Java中:所有的“函數”都是基於“類”這個概念構建的,也就是只有在“類”中才會包含所謂的“函數”,這裡的“函數”被稱為“方法”,可見上方聲明實體並使用。
在Golang中:“函數”和“方法”的最基本區別是:函數不基於結構體而是基於包名調用,方法基於結構體調用。如下實例:
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)
}
func main() {
userPoint := new(entity.User) // 通過new方法創建結構體指針
entity.Solve(userPoint) // 函數調用
userPoint.Solve() // 方法調用
}
5、值類型、引用類型以及****指針
Java:在Java中不存在顯式的指針操作;8種基本數據類型是值類型,數組和對象屬於引用類型。
Golang:而Golang中存在顯式的指針操作,但是Golang的指針不像C那麼複雜,不能進行指針運算;所有的基本類型都屬於值類型,但是有幾個類型比較特殊,表現出引用類型的特征,分別是slice、map、channel、interface,除賦值以外它們都可以當做引用類型來使用,因此當我們這樣做時,可以直接使用變數本身而不用指針。
註:slice與數組的區別為是否有固定長度,slice無固定長度,數組有固定長度。值得註意的是,在Golang中,只有同長度、同類型的數組才可視為“同一類型”,譬如[]int和[3]int則會被視為不同的類型,這在參數傳遞的時候會造成編譯錯誤。
a、數組對比
在Java中:當向方法中傳遞數組時,可以直接通過該傳入的數組修改原數組內部值(淺拷貝)。 在Golang中:則有兩種情況:在不限定數組長度(為slice)時也直接改變原數組的值,當限定數組長度時會完全複製出一份副本來進行修改(深拷貝):
Java的數組實踐:
public static void main(String[] args) {
int[] array = {1, 2, 3};
change(array);
System.out.println(Arrays.toString(array)); // -1,2,3
}
private static void change(int[] array) {
array[0] = -1;
}
Golang的數組實踐:
// 不限定長度(即slice):
func main() {
var array = []int{1, 2, 3}
change(array)
fmt.Println(array) // [-1 2 3]
}
func change(array []int) {
array[0] = -1
}
// 限定長度(即數組):
func main() {
var array = [3]int{1, 2, 3}
change(array)
fmt.Println(array) //[1 2 3]
}
func change(array [3]int) {
array[0] = -1
}
b、對象對比
在Golang中:傳入函數參數的是原對象的一個全新的copy(有自己的記憶體地址);go對象之間賦值是把對象記憶體的 內容(欄位值等) copy過去,所以才會看到globalUser修改前後的地址不變,但是對象的內容變了。 在Java中:傳入函數參數的是原對象的引用的copy(指向的是同樣的記憶體地址); Java對象之間的賦值是把對象的引用 copy過去,因為引用指向的地址變了,所以對象的內容也變了。
Golang的對象實踐:
//User 定義User結構體
type User struct {
Name string
Age int
}
// 定義一個全局的User
var globalUser = User {
"xiaoming",
28,
}
// modifyUser 定義一個函數,參數為User結構體“對象”,將全局globalUser指向傳遞過來的User結構體“對象”
func modifyUser(user User) {
fmt.Printf("參數user的地址 = %p\n",&user)
fmt.Printf("globalUser修改前的地址 = %p\n",&globalUser)
fmt.Println("globalUser修改前 = ",globalUser)
// 修改指向
globalUser = user
fmt.Printf("globalUser修改後的地址 = %p\n",&globalUser)
fmt.Println("globalUser修改後 = ",globalUser)
}
func main() {
var u User = User {
"xiaohong",
29,
}
fmt.Printf("將要傳遞的參數u的地址 = %p\n",&u)
modifyUser(u)
}
// 執行結果
將要傳遞的參數u的地址 = 0xc0000ac018
參數user的地址 = 0xc0000ac030
globalUser修改前的地址 = 0x113a270
globalUser修改前 = {xiaoming 28}
globalUser修改後的地址 = 0x113a270
globalUuser修改後 = {xiaohong 29}
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 {
private static User globalUser = new User("xiaoming",28);
public static void modifyUser(User user) {
System.out.println("參數globalUser的地址 = " + user);
System.out.println("globalUser修改前的地址 = " + globalUser);
System.out.println("globalUser修改前 = " + globalUser.print());
globalUser = user;
System.out.println("globalUser修改後的地址 = " + globalUser);
System.out.println("globalUser修改後 = " + globalUser.print());
}
public static void main(String[] args) {
User user = new User("xiaohong", 29);
System.out.println("將要傳遞的參數user的地址 = " + user);
modifyUser(user);
}
}
//執行結果
將要傳遞的參數user的地址 = com.example.demo.User@5abca1e0
參數globalUser的地址 = com.example.demo.User@5abca1e0
globalUser修改前的地址 = com.example.demo.User@2286778
globalUser修改前 = {name = xiaoming,age = 28}
globalUser修改後的地址 = com.example.demo.User@5abca1e0
globalUser修改後 = {name = xiaohong,age = 29}
c、指針的區別
在Java中:如果傳遞了引用類型(對象、數組等)會複製其指針進行傳遞 在Golang中:必須要顯式傳遞Person的指針,不然只是傳遞了該對象的一個副本。
Golang的指針:
// User 定義User結構體
type User struct {
Name string
Age int
}
func main() {
p1 := User{
Name: "xiaohong",
Age: 21,
}
changePerson(p1)
fmt.Println(p1.Name) // xiaohong
changePersonByPointer(&p1)
fmt.Println(p1.Name) // xiaoming
}
func changePersonByPointer(user *User) {
user.Name = "xiaoming"
}
func changePerson(user User) {
user.Name = "xiaoming"
}
Java的指針:
public class Demo {
public static void changePerson(User user) {
user.setName("xiaoming");
}
public static void main(String[] args) {
User user = new User("xiaohong", 29);
changePerson(user);
System.out.println("user信息:" + user.getName()); // xiaoming
}
}
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;
}
}
二、面向對象
在Golang中:沒有明確的OOP概念,Go語言只提供了兩個關鍵類型:struct,interface。 在Java中: 面向對象語言的封裝、繼承、多態的特性以及“繼承(extends)、實現(implements)”等關鍵字。
1、Java的OOP與Golang的結構體組合
假設有這麼一個場景:動物(Animal)具備名字(Name)、年齡(Age)的基本特性,現在需要實現一個狗(Dog),且Dog需要具備Animal所需的所有特性,並且自身具備犬吠(bark())的動作。
首先來看看最熟悉的Java要如何寫,很簡單,使用抽象類描述Animal作為所有動物的超類,Dog extends Animal:
public abstract class Animal {
String name;
int age;
}
public class Dog extends Animal {
public void bark() {
System.out.println(age + "歲的" + name + "在汪汪叫");
}
}
public class Demo {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "小龍";
dog.age = 2;
dog.bark(); // 2歲的小龍在汪汪叫
}
}
在Golang中,可以這樣通過結構體的組合來實現:
package main
import "fmt"
type Animal struct {
Name string
Age int
}
type Dog struct {
*Animal
}
func (dog *Dog) Bark() {
fmt.Printf("%d歲的%s在汪汪叫", dog.Age, dog.Name)
}
func main() {
dog := &Dog{&Animal{
Name: "小龍",
Age: 2,
}}
dog.Bark() // 2歲的小龍在汪汪叫...
}
2、侵入式與非侵入式介面
在Java中:介面主要作為不同組件之間的契約存在。對契約的實現是強制的,你必須聲明你的確實現了該介面。這類介面我們稱為侵入式介面。“侵入式”的主要表現在於實現類需要明確聲明自己實現了某個介面。
在Golang中:非侵入式介面不需要通過任何關鍵字聲明類型與介面之間的實現關係,只要一個類型實現了介面的所有方法,那麼這個類型就是這個介面的實現類型。
Java:管理狗的行為,可以通過以下介面實現:
public interface Dog {
void Bark();
}
public class DogImpl implements Dog{
@Override
public void Bark() {
System.out.println("汪汪叫");
}
}
public class Demo {
public static void main(String[] args) {
Dog dog = new DogImpl();
dog.Bark(); // 汪汪叫
}
}
Golang: 假設現在有一個Factory介面,該介面中定義了Produce()方法及Consume()方法,CafeFactory結構體作為其實現類型,那麼可以通過以下代碼實現:
package entity
type Factory interface {
Produce() bool
Consume() bool
}
type CarFactory struct {
ProductName string
}
func (c *CarFactory) Produce() bool {
fmt.Printf("CarFactory生產%s成功", c.ProductName)
return true
}
func (c *CarFactory) Consume() bool {
fmt.Printf("CarFactory消費%s成功", c.ProductName)
return true
}
// --------------
package main
func main() {
factory := &entity.CarFactory{"Car"}
doProduce(factory)
doConsume(factory)
}
func doProduce(factory entity.Factory) bool {
return factory.Produce()
}
func doConsume(factory entity.Factory) bool {
return factory.Consume()
}
Golang的非侵入式介面優點:簡單、高效、按需實現
在Go中,類沒有繼承的概念,只需要知道這個類型實現了哪些方法,每個方法是啥行為。
實現類型的時候,只需要關心自己應該提供哪些方法,不用再糾結介面需要拆得多細才合理。介面由使用方按需定義,而不用事前規劃
減少包的引入,因為多引用一個外部的包,就意味著更多的耦合。介面由使用方按自身需求來定義,使用方無需關心是否有其他模塊定義過類似的介面
Java的侵入式介面優點: 層次結構清晰,對類型的動作行為有嚴格的管理
三、異常處理
在Java中: 通過try..catch..finally的方式進行異常處理,有可能出現異常的代碼會被try塊給包裹起來,在catch中捕獲相關的異常併進行處理,最後通過finally塊來統一執行最後的結束操作(釋放資源)。
在Golang中:錯誤處理方式有兩種方式:**,ok模式** 與 defer、panic及recover的組合
1、Java的異常處理:
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("回收資源");
}
}
}
2、Golang的異常處理:
Golang的**,ok模式**。所有可能出現異常的方法或者代碼直接把錯誤當作第二個響應值進行返回,程式中對返回值進行判斷,非空則進行處理並且立即中斷程式的執行。 優點:這種比Java的簡單很多,是Golang在異常處理方式上的一大特色。
缺點:代碼冗餘,所有的異常都需要通過if err != nil {}
去做判斷和處理,不能做到統一捕捉和處理,容易遺漏。
func main() {
value, err := Bark()
if err != nil {
// 返回了異常,進行處理
log.error("...異常:", err)
return err
}
// Bark方法執行正確,繼續執行後續代碼
Process(value)
}
Golang的defer、panic及recover
defer是Golang錯誤處理中常用的關鍵字,pannic及recover是Golang中的內置函數,通常與defer結合進行錯誤處理,它們各自的用途為:
defer的作用是延遲執行某段代碼,一般用於關閉資源或者執行必須執行的收尾操作,無論是否出現錯誤defer代碼段都會執行,類似於Java中的finally代碼塊的作用;defer也可以執行函數或者是匿名函數:
defer func() {
// 清理工作
} ()
// 這是傳遞參數給匿名函數時的寫法
var num := 1
defer func(num int) {
// 做你複雜的清理工作
} (num)
需要註意的是,defer使用一個棧來維護需要執行的代碼,所以defer函數所執行的順序是和defer聲明的順序相反的。
defer fmt.Println(a)
defer fmt.Println(b)
defer fmt.Println(c)
執行結果:
c
b
a
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
註:利用recover處理panic指令,defer必須在panic之前聲明,否則當panic時,recover無法捕獲到panic。
四、併發編程
Java 中 CPU 資源分配對象是 Thread,Go 中 CPU 資源分配對象是 goroutine。Java Thread 與系統線程為一一對應關係,goroutine 是 Go 實現的用戶級線程,與系統線程是 m:n 關係。
1、Java 和 Golang 的基本實現:
在 Java 中,如要獲得 CPU 資源並非同步執行代碼單元,需要將代碼單元包裝成 Runnable,並創建可以運行代碼單元的 Thread ,執行 start 方法啟動線程。
Runnable task = ()-> System.out.println("task running");
Thread t = new Thread(task);
t.start();
Java 應用一般使用線程池集中處理任務,以避免線程反覆創建回收帶來的開銷。
Runnable task = ()-> System.out.println("task running");
Executor executor = Executors.newCachedThreadPool();
executor.execute(task);
在 Golang 中,則需要將代碼包裝成函數。使用 go
關鍵字調用函數之後,便創建了一個可以運行代碼單元的 goroutine。一旦 CPU 資源就緒,對應的代碼單元便會在 goroutine 中執行。
go func() {
fmt.Println("test task running")
}()
2、Java 和 Golang 的區別:
Golang語言採用了CSP(Communicating Sequential Processes)的模型,其中以goroutine和channel作為主要實現手段。 Java則採用了多線程模型,其中以Thread和Synchronization作為主要實現手段。 Golang語言的goroutine是一種輕量級的線程,它們的創建和銷毀速度比Java中的線程快得多。在Java中,創建和銷毀線程都需要相當大的開銷。
Golang語言的channel是一種同步數據傳遞的機制,它可以方便地解決多道程式之間的通信問題。Java中則需要使用同步工具(如Semaphore、CountDownLatch等)來解決多線程之間的通信問題。
Java 和 Go 官方庫中同步方式的對應關係
Java | Golang | |
---|---|---|
鎖 | synchronized,ReentrantLock | sync.Mutex, one unit buffered channel |
讀寫鎖 | ReentrantReadWriteLock, StampedLock | sync.RWMutex |
條件變數 | condition | sync.Cond |
CAS/Atomic | Varhandle、volatile,Atomic 類 | atomic.Value,atomic 包 |
once | 單例模式 | sync.Once |
a、Java synchronized 與Golang Mutex
Java synchronized:線程 A 在 t1 時刻釋放 JVM 鎖後(monitor exit),在隨後的 t2 時刻,若任意線程 B 獲取到 JVM 鎖(monintor enter),則線程 A 在 t1 時刻之前發生的所有寫入均對 B 可見。synchronized 是 JVM 內置鎖實現,寫入 volatile 變數相當於 monitor exit,讀取 volatile 變數相當於 monintor enter。(即一把鎖只能同時被一個線程獲取,沒有獲得鎖的線程只能阻塞等待)
synchronized的使用: 修飾一個代碼塊,被修飾的代碼塊稱為同步代碼塊,作用範圍是大括弧{}括起來的代碼;
public void method()
{
synchronized(this) {
// todo some thing
}
}
修飾一個方法,被修飾的方法稱為同步方法,其作用範圍是整個方法;
public synchronized void method()
{
// todo some thing
}
修改一個靜態方法,作用範圍是整個靜態方法;
public synchronized static void method() {
// todo some thing
}
修改一個類,作用範圍是synchronized後面括弧括起來的部分。
class DemoClass {
public void method() {
synchronized(DemoClass.class) {
// todo some thing
}
}
}
Go Mutex:Go 並未像 Java 一樣提供 volatile 這樣基礎的關鍵字,但其 Mutex 相關記憶體模型和 synchronized 或 Java 官方庫 Lock 實現有十分接近語義。若 goroutine A 在 t1 時刻釋放 sync.Mutex 或 sync.RWMutex 後,在隨後的 t2 時刻,若任意 goroutine B 獲取到鎖,則 goroutine A 在 t1 時刻之前發生的所有寫入均對 B 可見。
Mutex的使用:
修飾關鍵代碼:每次只有一個線程對這個關鍵變數進行修改,避免多個線程同時這個關鍵代碼進行操作。
func main() {
var mutex sync.Mutex
count := 0
for i := 0; i < 100; i++ {
go func() {
mutex.Lock() // 加鎖
count += 1
mutex.Unlock() // 解鎖
}()
}
// 休眠,等待2s
time.Sleep(time.Second * 2)
// 100,沒有加鎖結果不正確
fmt.Println("count = ", count)
}
修飾結構體: 帶鎖結構體初始化後,直接調用對應的線程安全函數就可以。
type count struct {
lock sync.Mutex
value int
}
// 結構體對應的結構方法
func (receiver *count) countOne() {
receiver.lock.Lock()
defer receiver.lock.Unlock()
receiver.value++
}
func main() {
c := count{
lock: sync.Mutex{},
value: 0,
}
group := sync.WaitGroup{}
for i := 0; i < 10; i++ {
group.Add(1)
go func(count2 *count) {
defer group.Done()
for i := 0; i < 100; i++ {
count2.countOne()
}
}(&c)
}
group.Wait()
fmt.Printf("The count value is %d", c.value) // The count value is 1000
}
b、條件變數
Java 和 Golang 相似點:一般來說,條件變數衍生於鎖,不同條件變數只是同一鎖空間下的不同等待隊列。Java 可以使用 synchronized 代碼塊保護特定代碼路徑,兼而可以在 synchronized 代碼塊中使用 Object wait 和 notify、notifyall 方法實現單一條件等待。如果需要多個條件,可以使用官方庫提供的 Lock 實現和 Condition 實現。
Java 和 Golang 區別點:Java 創建條件變數的方式是調用 Lock 介面 newCondition 方法。Go sync.Cond 結構體需設置 sync.Mutex 欄位才能工作,掛起方法為 Wait,喚醒方法為 Braodcast。Go 語言裡面條件變數的通知 Signal() 和 Broadcast(),並沒有在鎖的保護下執行,而是在 Unlock() 之後執行。
c、CAS/Atomic
原子性:一個或者多個操作在 CPU 執行的過程中不被中斷的特性,稱為原子性(atomicity)。CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變數時,只有其中一個線程能更新變數的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。Java 和 Go 均支持 CAS 及原子操作。
在Java中: CAS 操作由 volatile 關鍵字和 VarHandle(9 之前是 UnSafe)支持,在此基礎上有了 Atomic 類和併發包中的大量無鎖實現(如 ConcurrentHashMap, AQS 隊列等)。
在Golang中:atomic.Value 提供了 CAS 操作基礎,它保證任意類型(interface {}) 的 Load 和 Store 為原子操作,在此基礎上有 atomic 包。
d、Once 與單例模式
sync.Once 是 Golang 標準庫提供的使函數只執行一次的實現,常應用於單例模式,例如初始化配置、保持資料庫連接等。它有 2 個特性:
保證程式運行期間某段代碼只會執行一次
如果多個 goroutine 同時執行 Once 守護代碼,只有 1 個 goroutine 會獲得執行機會,其他 goroutine 會阻塞直至代碼執行完畢。
func main() {
var once = sync.Once{}
f := func() {
time.Sleep(10 * time.Millisecond)
fmt.Println("do once")
}
go func() {
fmt.Println("do once start")
once.Do(f)
fmt.Println("do once finish")
}()
time.Sleep(1 * time.Millisecond)
for i := 0; i < 2; i++ {
go func() {
fmt.Println("block...")
once.Do(f)
fmt.Println("resume")
}()
}
time.Sleep(10 * time.Millisecond)
}//~
do once start
block...
block...
do once
do once finish
resume
resume
java中單例模式的寫法有好幾種,主要是懶漢式單例、餓漢式單例。
懶漢式單例: 懶漢式單例的實現沒有考慮線程安全問題,需要結合synchronized,保證線程安全
//懶漢式單例類.在第一次調用的時候實例化自己
public class Singleton {
private Singleton() {}
private static Singleton single=null;
//靜態工廠方法
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
餓漢式單例:餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不再改變,所以天生是線程安全的。
//餓漢式單例類.在類初始化時,已經自行實例化
public class Singleton {
private Singleton() {}
private static final Singleton single = new Singleton();
//靜態工廠方法
public static Singleton getInstance() {
return single;
}
}
五、垃圾回收
GC(Garbage Collection)垃圾回收是一種自動管理記憶體的方式,支持GC的語言無需手動管理記憶體,程式後臺自動判斷對象是否存活並回收其記憶體空間,使開發人員從記憶體管理上解脫出來。 因為支持更多的特性和更靈活多樣的GC策略, 比如分代,對象可移動,各種參數調節等等. 而Go只做了一種GC方案,不分代,不可移動,沒什麼參數能調節,而且更註重暫停時間的優化,執行GC的時機更頻繁, 所以Go通常更占更少的記憶體,但代價就是GC性能比JVM差了不少。
1、Java的垃圾回收體系
Java基於JVM完成了垃圾收集的功能,其體系很龐大,包括了垃圾回收器(G1、CMS、Serial、ParNew等)、垃圾回收演算法(標記-清除、標記-整理、複製、分代收集)、可達性演算法(可達性分析、引用計數法)、引用類型、JVM記憶體模型等內容。經過多代發展,Java的垃圾回收機制較為完善,Java劃分新生代、老年代來存儲對象。對象通常會在新生代分配記憶體,多次存活的對象會被移到老年代,由於新生代存活率低,產生空間碎片的可能性高,通常選用“標記-複製”作為回收演算法,而老年代存活率高,通常選用“標記-清除”或“標記-整理”作為回收演算法,壓縮整理空間。
2、Golang GC特征
三色標記、併發標記和清掃、非分代、非緊縮、寫屏障
a、三色標記
a、程式開始時有黑白灰三個集合,初始時所有對象都是白色; b、從root對象開始標記,將所有可達對象標記為灰色; c、從灰色對象集合取出對象,將其引用對象標記為灰色,放入灰色集合,並將自己標記為黑色; d、重覆第三步,直到灰色集合為空,即所有可達對象全部都被標記; e、標記結束後,不可達白色對象即為垃圾,對記憶體進行迭代清掃,回收白色對象; f、重置GC狀態;
b、非分代
Java採用分代回收(按照對象生命周期長短劃分不同的代空間,生命周期長的放入老年代,短的放入新生代,不同代有不同的回收演算法和回收頻率),Golang沒有分代,一視同仁;
c、非緊縮
在垃圾回收之後不會進行記憶體整理以清除記憶體碎片;
d、寫屏障
在併發標記的過程中,如果應用程式修改了對象圖,就可能出現標記遺漏的可能,寫屏障是為了處理標記遺漏的問題。
六、資源消耗對比
在記憶體利用效率上,Go語言確實比Java做得更好,在4個不同的角度來總結:
1、Java的JIT策略比Golang的AOT策略
Java在運行時相比Golang多占用了一些記憶體。原因在於:
Java運行態中包含了一個完整的解釋器、一個JIT編譯期以及一個垃圾回收器,這會顯著地增加記憶體。
Golang語言直接編譯到機器碼,運行態只包含機器碼和一個垃圾回收器。
因此Golang的運行態相對消耗記憶體較少。
2、記憶體分配和垃圾回收器
Java確實在起步占用上偏多,畢竟jvm需要更多記憶體做jit,預設的gc演算法對記憶體要求偏高,但這不能代表後續占用仍然線性增長。如果目標是啟動成百上千個記憶體需求較少的進程,那Java確實不擅長。
3、併發
協程模型比線程模型更加節省記憶體。
4、反射
Golang的反射更加簡單,導致框架的記憶體消耗Golang程式比Java程式優秀。主要是因為: Java的框架實現中大量使用反射,並使用hashmap緩存信息,這2個都是極度消耗記憶體的行為。 Golang的框架中也使用reflect、map。但是Golang是面向interface和值類型的,這導致Golang的反射模型要比Java的反射模型簡單非常多,反射過程要產生的對象數量也少非常多。
七、生態
Java 在生態這方面簡直是無敵的存在,這主要得益於 Spring 全家桶,Spring 讓 Java 走上了神座。Golang 語言知名的框架也很多,但是遠遠沒有 Spring 影響那麼大。
總結
優點 | 缺點 | |
---|---|---|
Golang | 代碼簡潔性 靜態類型可編譯成機器碼直接運行 天生多核並行 垃圾收集 跨平臺且不依賴運行時環境 簡潔的泛型 | 有限的庫支持 泛型不夠完善 靈活度沒Java高(這個可算優點也可算缺點) |
java | 優秀的生態 優秀的三方庫 多線程 靈活性高 平臺獨立性 完善的語言特性 代碼結構層次清晰 | 大量冗餘的陳舊實現導致性能不佳 生態的複雜性 複雜的繼承機制 |
作者:xindong
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Summary-of-Comprehensive-Comparison-between-Golang-and-Java.html