Go程式語言評估報告

尹成發表於2018-05-21

 

Go程式語言評估報告

 

1. Go簡介

 

Go是由Google於2007年9月21日開始開發,2009年11月10日開放原始碼,2012年

328日推出第一個正式版本的通用型程式語言。它為系統程式設計而7設計,是強型別化的

語言,具有垃圾回收機制,並顯式支援併發程式設計。Go程式由包構造,以此來提供高效的

依賴管理功能。當前的編譯器實現使用傳統的編譯-連結模型來生成可執行的二進位制文

件。


十年以來,主流的系統級程式語言並未出現過,但在這期間,計算環境已經發生了巨大的

變化。以下是一些變化趨勢:


相計算機的速度變得極快,但軟體開發還不夠快。

在今天,依賴管理已然成為了軟體開發中當重要的部分,但傳統的C家族語言以頭文

的方式組織原始碼,這與清晰的依賴分析以及快速編譯背道而馳。

Java和C++等語言的型別系統比較笨重,人們的反抗越來越多,因此他們轉向了

PythonJavaScript之類的動態型別語言。

目前流行的系統語言對於像垃圾回收及平行計算等基本思想的支援並不算好。

多核計算機的出現產生了一些麻煩與混亂。

隨著系統的龐大,分散式的要求越來越高,現有的語言開發分散式系統越來越笨重以及難以維護。

 

Go則是一種新的語言,一種併發的、帶垃圾回收的、快速編譯的語言。它滿足了以下特點:




它可以在一臺計算機上用幾秒鐘的時間編譯一個大型的Go程式。

Go為軟體構造提供了一種模型,它使依賴分析更加容易,且避免了大部分C風格

include檔案與庫的開頭。

Go是靜態型別的語言,它的型別系統沒有層級。因此使用者不需要在定義型別間的關係上

花費時間,這樣感覺起來比典型的面嚮物件語言更輕量級。

Go完全是垃圾回收型的語言,併為併發執行與通訊提供了基本的支援。

按照其設計,Go打算為多核機器上系統軟體的構造提供一種方法。


 


Go試圖成為結合解釋型程式設計的輕鬆、動態型別語言的高效以及靜態型別語言的安全的編譯型語

言。它也打算成為現代的,支援網路與多核計算的語言。要滿足這些目標,需要解決一些語言上

的問題:一個富有表達能力但輕量級的型別系統,併發與垃圾回收機制,嚴格的依賴規範等等。

這些無法通過庫或工具解決好,因此Go也就應運而生了。


 


2. C/C++的缺陷

 

a. 全域性變數的初始化順序

由於在C/C++中,全域性變數的初始化順序並不確定,因此依賴於全域性變數初始化

順序的操作,可能會給程式帶來不可預知的問題。

 

b. 變數預設不會被初始化

由於變數預設不會被初始化,因此如果在程式中忘記初始化某個變數,就有可能

造成一些奇怪的細節性錯誤,以至於在Coding Standard中都為之專門加以強調。

 

c. 字符集的支援1

C/C++最先支援的字符集是ANSI雖然在C99/C++98之後提供了對Unicode的支

持,但在日常的編碼工作中卻要在ANSIUnicode之間來回轉換,相當地繁瑣。

 

d. 複雜的繼承模式

C++提供了單/多繼承,而多繼承則引入了大量的複雜性,比如鑽石型繼承等。

細節請參閱《深度探索C++物件模型》。

 

e. 對併發的支援

在C++中,併發更多的是通過建立執行緒來啟用,而執行緒間的通訊則是通過加鎖共

享變數來實現,很容易死鎖。雖然在C++11中新增了併發處理機制,但這給本來就十分復

雜的型別系統又新增了更重的負擔。

 

f. 不支援自動垃圾回收

關於這一點存在爭議。記憶體洩漏是C/C++程式設計師經常遭遇到的問題,但隨著垃圾

回收演算法的成熟,對於大多數開發者來說,自動回收帶來的便利已經超過手工操作提高的

效率。而在C++中雖然可使用智慧指標來減少原生指標的使用,但不能杜絕它,因此這個

問題仍然存在。

 

g. 落後的包管理機制

C/C++中採用.h/.c(pp)檔案來組織程式碼,這種方式使編譯時間變得過於漫長

