mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-01 02:28:56 +00:00
fmr code
This commit is contained in:
parent
8772a9c000
commit
ba03c527c0
@ -44,6 +44,7 @@ fmt.Println(p) // "{2, 4}"
|
||||
```
|
||||
|
||||
不過後面兩種方法有些笨拙。幸運的是,go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量,併且其方法需要一個Point指針作爲接收器,我們可以用下面這種簡短的寫法:
|
||||
|
||||
```go
|
||||
p.ScaleBy(2)
|
||||
```
|
||||
@ -61,21 +62,23 @@ pptr.Distance(q)
|
||||
(*pptr).Distance(q)
|
||||
```
|
||||
|
||||
Let’s summarize these three cases again, since they are a frequent point of confusion. In every valid method call expression, exactly one of these three statements is true.
|
||||
這里的幾個例子可能讓你有些睏惑,所以我們總結一下:在每一個合法的方法調用表達式中,也就是下面三種情況里的任意一種情況都是可以的:
|
||||
|
||||
不論是接收器的實際參數和其接收器的形式參數相同,比如兩者都是類型T或者都是類型`*T`:
|
||||
|
||||
```go
|
||||
Point{1, 2}.Distance(q) // Point
|
||||
pptr.ScaleBy(2) // *Point
|
||||
```
|
||||
|
||||
或者接收器形參是類型T,但接收器實參是類型`*T`,這種情況下編譯器會隱式地爲我們取變量的地址:
|
||||
|
||||
```go
|
||||
p.ScaleBy(2) // implicit (&p)
|
||||
```
|
||||
|
||||
或者接收器形參是類型`*T`,實參是類型T。編譯器會隱式地爲我們解引用,取到指針指向的實際變量:
|
||||
|
||||
```go
|
||||
pptr.Distance(q) // implicit (*pptr)
|
||||
```
|
||||
@ -83,6 +86,7 @@ pptr.Distance(q) // implicit (*pptr)
|
||||
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麽拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會産生一個值的拷貝。比如time.Duration的這個類型,在調用其方法時就會被全部拷貝一份,包括在作爲參數傳入函數的時候。但是如果一個方法使用指針作爲接收器,你需要避免對其進行拷貝,因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝,那麽可能會引起原始對象和拷貝對象隻是别名而已,但實際上其指向的對象是一致的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
|
||||
|
||||
譯註:作者這里説的比較繞,其實有兩點:
|
||||
|
||||
1.不管你的method的receiver是指針類型還是非指針類型,都是可以通過指針/非指針類型進行調用的,編譯器會幫你做類型轉換
|
||||
2.在聲明一個method的receiver該是指針還是非指針類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特别大,如果聲明爲非指針變量時,調用會産生一次拷貝;第二方面是如果你用指針類型作爲receiver,那麽你一定要註意,這種指針類型指向的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艹的人這里應該很快能明白。
|
||||
|
||||
@ -109,7 +113,6 @@ func (list *IntList) Sum() int {
|
||||
|
||||
下面是net/url包里Values類型定義的一部分。
|
||||
|
||||
|
||||
```go
|
||||
net/url
|
||||
package url
|
||||
|
@ -1,6 +1,7 @@
|
||||
## 6.3. 通過嵌入結構體來擴展類型
|
||||
|
||||
來看看ColoredPoint這個類型:
|
||||
|
||||
```go
|
||||
gopl.io/ch6/coloredpoint
|
||||
import "image/color"
|
||||
@ -33,6 +34,7 @@ p.ScaleBy(2)
|
||||
q.ScaleBy(2)
|
||||
fmt.Println(p.Distance(q.Point)) // "10"
|
||||
```
|
||||
|
||||
Point類的方法也被引入了ColoredPoint。用這種方式,內嵌可以使我們定義字段特别多的複雜類型,我們可以將字段先按小類型分組,然後定義小類型的方法,之後再把它們組合起來。
|
||||
|
||||
讀者如果對基於類來實現面向對象的語言比較熟悉的話,可能會傾向於將Point看作一個基類,而ColoredPoint看作其子類或者繼承類,或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型,但q併不是一個Point類,所以盡管q有着Point這個內嵌類型,我們也必鬚要顯式地選擇它。嚐試直接傳q的話你會看到下面這樣的錯誤:
|
||||
@ -72,6 +74,7 @@ fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
|
||||
```
|
||||
|
||||
一個struct類型也可能會有多個匿名字段。我們將ColoredPoint定義爲下面這樣:
|
||||
|
||||
```go
|
||||
type ColoredPoint struct {
|
||||
Point
|
||||
@ -118,6 +121,3 @@ func Lookup(key string) string {
|
||||
```
|
||||
|
||||
我們給新的變量起了一個更具表達性的名字:cache。因爲sync.Mutex字段也被嵌入到了這個struct里,其Lock和Unlock方法也就都被引入到了這個匿名結構中了,這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
|
||||
|
||||
|
||||
|
||||
|
@ -9,7 +9,8 @@ q := Point{4, 6}
|
||||
distanceFromP := p.Distance // method value
|
||||
fmt.Println(distanceFromP(q)) // "5"
|
||||
var origin Point // {0, 0}
|
||||
fmt.Println(distanceFromP(origin)) // "2.23606797749979", ;5
|
||||
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
|
||||
|
||||
scaleP := p.ScaleBy // method value
|
||||
scaleP(2) // p becomes (2, 4)
|
||||
scaleP(3) // then (6, 12)
|
||||
@ -30,6 +31,7 @@ time.AfterFunc(10 * time.Second, func() { r.Launch() })
|
||||
```go
|
||||
time.AfterFunc(10 * time.Second, r.Launch)
|
||||
```
|
||||
|
||||
譯註:省掉了上面那個例子里的匿名函數。
|
||||
|
||||
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
|
||||
@ -41,7 +43,6 @@ p := Point{1, 2}
|
||||
q := Point{4, 6}
|
||||
|
||||
distance := Point.Distance // method expression
|
||||
//譯註:這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance(),但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數,卽其需要用第一個額外參數指定接收器,後面排列Distance方法的參數。看起來本書中函數和方法的區别是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
|
||||
fmt.Println(distance(p, q)) // "5"
|
||||
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
|
||||
|
||||
@ -49,6 +50,11 @@ scale := (*Point).ScaleBy
|
||||
scale(&p, 2)
|
||||
fmt.Println(p) // "{2 4}"
|
||||
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
|
||||
|
||||
// 譯註:這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance(),
|
||||
// 但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數,
|
||||
// 卽其需要用第一個額外參數指定接收器,後面排列Distance方法的參數。
|
||||
// 看起來本書中函數和方法的區别是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
|
||||
```
|
||||
|
||||
當你根據一個變量來決定調用同一個類型的哪個函數時,方法表達式就顯得很有用了。你可以根據選擇來調用接收器各不相同的方法。下面的例子,變量op代表Point類型的addition或者subtraction方法,Path.TranslateBy方法會爲其Path數組中的每一個Point來調用對應的方法:
|
||||
|
@ -69,6 +69,7 @@ func (s *IntSet) String() string {
|
||||
這里留意一下String方法,是不是和3.5.4節中的intsToString方法很相似;bytes.Buffer在String方法里經常這麽用。當你爲一個複雜的類型定義了一個String方法時,fmt包就會特殊對待這種類型的值,這樣可以讓這些類型在打印的時候看起來更加友好,而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言,在第7章中我們會詳細介紹。
|
||||
|
||||
現在我們就可以在實戰中直接用上面定義好的IntSet了:
|
||||
|
||||
```go
|
||||
var x, y IntSet
|
||||
x.Add(1)
|
||||
@ -86,14 +87,17 @@ fmt.Println(x.Has(9), x.Has(123)) // "true false"
|
||||
```
|
||||
|
||||
這里要註意:我們聲明的String和Has兩個方法都是以指針類型*IntSet來作爲接收器的,但實際上對於這兩個類型來説,把接收器聲明爲指針類型也沒什麽必要。不過另外兩個函數就不是這樣了,因爲另外兩個函數操作的是s.words對象,如果你不把接收器聲明爲指針對象,那麽實際操作的是拷貝對象,而不是原來的那個對象。因此,因爲我們的String方法定義在IntSet指針上,所以當我們的變量是IntSet類型而不是IntSet指針時,可能會有下面這樣讓人意外的情況:
|
||||
|
||||
```go
|
||||
fmt.Println(&x) // "{1 9 42 144}"
|
||||
fmt.Println(x.String()) // "{1 9 42 144}"
|
||||
fmt.Println(x) // "{[4398046511618 0 65536]}"
|
||||
```
|
||||
|
||||
在第一個Println中,我們打印一個*IntSet的指針,這個類型的指針確實有自定義的String方法。第二Println,我們直接調用了x變量的String()方法;這種情況下編譯器會隱式地在x前插入&操作符,這樣相當遠我們還是調用的IntSet指針的String方法。在第三個Println中,因爲IntSet類型沒有String方法,所以Println方法會直接以原始的方式理解併打印。所以在這種情況下&符號是不能忘的。在我們這種場景下,你把String方法綁定到IntSet對象上,而不是IntSet指針上可能會更合適一些,不過這也需要具體問題具體分析。
|
||||
|
||||
練習6.1: 爲bit數組實現下面這些方法
|
||||
|
||||
```go
|
||||
func (*IntSet) Len() int // return the number of elements
|
||||
func (*IntSet) Remove(x int) // remove x from the set
|
||||
|
@ -5,6 +5,7 @@
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
|
||||
|
||||
這也就是前面的小節中IntSet被定義爲struct類型的原因,盡管它隻有一個字段:
|
||||
|
||||
```go
|
||||
type IntSet struct {
|
||||
words []uint64
|
||||
@ -12,6 +13,7 @@ type IntSet struct {
|
||||
```
|
||||
|
||||
當然,我們也可以把IntSet定義爲一個slice類型,盡管這樣我們就需要把代碼中所有方法里用到的s.words用*s替換掉了:
|
||||
|
||||
```go
|
||||
type IntSet []uint64
|
||||
```
|
||||
@ -35,7 +37,6 @@ type Buffer struct {
|
||||
|
||||
// Grow expands the buffer's capacity, if necessary,
|
||||
// to guarantee space for another n bytes. [...]
|
||||
|
||||
func (b *Buffer) Grow(n int) {
|
||||
if b.buf == nil {
|
||||
b.buf = b.initial[:0] // use preallocated space initially
|
||||
|
@ -6,16 +6,15 @@
|
||||
|
||||
在早些的章節中,我們已經使用了標準庫提供的一些方法,比如time.Duration這個類型的Seconds方法:
|
||||
|
||||
```
|
||||
```Go
|
||||
const day = 24 * time.Hour
|
||||
fmt.Println(day.Seconds()) // "86400"
|
||||
```
|
||||
|
||||
併且在2.5節中,我們定義了一個自己的方法,Celsius類型的String方法:
|
||||
|
||||
```go
|
||||
```Go
|
||||
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
```
|
||||
|
||||
在本章中,OOP編程的第一方面,我們會向你展示如何有效地定義和使用方法。我們會覆蓋到OOP編程的兩個關鍵點,封裝和組合。
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
在Go語言中還存在着另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露出它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合;它們隻會展示出它們自己的方法。也就是説當你有看到一個接口類型的值時,你不知道它是什麽,唯一知道的就是可以通過它的方法來做什麽。
|
||||
|
||||
在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸出和fmt.Sprintf它會把結果以字符串的形式返迴。得益於使用接口,我們不必可悲的因爲返迴結果在使用方式上的一些淺顯不同就必需把格式化這個最睏難的過程複製一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麽使用是完全不知道的。
|
||||
|
||||
``` go
|
||||
package fmt
|
||||
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
|
||||
@ -17,10 +18,12 @@ func Sprintf(format string, args ...interface{}) string {
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
|
||||
Fprintf的前綴F表示文件(File)也表明格式化輸出結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指向可以寫入字節的內存緩衝區,然而它
|
||||
併不是一個文件類型盡管它在某種意義上和文件類型相似。
|
||||
|
||||
卽使Fprintf函數中的第一個參數也不是一個文件類型。它是io.Writer類型這是一個接口類型定義如下:
|
||||
|
||||
``` go
|
||||
package io
|
||||
// Writer is the interface that wraps the basic Write method.
|
||||
@ -41,6 +44,7 @@ io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定。
|
||||
因爲fmt.Fprintf函數沒有對具體操作的值做任何假設而是僅僅通過io.Writer接口的約定來保證行爲,所以第一個參數可以安全地傳入一個任何具體類型的值隻需要滿足io.Writer接口。一個類型可以自由的使用另一個滿足相同接口的類型來進行替換被稱作可替換性(LSP里氏替換)。這是一個面向對象的特徵。
|
||||
|
||||
讓我們通過一個新的類型來進行校驗,下面\*ByteCounter類型里的Write方法,僅僅在丟失寫向它的字節前統計它們的長度。(在這個+=賦值語句中,讓len(p)的類型和\*c的類型匹配的轉換是必鬚的。)
|
||||
|
||||
```go
|
||||
// gopl.io/ch7/bytecounter
|
||||
type ByteCounter int
|
||||
@ -50,6 +54,7 @@ func (c *ByteCounter) Write(p []byte) (int, error) {
|
||||
}
|
||||
```
|
||||
因爲*ByteCounter滿足io.Writer的約定,我們可以把它傳入Fprintf函數中;Fprintf函數執行字符串格式化的過程不會去關註ByteCounter正確的纍加結果的長度。
|
||||
|
||||
```go
|
||||
var c ByteCounter
|
||||
c.Write([]byte("hello"))
|
||||
@ -59,7 +64,9 @@ var name = "Dolly"
|
||||
fmt.Fprintf(&c, "hello, %s", name)
|
||||
fmt.Println(c) // "12", = len("hello, Dolly")
|
||||
```
|
||||
|
||||
除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數向類型提供了一種控製它們值輸出的途徑。在2.5節中,我們爲Celsius類型提供了一個String方法以便於可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集合可以用傳統的符號來進行表示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣泛使用之一的接口類型fmt.Stringer:
|
||||
|
||||
```go
|
||||
package fmt
|
||||
// The String method is used to print values passed
|
||||
@ -69,12 +76,15 @@ type Stringer interface {
|
||||
String() string
|
||||
}
|
||||
```
|
||||
|
||||
我們會在7.10節解釋fmt包怎麽發現哪些值是滿足這個接口類型的。
|
||||
|
||||
練習7.1:使用來自ByteCounter的思路,實現一個針對對單詞和行數的計數器。你會發現bufio.ScanWords非常的有用。
|
||||
|
||||
練習7.2:寫一個帶有如下函數籤名的函數CountingWriter,傳入一個io.Writer接口類型,返迴一個新的Writer類型把原來的Writer封裝在里面和一個表示寫入新的Writer字節數的int64類型指針
|
||||
|
||||
```go
|
||||
func CountingWriter(w io.Writer) (io.Writer, *int64)
|
||||
```
|
||||
|
||||
練習7.3:爲在gopl.io/ch4/treesort (§4.4)的*tree類型實現一個String方法去展示tree類型的值序列。
|
||||
|
@ -2,6 +2,7 @@
|
||||
接口類型具體描述了一繫列方法的集合,一個實現了這些方法的具體類型是這個接口類型的實例。
|
||||
|
||||
io.Writer類型是用的最廣泛的接口之一,因爲它提供了所有的類型寫入bytes的抽象,包括文件類型,內存緩衝區,網絡鏈接,HTTP客戶端,壓縮工具,哈希等等。io包中定義了很多其它有用的接口類型。Reader可以代表任意可以讀取bytes的類型,Closer可以是任意可以關閉的值,例如一個文件或是網絡鏈接。(到現在你可能註意到了很多Go語言中單方法接口的命名習慣)
|
||||
|
||||
```go
|
||||
package io
|
||||
type Reader interface {
|
||||
@ -11,9 +12,10 @@ type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
在往下看,我們發現有些新的接口類型通過組合已經有的接口來定義。下面是兩個例子:
|
||||
```go
|
||||
|
||||
在往下看,我們發現有些新的接口類型通過組合已經有的接口來定義。下面是兩個例子:
|
||||
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
@ -25,24 +27,29 @@ type ReadWriteCloser interface {
|
||||
}
|
||||
```
|
||||
上面用到的語法和結構內嵌相似,我們可以用這種方式以一個簡寫命名另一個接口,而不用聲明它所有的方法。這種方式本稱爲接口內嵌。盡管略失簡潔,我們可以像下面這樣,不使用內嵌來聲明io.Writer接口。
|
||||
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
```
|
||||
|
||||
或者甚至使用種混合的風格:
|
||||
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
上面3種定義方式都是一樣的效果。方法的順序變化也沒有影響,唯一重要的就是這個集合里面的方法。
|
||||
|
||||
練習7.4:strings.NewReader函數通過讀取一個string參數返迴一個滿足io.Reader接口類型的值(和其它值)。實現一個簡單版本的NewReader,併用它來構造一個接收字符串輸入的HTML解析器(§5.2)
|
||||
|
||||
練習7.5:io包里面的LimitReader函數接收一個io.Reader接口類型的r和字節數n,併且返迴另一個從r中讀取字節但是當讀完n個字節後就表示讀到文件結束的Reader。實現這個LimitReader函數:
|
||||
|
||||
```go
|
||||
func LimitReader(r io.Reader, n int64) io.Reader
|
||||
```
|
||||
|
@ -41,9 +41,11 @@ func fib(x int) int {
|
||||
```
|
||||
|
||||
動畵顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果:
|
||||
|
||||
```
|
||||
Fibonacci(45) = 1134903170
|
||||
```
|
||||
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退出。除了從主函數退出或者直接退出程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,併讓其自己結束執行。
|
||||
|
||||
註意這里的兩個獨立的單元是如何進行組合的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會併發地執行。
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
網絡編程是併發大顯身手的一個領域,由於服務器是最典型的需要同時處理很多連接的程序,這些連接一般來自遠彼此獨立的客戶端。在本小節中,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,無論兩者間通信是使用TCP,UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包里的方法,也算是net包的一部分。
|
||||
|
||||
我們的第一個例子是一個順序執行的時鐘服務器,它會每隔一秒鐘將當前時間寫到客戶端:
|
||||
|
||||
```go
|
||||
gopl.io/ch8/clock1
|
||||
// Clock1 is a TCP server that periodically writes the time.
|
||||
@ -92,6 +93,7 @@ func mustCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸出中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這里可以開兩個終端窗口,下面左邊的是其中的一個的輸出,右邊的是另一個的輸出:
|
||||
|
||||
```
|
||||
@ -124,6 +126,7 @@ for {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
現在多個客戶端可以同時接收到時間了:
|
||||
|
||||
```
|
||||
|
@ -8,6 +8,7 @@ func handleConn(c net.Conn) {
|
||||
c.Close()
|
||||
}
|
||||
```
|
||||
|
||||
一個更有意思的echo服務應該模擬一個實際的echo的“迴響”,併且一開始要用大寫HELLO來表示“聲音很大”,之後經過一小段延遲返迴一個有所緩和的Hello,然後一個全小寫字母的hello表示聲音漸漸變小直至消失,像下面這個版本的handleConn(譯註:笑看作者腦洞大開):
|
||||
|
||||
```go
|
||||
|
@ -56,6 +56,7 @@ https://golang.org/blog/
|
||||
too many open files
|
||||
...
|
||||
```
|
||||
|
||||
最初的錯誤信息是一個讓人莫名的DNS査找失敗,卽使這個域名是完全可靠的。而隨後的錯誤信息揭示了原因:這個程序一次性創建了太多網絡連接,超過了每一個進程的打開文件數限製,旣而導致了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
|
||||
這個程序實在是太他媽併行了。無窮無盡地併行化併不是什麽好事情,因爲不管怎麽説,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製併發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來説,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這里的n是fd的limit-20,一般情況下。這個一個夜店里限製客人數目是一個道理,隻有當有客人離開時,才會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
|
@ -126,6 +126,7 @@ default:
|
||||
// do nothing
|
||||
}
|
||||
```
|
||||
|
||||
channel的零值是nil。也許會讓你覺得比較奇怪,nil的channel有時候也是有一些用處的。因爲對一個nil的channel發送和接收操作會永遠阻塞,在select語句中操作nil的channel永遠都不會被select到。
|
||||
|
||||
這使得我們可以用nil來激活或者禁用case,來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
|
||||
|
@ -32,7 +32,6 @@ ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型
|
||||
|
||||
下面的主函數,用了兩個goroutine。後台的goroutine調用walkDir來遍歷命令行給出的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,併輸出其和。
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@ -75,7 +74,9 @@ func printDiskUsage(nfiles, nbytes int64) {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
這個程序會在打印其結果之前卡住很長時間。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/du1
|
||||
$ ./du1 $HOME /usr /bin /etc
|
||||
@ -176,11 +177,8 @@ func dirents(dir string) []os.FileInfo {
|
||||
sema <- struct{}{} // acquire token
|
||||
defer func() { <-sema }() // release token
|
||||
// ...
|
||||
|
||||
```
|
||||
|
||||
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
|
||||
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算併顯示出來。
|
||||
|
||||
|
||||
|
@ -63,7 +63,6 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在walkDir函數的循環中我們對取消狀態進行輪詢可以帶來明顯的益處,可以避免在取消事件發生時還去創建goroutine。取消本身是有一些代價的;想要快速的響應需要對程序邏輯進行侵入式的脩改。確保在取消發生之後不要有代價太大的操作可能會需要脩改你代碼里的很多地方,但是在一些重要的地方去檢査取消事件也確實能帶來很大的好處。
|
||||
|
@ -24,7 +24,6 @@ func main() {
|
||||
|
||||
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發出channel的"資格"信息。
|
||||
|
||||
|
||||
```go
|
||||
type client chan<- string // an outgoing message channel
|
||||
|
||||
@ -90,6 +89,7 @@ func clientWriter(conn net.Conn, ch <-chan string) {
|
||||
另外,handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發出消息channel中發送的廣播消息,併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
|
||||
|
||||
下面演示的是當服務器有兩個活動的客戶端連接,併且在兩個窗口中運行的情況,使用netcat來聊天:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/chat
|
||||
$ go build gopl.io/ch8/netcat3
|
||||
@ -110,7 +110,6 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
|
||||
127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome.
|
||||
^C
|
||||
127.0.0.1:64211 has left”
|
||||
|
||||
```
|
||||
|
||||
當與n個客戶端保持聊天session時,這個程序會有2n+2個併發的goroutine,然而這個程序卻併不需要顯式的鎖(§9.2)。clients這個map被限製在了一個獨立的goroutine中,broadcaster,所以它不能被併發地訪問。多個goroutine共享的變量隻有這些channel和net.Conn的實例,兩個東西都是併發安全的。我們會在下一章中更多地解決約束,併發安全以及goroutine中共享變量的含義。
|
||||
|
Loading…
Reference in New Issue
Block a user