天天看點

Go程式設計語言評估報告

Go程式設計語言評估報告

1. Go簡介

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

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

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

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

件。

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

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

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

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

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

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

Python和JavaScript之類的動态類型語言。

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

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

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

而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的支

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

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所說:

“我們——Ken,Robert和我自己曾經是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又按序import了a,b,即a在b前被import。a先import了p,是以此時gInt被

初始化,這樣就解決了C/C++中全局變量初始化順序不一緻的問題。

b. 預設自動初始化

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

值。例如,string為””,指針為nil,int為0等等,這樣就保證了變量在使用時,不會因為忘

記初始化而出現一些莫名其妙的問題。此外,由于零值的引入,也友善了代碼的編寫。比

如說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. 隻支援組合不支援繼承

OOP在Go中是通過組合而非繼承來實作的,因為“繼承”存在一些弊端,比如

:“不适應變化”,“會繼承到不适用的功能”。是以在編碼實踐中一般建議優先使用組合而

非繼承。在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就擁有了HelloA、HelloB兩個方法,即我們很容易地實作了“實作繼承”。

同時已經支援帶BOM的UTF-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-

Typing與Structural-Typing的差別,請參考minux.ma的相關注釋)。

假設我要定義一個叫Shape的interface,它有Circle、Square、Triangle等實作

類。

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

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

積,都可以被繪制出來,即Shape擁有Area與Show方法。在定義出了Shape過後,再定

義Circle、Square、Triangle等實作類,這些類都顯式的從Shape派生,即我們先實作了

接口再實作了“實作”。在實作“實作”的過程中,如果發現定義的接口不合适,因為“實作”顯

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

public interface Shape {

public float Area();

public void Show();

}

public class Circle : implements Shape {

public float Area() { return …}

public void Show() {…}

}

// (同理Square和Triangle)

而在Go中,由于interface是隐式的,非侵入式的,我們就可以先實作Circle、

Square、Triangle等子類。在實作這些“實作類”的過程中,由于知識的增加,我們可以更

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

type Circle struct {}

func (c Circle) Area() float32 {}

func (c Circle) Show() {}

// (同理Square和Triangle)

type Shape interface {

Area() float32

Show()

}

這樣Circle、Square和Triangle就實作了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還是

whitespace,whitespace是2個字元還是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還存在不足。a、c、e這幾個缺陷在

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

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中,new與delete和在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類型,

如slice、map和chan;而對于非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實作的

高品質的第三方庫,比如OpenAL、OpenCL、OpenGL等。為了解決這個問題,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架構​​

一個由leetaifook與borderj開發的開源Web架構。如果您在閱讀/使用過程

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

d. 推薦站點

1.

​​Golang-Chinese​​

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

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

2.            Go-zh

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

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

注:

除特别聲明外,本文檔内容使用​​CC​​​ BY-SA ​​3.0​​​ ​​License​​(創作共用 署名-相同方式共享

3.0許可協定)授權,代碼遵循​​BSD​​​ ​​3-Clause​​​ ​​License​​(3項條款的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)

繼續閱讀