C++中的模板更讓這個問題雪上加霜。

 

h. C++編譯器總會私自生成一些程式碼

比如: 建構函式/解構函式/new/delete等。如果是動態庫,當include不同版本的頭

檔案時,容易生成版本不相容的程式碼。

 

i. 不加約束的指標使用是導致C/C++軟體BUG的重要根源之一

3. Go的優勢

1


C/C++編譯時的編碼方式也不確定。main.cpp如果是UTF-8編碼, 在gcc和VC下的行為也

會不一樣。——柴樹杉


正如語言的設計者之一Rob Pike所說:

我們——KenRobert和我自己曾經是C++程式設計師,我們設計新的語言是為了解

決那些我們在編寫軟體時遇到的問題。

 

這些問題中的大部分,就是在第2節中列舉的內容。這一小節就是Go針對這些缺陷提出的

解決方案。

 

a. Init

每個包都可以定義一個或多個init函式2(原型為 func init()),init函式在包初次

被匯入時呼叫,同一個包內的多個init函式的執行的順序是不定的,而如果這個包又匯入

了其他的包,則級連呼叫,所有包import完成,所有init函式執行完後,則開始main的執

行。

而對於全域性變數,以一個簡單的例子來說明:

 

// package p

var gInt int

 

// package a

import "p"

 

// package b

import "p"

 

// package main

import (

"a"

"b"

)

 

在package p中,我們定義了一個全域性變數gInt,而p被package a,b所import,接

著package main又按序importa,b,即ab前被importaimportp,所以此時gInt

初始化,這樣就解決了C/C++中全域性變數初始化順序不一致的問題。

 

b. 預設自動初始化

Go引入了零值的概念,即每個物件被建立的時候,預設初始化為它相應型別的零

值。例如,string””,指標為nilint0等等,這樣就保證了變數在使用時,不會因為忘

記初始化而出現一些莫名其妙的問題。此外,由於零值的引入,也方便了程式碼的編寫。比

如說sync包的mutex型別,在引入零值後,就能以如下方式使用:


2


每個原始檔也可包含多個init函式。多個init之間的呼叫順序不確定。init函式本身不能被其它變數或函式引

用(呼叫或取函式地址)。——柴樹杉


3


var locker sync.Mutex

locker.Lock()

defer locker.Unlock()


而相應的C/C++程式碼,可能就要這樣寫了:

 

CRITICAL_SECTION locker

InitializeCriticalSection(&locker)

EnterCriticalSection(&locker)

LeaveCriticalSection(&locker)

DeleteCriticalSection(&locker)

 

忘記任何一步操作,都將造成死鎖(dead lock)或者其他的問題。

 

c. UTF-8

Go語言原生支援UTF-8編碼格式3。同時Go涉及到字串的各種包,也直接為

UTF-8提供了支援,比如:

str := "示例"

if str == "示例" {...}

 

d. 只支援組合不支援繼承

OOPGo中是通過組合而非繼承來實現的,因為繼承存在一些弊端,比如

不適應變化會繼承到不適用的功能。所以在編碼實踐中一般建議優先使用組合而

非繼承。在Go中則更進一步,直接去掉了繼承,只支援組合。在定義struct時,採用匿名

組合的方式,也更好地實現了C++中的實現繼承,而在定義interface時,也可以實現接

口繼承。比如:

type A struct{}

func (a A) HelloA() {

}

 

type B struct{}

func (b B) HelloB() {

}

 

type C struct {

A

B

}

 

c := &C{}

c.HelloA()

c.HelloB()

 

此時c就擁有了HelloAHelloB兩個方法,即我們很容易地實現了實現繼承

 

 

同時已經支援帶BOMUTF-8了。——柴樹杉


 

e. Go程(goroutine)與通道(channel)

Go對併發的支援,採用的是CSP模型,即在程式碼編寫的時候遵循通過通訊來共

享記憶體,而非通過共享記憶體來通訊的原則。為此,Go提供了一種名為“Go的抽象。由

Go程是一種高於執行緒的抽象,因此它使用起來也就更加輕量方便。而當多個Go程需要

通訊的時候,通道就成為了它們之間的橋樑。例如:

 

func goroutine(pass chan bool) {

fmt.Println("hello, i'm in the goroutine")

pass <- true

}

 

func main() {

pass := make(chan bool)

go goroutine(pass)

<-pass

fmt.Println("passed")

}

 

程式碼中通過關鍵字chan來宣告一個通道,在函式前加上關鍵字go來開啟一個新的

Go程。此Go程在執行完成後,會自動銷燬。而在通訊過程中,可通過<-操作符向通道中

放入或從中取出資料。

 

f. 自動垃圾回收

C#Java等語言類似,為了將程式設計師從記憶體洩漏的泥沼中解救出來,Go提供

了自動垃圾回收機制,同時不再區分物件是來自於棧(stack)還是堆(heap)

 

g. 介面(interface)

除Go程外,Go語言的最大特色就是介面的設計,Go的介面與Java的介面,

C++的虛基類是不同的,它是非侵入式的,即我們在定義一個struct的時候,不需要顯

式的說明它實現了哪一/幾個interface,而只要某個struct定義了某個interface所宣告的

所有方法,則它就隱式的實現了那個interface,即所謂的Structural-Typing(關於Duck-

TypingStructural-Typing的區別,請參考minux.ma的相關注釋)。

 

假設我要定義一個叫Shapeinterface,它有CircleSquareTriangle等實現

類。

在java等語言中,我們是先在大腦中從多個實現中抽象出一個interface,即:

在定義Shape的時候,我們會先從實現類中得出共性。比如它們都可以計算面

積,都可以被繪製出來,即Shape擁有AreaShow方法。在定義出了Shape過後,再定

CircleSquareTriangle等實現類,這些類都顯式的從Shape派生,即我們先實現了

介面再實現了實現。在實現實現的過程中,如果發現定義的介面不合適,因為實現

式地指定了它派生自哪個基類,所以此時我們需要重構:


 

public interface Shape {

public float Area();

public void Show();

}

public class Circle : implements Shape {

public float Area() { return …}

public void Show() {…}

}

// (同理SquareTriangle)

 

而在Go中,由於interface是隱式的,非侵入式的,我們就可以先實現Circle

SquareTriangle等子類。在實現這些實現類的過程中,由於知識的增加,我們可以更

好地瞭解哪些方法應該放到interface中,即在抽象的過程中完成了重構。

 

type Circle struct {}

func (c Circle) Area() float32 {}

func (c Circle) Show() {}

// (同理SquareTriangle)

type Shape interface {

Area() float32

Show()

}

 

這樣CircleSquareTriangle就實現了Shape

 

對於一個模組來說,只有模組的使用者才能最清楚地知道,它需要使用由其它被

使用模組提供的哪些方法,即interface應該由使用者定義。而被使用者在實現時,並不知

道它會被哪些模組使用,所以它只需要實現自己就好了,不需要去關心介面的粒度是多細

才合適這一類的瑣碎問題。interface是由使用方按需定義,而不用事前規劃。

 

Go的interface與Java等的interface相比優勢在於:

1.    按需定義,最小化重構的代價。

2.    先實現後抽象,搭配結構嵌入,在編寫大型軟體的時候,我們的模組可

以組織得耦合度更低。

 

h. Go命令

在Unix/Linux下為了編譯程式的方便,都可能需要編寫makefile或者各種高階的自

動構建工具(Windows也存在類似的工具,只不過被各種強大的IDE給隱藏在背後了)。

而Rob Pike等人當初發明Go的動機之一就是:“Google的大型的C++程式的編譯時間過

。所以為了達到:編譯Go程式時,作為程式設計師除開編寫程式碼外,不需要編寫任何配

置檔案或類似額外的東西。這個目標,引入了Go命令族。通過Go命令族,你可以很容

易從實現的線上repository上獲得開原始碼,編譯並執行程式碼,測試程式碼等功能。這與C/

C++的處理方式相比,前進了一大步。

 

i. 自動型別推導


Go雖然是一門編譯型語言,但是在編寫程式碼的時候,卻可以給你提供動態語言的

靈活性。在定義一個變數的時候,你可以省略型別,而讓編譯器自動為之推導型別,這樣

減少了程式設計師的輸入字數。比如:

i := 0  var i int

s := "hello world"  var s string = "hello world"

 

j. 強制編碼風格規範

在C/C++中,大家為大括號的位置採用K&R還是ANSI,是使用tab還是

whitespacewhitespace2個字元還是4個字元等瑣碎的問題而爭論不休。每個公司內

部都定義了自己的Coding Standard來強制約束。而隨著網際網路的蓬勃發展,開源專案的

越發增多,這些小問題卻影響了大家的工作效率。而有一條程式設計準則是“less is more”。為

了一致性,Go提供了專門的格式化命令go fmt,用以統一大家的編碼風格。

 

作為程式設計師,你在編寫程式碼的時候,可以按你喜歡的風格編寫。編寫完成後,

執行一下go fmt命令,就可以將你的程式碼統一成Go的標準風格。這樣你在接觸到陌生的

Go程式碼時,減少了因為編碼風格差異帶來的陌生感,強調了一致性。

 

k. 自帶單元測試及效能測試工具

C/C++雖未提供官方的單元測試與效能測試工具,但有大量第三方的相關工具。

而由於每個人接觸的,喜歡的工具可能不一樣,就造成了在交流時的負擔。有鑑於此,

Go提供了官方測試工具go test,你可以很方便地編寫出單元測試用例。比如這樣就完成

了一個單元測試的編寫:

package test

 

// example.go

func Add(a, b int) int {

return a + b

}

// example_test.go

func TestAdd(t *testing.T) {

//定義一個表格,以展示 table-driven 測試

table := []struct {

a, b, result int

}{

{1, 0, 1},

{1, 2, 3},

{-1, -2, 0},

}

 

for _, row := range table {

if row.result != Add(row.a, row.b) {

t.Fatalf("failed")

}

}

}

 

同理效能測試。

編寫完成後執行go test就可完成測試。


 


l. 雲平臺的支援

最近幾年雲端計算髮展得如火如荼,Go被稱為“21世紀的C語言,當然它也不能忽

視這一塊的需求。現在有大量的雲端計算平臺支援Go語言開發,比如由官方維護的GAE

第三方的AWS等。

 

m. 簡化的指標

這一條,可能不算優勢,在C/C++中指標運算的功能非常強大,但是帶來的危害也很

突出,所以在Go中指標取消了運算功能,只保留了引用/解引用功能 

 

n. 簡單的語法,入門快速,對於新成員很容易上手

Go本質上是一個C家族的語言,所以如果有C家族語言的經驗,很容易上手。

 

4. Go的劣勢4

 

a. 排程器的不完善

 

b. 原生庫太少/

 

c. 32bit上的-記憶體洩漏

 

關於這一點,Go的貢獻者minux.ma在Golang-China討論組上有詳細解釋:

 

目前Go使用的GC是個保守的GC,換句通俗的話說就是寧可少釋放垃圾,也不

可誤釋放還在用的記憶體;這一點反映在設計上就是會從堆疊、全域性變數開始,把所有可能

是指標的uintptr全部當作指標,遍歷,找到所有還能訪問到的記憶體中的物件,然後把剩下

的釋放。

 

那麼如何判斷一個uintptr可能是指標呢?大家知道Go的記憶體分配是參考的

tcmalloc,並做了一些改動。原先tcmalloc是使用類似頁表的樹形結構儲存已經從操作

系統中獲得的記憶體頁面,Go使用了另外一個辦法。由於Go需要維護每個記憶體字的一些

狀態(比如是否包含指標?是否有finalizer?是否是結構體的開始?還有上面提到的是

否還能訪問到的狀態),綜合在一起是每個字需要4bit資訊;於是Go就先找一片區域

(arena),以不可訪問的許可權從作業系統那裡申請過來(mmap

的prot引數是PROT_NONE),然後根據每一個uintptr對應4位申請一片RW的內

存(bitmap)與前面的arena對應;這樣已知heap上記憶體的地址想獲得對應的bitmap地址

就很簡單了,不需要像tcmalloc似的查詢,直接簡單的右移和加法就能獲得;同時呢,操


 


4


可以歸納為: 效能/生態/Bug/工具等。——柴樹杉


作系統的demand paging會自動處理還沒有使用到的bitmap

這裡大家就明白了為啥Go用了那麼大的虛擬記憶體(arena)並且知道為啥經常在

記憶體不足的時候panic說申請到的記憶體不在範圍了(因為記憶體不在bitmap所能對映的範圍

裡,當然多個bitmap是可以解決這個問題的,不過目前還不支援);回到開始的那個問

題,既然arena有個地址範圍,判斷一個uintptr是否可能是指標就是判斷是否在這個範圍

裡了。

 

這樣的問題就來了。如果我有一個int32,他的內容恰巧在那個範圍裡,更碰巧的

是如果把它當作指標,它恰巧指向一個大的資料結構,那麼GC只能認為那個資料結構還

在使用中。這樣就造成了洩露。這個問題在32/64位平臺上都是存在的。但是在32位上

問題更嚴重些,主要是32位表示的地址空間有768MB是Arena,也就是說一個均勻分佈的

uintptr是指標的概率是768/4096,這個遠比64位系統的16GiB/(2^64B)的概率大得多。

 

Go 1.1不出意外的話會使用記錄每個heap上分配的物件的型別的方式來幾乎完整

地解決這個問題;說幾乎完整是因為,堆疊上的資料還是沒有型別的,所以這裡面還是有

前面說的問題的,只不過會影響會小很多了。

 

d. 無強大IDE支援

 

e. 最大可用記憶體16G限制

 

因為今年3月28日Go才推出Go 1,所以目前Go還存在不足。ace這幾個缺陷在

2013年初的Go 1.1中會得到解決,而bd則需要等時間的積累才能完善。

 

5. Go的爭議

 

a. 錯誤處理機制

在錯誤處理上,Go不像C++Java等提供了異常機制,而是採取檢查返回值的方

案,這是目前Go最大爭議所在。

 

反對者的理由:

1.    每一步,都得做檢查繁瑣,原始。

2.    返回的error型別可以通過 _ 給忽略掉。

3.    返回的官方error介面只有一個Error()方法來輸出字串,無法用來判斷

複雜的錯誤,比我定義一個方法OpenJsonFile(name string) (jFile JFile,

err Error), 這個方法可能引法的錯誤有兩種1.檔案沒找到,2,檔案解析

錯誤,這時我希望返回的錯誤中帶有兩個資訊,1,錯誤碼,2錯誤的提

示, 錯誤碼,用於程式中的判斷,錯誤提示用於快速瞭解這個錯誤,現

在官方的error介面只有錯誤提示,而沒有錯誤碼。

 

支援的理由:

1.    在離錯誤發生最近的地方,可以用最佳的方式處理錯誤。

2.    異常在crash後丟擲的stack資訊,對於別有用心者,會洩漏關鍵資訊;而

對於終端使用者,他將看不明白究竟發生了什麼情況。而使用錯誤機制能

讓你有機會將stack資訊替換為更有意義的資訊,這樣就能提高安全性和

使用者友好性。


3.    異常也可以預設處理。

 

b. new與變數初始化

Go中,newdelete和在C++中的含義是不一樣的。delete用以刪除一個

map項,而new用以獲得一個指向某種型別物件的指標,而因為Go支援類似如下的語法

 

type T struct {

}

obj := &T{}  obj = new(T)

 

同時Go提供另一個關鍵字make用以建立內建的物件,所以&T{}這種語法與

make合起來,就基本可以替代new(但目前new(int)這類基本型別指標的建立,則無法用

&T{}的寫法),因此new看起來有點冗餘了,這與Go的簡單原則有點不一致。

 

c. For…range不能用於自定義型別

為了遍歷的方便,Go提供了for-range語法,但是這種構造只能用於built-in型別,

slicemapchan;而對於非built-in型別,即使官方包container中的相關資料結構也

不行,這降低了for-range的易用性。而目前在不支援泛型的前提下,要實現一個很友好的

for-range看起來還是很不容易的。

 

d. 不支援動態連結

目前Go只支援靜態連結(但gccgo支援動態連結,Go 1.1可能會支援部分動態鏈

接),這又是另一個引起爭論的地方。爭論雙方的論據就是動態連結/靜態連結的優、缺

點,在此不再贅述。

 

e. 無泛型

現代的大多數程式語言都提供了對泛型的支援,而在Go 1中則沒有提供對泛型

的支援。按官方團隊成員Russ Cox的說法,支援泛型要麼降低編譯效率,要麼降低程式

員效率,要麼降低執行效率。而這三個恰好與Go的快速、高效、易編寫的目標是相沖突

的。同時Go提供的interface{}可以降低對泛型的期望和需求,因此是否需要泛型也成了爭

論的焦點。

 

f. 首字母大寫表示可見性

Go中只支援包級別的可見性,即無論變數、結構、方法、還是函式等,如果以大

寫字母開頭,則它的可見性是公共的,在其它包中可加以引用;如果以小寫字母開頭,

則其可見性為其所在的包。由於Go支援UTF-8,而對於像中文這種沒有大小寫分別的字

符在需要匯出時,就會出現問題。關於這個問題,支持者的理由是:既然語言本身支援

UTF-8,那麼在變數命名上就應該是一致的;不支持者的理由是,中國人用中文命名,日

本人用日語命名而且非要用類似中文這類符號編寫的話,可以在中文符號前加一個英文

符號.比如:

 

var 不可匯出 int = 0

var E可匯出 int = 0


 


6. 替代方案

 

a. Cgo

在前邊的劣勢部分有講過,Go缺乏原生包,而現在世面上已經有大量的C實現的

高質量的第三方庫,比如OpenALOpenCLOpenGL等。為了解決這個問題,Go引入

一個叫做cgo的命令,通過遵守簡單的約定,就可以將一個C庫wrapper成一個Go包,這

也是為何在短短几年Go擁有了大量高質量包的原因。cgo相關示例在此不再展示。

 

b. B/S

因為到目前為止,Go尚未提供GUI相關的支援。同時在雲端計算時代,越來越多的

程式採用了B/S結構,而Go對Web程式設計提供了最完善的支援,所以如果程式需要提供介面

,無論是本地程式,還是伺服器程式,在當下建議使用B/S架構來替代。

 

7. 參考資料及推薦

 

a. 本文內容多來自於以下三個Google Groups:

1.    Golang-Dev(Go語言開發討論組)

2.            Golang-Nuts(Go語言使用討論組)

3.            Golang-China(Go語言使用中文討論組)

 

b. 推薦書籍

1.    《學習Go語言》

Mikespook翻譯的一本英文學習資料。

2.    Go語言程式設計》

由許式偉主筆的國內第2本關於Go語言的中文書籍。

3.    Go Web程式設計》

一本由Asta謝主筆,我參與審閱的開源中文書籍。如果在閱讀過程中有

任何疑問,請加入QQ群:259316004討論。

 

c. 推薦框架

1.    Golanger Web框架

一個由leetaifookborderj開發的開源Web框架。如果您在閱讀/使用過程

中有任何疑問,請加入QQ群:29994666討論。

 

d. 推薦站點


 



 


一個主要由我維護的Google+ Page,包含一些我認為值得分享的有關

Go語言的資料。如果哪位願意共同維護,請留言告之,Thanks

2.            Go-zh

Go語言官方網站的中文翻譯版,除pkg文件外,其它文件基本翻譯完

成。如果哪位願意共同翻譯維護,請聯絡Oling Cat


 


注:


除特別宣告外,本文件內容使用CC BY-SA 3.0 License(創作共用 署名-相同方式共享

3.0許可協議)授權,程式碼遵循BSD 3-Clause License3項條款的BSD許可協議)。


BSD 3-CLAUSE LINCENSE

 

Copyright (c) 2012, LewGun and The Contributors All rights reserved.

Redistribution and use in source and binary forms, with or without

modification, are permitted provided that the following conditions are met:

      Redistributions of source code must retain the above copyright notice,

this list of conditions and the following disclaimer.

      Redistributions in binary form must reproduce the above copyright notice,

this list of conditions and the following disclaimer in the documentation

and/or other materials provided with the distribution.

      Neither the name of the LewGun nor the names of its contributors may be

used to endorse or promote products derived from this software without

specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS

BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR

CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF

SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS

INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN

CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)

ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE

POSSIBILITY OF SUCH DAMAGE.


網址:http://www.qukuailianxueyuan.io/



欲領取造幣技術與全套虛擬機器資料

區塊鏈技術交流QQ群:756146052  備註:CSDN

尹成學院微信:備註:CSDN



相關文章