《The Go Programming Language》 知識點記載,學習筆記、章節練習與個人思考,前言 · Go語言圣經 (itsfun.top)
標題后標記了小丑符號的表示還沒寫,
Hello, World
Go語言不需要在陳述句或者宣告的末尾添加分號,除非一行上有多條陳述句,實際上,編譯器會主動把特定符號后的換行符轉換為分號,因此換行符添加的位置會影響Go代碼的正確決議(譯注:比如行末是識別符號、整數、浮點數、虛數、字符或字串文字、關鍵字break、continue、fallthrough或return中的一個、運算子和分隔符++、--、)、]或}中的一個),舉個例子,函式的左括號{必須和func函式宣告在同一行上,且位于末尾,不能獨占一行,而在運算式x + y中,可在+后換行,不能在+前換行(譯注:以+結尾的話不會被插入分號分隔符,但是以x結尾的話則會被分號分隔符,從而導致編譯錯誤),
命令列引數
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
練習 1.3
如前文所述,每次回圈迭代字串s的內容都會更新,+=連接原字串、空格和下個引數,產生新字串,并把它賦值給s,s原來的內容已經不再使用,將在適當時機對它進行垃圾回收,
如果連接涉及的資料量很大,這種方式代價高昂,所以我們使用strings包的Join函式
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
練習 1.3: 做實驗測量潛在低效的版本和使用了strings.Join的版本的運行時間差異,(1.6節講解了部分time包,11.4節展示了如何寫標準測驗程式,以得到系統性的性能評測,)這里使用 time 包計算運行時間,
// ^ 練習 1.3: 做實驗測量潛在低效的版本和使用了strings.Join的版本的運行時間差異,
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
func main() {
args := make([]string, 10000)
for i := 0; i < 10000; i++ {
args = append(args, strconv.Itoa(i+1))
}
var s string
start := time.Now()
for i := range args {
s += args[i]
s += " "
}
// fmt.Println(s) // ^ 因為切片數量過大先不列印就只做合并操作
t := time.Since(start)
fmt.Println("elapsed time", t)
/* -------------------------------------------------------------------------- */
start = time.Now()
s = strings.Join(args, " ")
fmt.Println(s)
t = time.Since(start)
fmt.Println("elapsed time", t)
}
elapsed time 230.8618ms
elapsed time 0s
可以看到前者耗費了0.23秒,后者幾乎沒有耗費時間
為了深入 \(strings.Join\) 方法,我們需要先深入 \(strings.Builder\) 型別
strings.Builder 原始碼決議
存在意義
使用 \(strings.Builder\),避免頻繁創建字串物件,進而提高性能
\(Source\ file\) https://go.dev/src/strings/builder.go
在上面的高耗時案例中,與許多支持 \(string\) 型別的語言一樣,\(golang\) 中的 \(string\) 型別也是只讀且不可變的( \(string\) 型別筆記 Go xmas2020 學習筆記 04、Strings - 小能日記 - 博客園 ),因此,這種拼接字串的方式會導致大量的string創建、銷毀和記憶體分配,如果你拼接的字串比較多的話,這顯然不是一個正確的姿勢,
在 \(strings.Builder\) 出來以前,我們是用 \(bytes.Buffer\) 來進行優化的,
func main() {
ss := []string{
"A",
"B",
"C",
}
var b bytes.Buffer
for _, s := range ss {
fmt.Fprint(&b, s)
}
print(b.String())
}
這里使用 var b bytes.Buffer 存放最終拼接好的字串,一定程度上避免上面 \(str\) 每進行一次拼接操作就重新申請新的記憶體空間存放中間字串的問題,
但這里依然有一個小問題: \(b.String()\) 會有一次 \([\ ]byte -> string\) 型別轉換,而這個操作是會進行一次記憶體分配和內容拷貝的,
原理決議
Golang 官方將 \(strings.Builder\) 作為一個\(feature\) 引入,
- 與 \(byte.Buffer\) 思路類似,既然 \(string\) 在構建程序中會不斷的被銷毀重建,那么就盡量避免這個問題,底層使用一個 \(buf\ [\ ]byte\) 來存放字串的內容,
- 對于寫操作,就是簡單的將 \(byte\) 寫入到 \(buf\) 即可,
- 為了解決 \(bytes.Buffer.String()\) 存在的 \([\ ]byte -> string\) 型別轉換和記憶體拷貝問題,這里使用了一個\(unsafe.Pointer\) 的記憶體指標轉換操作,實作了直接將 \(buf\ [\ ]byte\)轉換為 \(string\) 型別,同時避免了記憶體充分配的問題,
- 如果我們自己來實作 \(strings.Builder\) , 大部分情況下我們完成前3步就覺得大功告成了,但是標準庫做得要更近一步,我們知道 Golang 的堆疊在大部分情況下是不需要開發者關注的,如果能夠在堆疊上完成的作業逃逸到了堆上,性能就大打折扣了,因此,\(copyCheck\) 加入了一行比較 \(hack\) 的代碼來避免 \(buf\) 逃逸到堆上,Go 堆疊、堆的知識可看 GopherCon SG 2019 "Understanding Allocations" 學習筆記 - 小能日記 - 博客園
常用方法
- String方法回傳Builder構建的資料
- Len方法回傳位元組陣列占據的位元組數,1個漢字三個位元組
- Cap方法回傳位元組陣列分配的記憶體空間大小
- Reset方法將Builder重置為初始狀態
- Write方法將位元組陣列加添加到buf陣列后面
- WriteByte將位元組c添加到buf陣列后邊
- WriteRune將rune字符添加到buf陣列后面
- WriteString將字串添加到buf陣列后面
寫入方法
\(bytes.Buffer\) 也支持這四個寫入方法,
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)

strings.Builderorganizes the content based on the internal slice to organize. When you call write-methods, they append new bytes to inner-slice. If the slice’s capacity is reached, Go will allocate a new slice with different memory space and copy old slice to a new one. It will take resource to do when the slice is large or it may create the memory issueThe
runeand a character ofstringcan be more than 1 bytes when youWriteRune()orWriteString()
我們可以預定義切片的容量來避免重新分配,
擴容方法
追加內容也有講究,因為底層是 \(slice\),追加資料時有可能引起 \(slice\) 擴容,一般的優化方案是為 \(slice\) 初始化合理的空間,避免多次擴容復制,\(Builder\) 也提供了預分配記憶體的方法,如 \(Grow\) 方法,
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
注意擴容的容量和 \(slice\) 直接擴容兩倍的方式略有不同,它是2*cap(b.buf)+n,之前容量的兩倍加 \(n\),
- 如果容量是10,長度是5,呼叫 \(Grow(3)\)結果是什么?當前容量足夠使用,沒有任何操作;
- 如果容量是10,長度是5,呼叫 \(Grow(7)\)結果是什么?剩余空間是5,不滿足7個擴容空間,底層需要擴容,擴容的時候按照之前容量的兩倍再加 \(n\) 的新容量擴容,結果是 \(2*10+7=27\),
String() 方法
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
先獲取 \([\ ]byte\) 地址,然后轉成字串指標,然后再取地址,
從 ptype 輸出的結構來看,string 可看做 [2]uintptr,而 [ ]byte 則是 [3]uintptr,這便于我們撰寫代碼,無需額外定義結構型別,如此,str2bytes 只需構建 [3]uintptr{ptr, len, len},而 bytes2str 更簡單,直接轉換指標型別,忽略掉 cap 即可,
禁止復制
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
\(Builder\) 的底層資料,它還有個欄位 \(addr\) ,是一個指向 \(Builder\) 的指標,默認情況是它會指向自己,
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
\(copyCheck\) 用來保證復制后不允許修改的邏輯,仔細看下原始碼,如果 \(addr\) 是空,也就是沒有資料的時候是可以被復制后修改的,一旦那邊有資料了,就不能這么搞了,在 \(Grow\)、\(Write\)、\(WriteByte\)、\(WriteString\)、\(WriteRune\) 這五個函式里都有這個檢查邏輯,
var b1 strings.Builder
b2 := b1
b2.WriteString("DEF")
b1.WriteString("ABC")
// b1 = ABC, b2 = DEF
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF")
代碼將會報錯 illegal use of non-zero Builder copied by value
下面的意思是拷貝過來的Builder進行添加修改,會造成其他Builder的修改,
When we copy the Builder, we clone the pointer of the slice but they still point to the old array. The problem will be occurs when you try to
Writesomething to copied Builder or source Builder, the other’s content will be affects. That’s reason whystrings.Builderprevent copy actions.

我們可以使用 \(Reset\) 方法對 \(addr、buf\) 置空,下面拷貝了使用 \(Reset\) 后不會報錯,
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.Len()) // 3
fmt.Println(b2.String()) // ABC
b2.Reset()
b2.WriteString("DEF")
fmt.Println(b2.String()) // DEF
執行緒不安全
func main() {
var b strings.Builder
var n int32
var wait sync.WaitGroup
for i := 0; i < 1000; i++ {
wait.Add(1)
go func() {
atomic.AddInt32(&n, 1)
b.WriteString("1")
wait.Done()
}()
}
wait.Wait()
fmt.Println(len(b.String()), n)
}
905 1000
結果是 \(905\ 1000\),并不都是 \(1000\) ,如果想保證執行緒安全,需要在 \(WriteString\) 的時候加鎖,
io.Writer 介面
\(strings.Builder\) 實作了 \(io.Writer\) 介面,可以使用在很多例子中
- io.Copy(dst Writer, src Reader) (written int64, err error)
- bufio.NewWriter(w io.Writer) *Writer
- fmt.Fprint(w io.Writer, a …interface{}) (n int, err error)
- func (r *http.Request) Write(w io.Writer) error
- and other libraries that uses \(io.Writer\)
代碼
func main() {
var b strings.Builder
fmt.Printf("%v", b)
fmt.Println(b.Len(), b.Cap())
for i := 3; i >= 1; i-- {
fmt.Fprintf(&b, "%d#", i)
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
}
b.WriteString("Hello")
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
fmt.Println(b.String())
// b.Grow(5) // ^ 擴容
b.Grow(88) // ^ 擴容
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
fmt.Println(unsafeEqual("Hello", []byte{72, 101, 108, 108, 111}))
}
func unsafeEqual(a string, b []byte) bool {
bbp := *(*string)(unsafe.Pointer(&b))
return a == bbp
}
{<nil> []}0 0
{0xc0001223a0 [51 35]}
2 8
{0xc0001223a0 [51 35 50 35]}
4 8
{0xc0001223a0 [51 35 50 35 49 35]}
6 8
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 16
3#2#1#Hello
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 120
true
strings.Join 原始碼決議
實作原理
// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
n += len(elems[i])
}
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}
前面計算出整個字串需要的長度 \(n\),然后創建 \(strings.Builder\) 并通過 \(Grow\) 方法直接擴容,大小為 \(0*2+n\) 為 \(n\) ,然后通過 \(WriteString\) 方法寫入,最后呼叫 \(String\) 方法回傳字串,只構造了一次字串物件,
查找重復的行
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}
bufio.Scanner
程式使用短變數宣告創建bufio.Scanner型別的變數input,
input := bufio.NewScanner(os.Stdin)
該變數從程式的標準輸入中讀取內容,每次呼叫input.Scan(),即讀入下一行,并移除行末的換行符;讀取的內容可以呼叫input.Text()得到,Scan函式在讀到一行時回傳true,不再有輸入時回傳false,
map 傳遞
map是一個由make函式創建的資料結構的參考,map作為引數傳遞給某函式時,該函式接收這個參考的一份拷貝(copy,或譯為副本),被呼叫函式對map底層資料結構的任何修改,呼叫者函式都可以通過持有的map參考看到,在我們的例子中,countLines函式向counts插入的值,也會被main函式看到,(譯注:類似于C++里的參考傳遞,實際上指標是另一個指標了(函式里的區域變數),但內部存的值指向同一塊記憶體)
為了列印結果,我們使用了基于range的回圈,并在counts這個map上迭代,跟之前類似,每次迭代得到兩個結果,鍵和其在map中對應的值,map的迭代順序并不確定,從實踐來看,該順序隨機,每次運行都會變化,這種設計是有意為之的,因為能防止程式依賴特定遍歷順序,而這是無法保證的,
ioutil.ReadFile
ReadFile函式一口氣把全部輸入資料讀到記憶體中,回傳一個位元組切片(byte slice),必須把它轉換為string,才能用strings.Split分割,
練習 1.4
練習 1.4: 修改dup2,出現重復的行時列印檔案名稱,
func main() {
counts := make(map[string]int)
counts_fileid := make(map[string]string)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts, counts_fileid)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts, counts_fileid)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\t%v\n", n, line, counts_fileid[line])
}
}
}
func countLines(f *os.File, counts map[string]int, counts_fileid map[string]string) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
counts_fileid[input.Text()] = f.Name()
}
}
hello Server
world
hello Server
123456
cat
123456
hello Server
hello
world
123456
3 hello Server data.txt
2 world data2.txt
3 123456 data2.txt
為什么 Map 中 Key 是無序的 ??
且聽下回分解,會更新一篇獨立的文章
GIF 影片
練習 1.6
練習 1.6: 修改Lissajous程式,修改其調色板來生成更豐富的顏色,然后修改SetColorIndex的第三個引數,看看顯示結果吧,
package main
import (
"image"
"image/color"
"image/gif"
"io"
"math"
"math/rand"
"os"
"time"
)
var palette = []color.Color{
color.White,
color.RGBA{0, 255, 0, 255},
color.RGBA{255, 0, 0, 255},
color.RGBA{0, 0, 255, 255},
color.RGBA{100, 255, 0, 255},
color.RGBA{0, 255, 100, 255},
}
const (
whiteIndex = 0 // first color in palette
blackIndex = 1 // next color in palette
)
func main() {
// The sequence of images is deterministic unless we seed
// the pseudo-random number generator using the current time.
// Thanks to Randall McPherson for pointing out the omission.
rand.Seed(time.Now().UTC().UnixNano())
lissajous(os.Stdout)
}
func lissajous(out io.Writer) {
const (
cycles = 2 // number of complete x oscillator revolutions
res = 0.001 // angular resolution
size = 100 // image canvas covers [-size..+size]
nframes = 64 // number of animation frames
delay = 8 // delay between frames in 10ms units
)
freq := rand.Float64() * 3.0 // relative frequency of y oscillator
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // phase difference
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
uint8(x*size)%5+1)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}
這里修改了原代碼中的 \(palette\) 、\(img.SetColorIndex\) 方法,需要注意的是,如果使用 \(powershell\) 重定向管道 go run . > out.gif 生成的 \(gif\) 圖片將會出錯無法打開,具體原因是 \(powershell\) 的標準輸出流如果進行管道重定向會進行轉換,
The GIF file (the gif data) is a binary format, not textual. Attempting to write it to the standard output and redirecting that to a file may suffer transformations. For example, the Windows PowerShell most likely converts some control characters (like
"\n"to"\r\n"), so the resulting binary will not be identical to whatgif.EncodeAll()writes to the standard output. Apparentlycmd.exedoes not do such transformations.
解決方法可以用其他命令列工具比如 \(cmd\),或者在代碼中明確使用 \(os.File\) 創建檔案等形式,
獲取URL
練習 1.7-1.9
練習 1.7: 函式呼叫io.Copy(dst, src)會從src中讀取內容,并將讀到的結果寫入到dst中,使用這個函式替代掉例子中的ioutil.ReadAll來拷貝回應結構體到os.Stdout,避免申請一個緩沖區(例子中的b)來存盤,記得處理io.Copy回傳結果中的錯誤,
練習 1.8: 修改fetch這個范例,如果輸入的url引數沒有 http:// 前綴的話,為這個url加上該前綴,你可能會用到strings.HasPrefix這個函式,
練習 1.9: 修改fetch列印出HTTP協議的狀態碼,可以從resp.Status變數得到該狀態碼,
// Fetch prints the content found at a URL.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
for _, url := range os.Args[1:] {
if !strings.HasPrefix(url, "https://") {
url = "https://" + url
}
resp, err := http.Get(url)
fmt.Println("Status", resp.Status)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
// b, err := ioutil.ReadAll(resp.Body)
b, err := io.Copy(os.Stdout, resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Println(b)
}
}
(base) PS L:\IT\Go\Codes\Work\Study\Gopl\ch1\1.789> go run . https://wolflong.com
Status 200 OK
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
location.href="https://www.cnblogs.com/linxiaoxu/";
</script>
</head>
<body>
</body>
</html>
206
并發獲取多個URL ??
練習 1.10: 找一個資料量比較大的網站,用本小節中的程式調研網站的快取策略,對每個URL執行兩遍請求,查看兩次時間是否有較大的差別,并且每次獲取到的回應內容是否一致,修改本節中的程式,將回應結果輸出到檔案,以便于進行對比,
練習 1.11: 在fetchall中嘗試使用長一些的引數串列,比如使用在alexa.com的上百萬網站里排名靠前的,如果一個網站沒有回應,程式將采取怎樣的行為?(Section8.9 描述了在這種情況下的應對機制),
Web 服務
練習 1.12
練習 1.12: 修改Lissajour服務,從URL讀取變數,比如你可以訪問 http://localhost:8000/?cycles=20 這個URL,這樣訪問可以將程式里的cycles默認的5修改為20,字串轉換為數字可以呼叫strconv.Atoi函式,你可以在godoc里查看strconv.Atoi的詳細說明,
將 Lissajous 函式內的區域常量移動到函式外成為包變數,將 const 修改為 var 后出錯,

錯誤原因是常量宣告時型別并沒有確定,在給定計算的時候才確定了常量的型別,而將 const 改為 var 后,型別在宣告的時候就被確定了,不同型別是不能做基本運算的,要進行型別轉換,如下面錯誤給 size 外側包一層 float64 即可,
invalid operation: size + int(x * size + 0.5) (mismatched types float64 and int)
package main
import (
"fmt"
"image"
"image/color"
"image/gif"
"io"
"log"
"math"
"math/rand"
"net/http"
"os"
"strconv"
"time"
)
func main() {
var err error
rand.Seed(time.Now().UTC().UnixNano())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Printf("Form[%q] = %q\n", k, v)
switch k {
case "cycles":
cycles, err = strconv.Atoi(v[0])
case "size":
size, err = strconv.Atoi(v[0])
}
if err != nil {
println(err)
os.Exit(1)
}
}
Lissajous(w)
}) // each request calls handler
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
var palette = []color.Color{
color.White,
color.RGBA{0, 255, 0, 255},
color.RGBA{255, 0, 0, 255},
color.RGBA{0, 0, 255, 255},
color.RGBA{100, 255, 0, 255},
color.RGBA{0, 255, 100, 255},
}
const (
whiteIndex = 0 // first color in palette
blackIndex = 1 // next color in palette
)
var (
cycles = 2 // number of complete x oscillator revolutions
res = 0.001 // angular resolution
size = 100 // image canvas covers [-size..+size]
nframes = 64 // number of animation frames
delay = 8 // delay between frames in 10ms units
)
func Lissajous(out io.Writer) {
freq := rand.Float64() * 3.0 // relative frequency of y oscillator
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // phase difference
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < float64(cycles*2)*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*float64(size)+0.5), size+int(y*float64(size)+0.5),
uint8(x*float64(size))%5+1)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}
運行程式,訪問本地服務 http://localhost:8000/?cycles=10&size=500

------ 課外補充 ------
靜態編譯、動態編譯
靜態庫
靜態庫是指在我們的應用中,有一些公共代碼是需要反復使用,就把這些代碼編譯為“庫”檔案;在鏈接步驟中,連接器將從庫檔案取得所需的代碼,復制到生成的可執行檔案中的這種庫,
程式編譯一般需經預處理、編譯、匯編和鏈接幾個步驟,靜態庫特點是可執行檔案中包含了庫代碼的一份完整拷貝;缺點就是被多次使用就會有多份冗余拷貝,
動態庫
元件(Dynamic Link Library 或者 Dynamic-link Library,縮寫為 DLL),是微軟公司在微軟Windows作業系統中,實作共享函式庫概念的一種方式,
元件檔案,是一種不可執行的二進制程式檔案,它允許程式共享執行特殊任務所必需的代碼和其他資源,Windows 提供的DLL檔案中包含了允許基于 Windows 的程式在 Windows 環境下操作的許多函式和資源,一般被存放在電腦的"C:\Windows\System32" 目錄下,
DLL的最初目的是節約應用程式所需的磁盤和記憶體空間,在一個傳統的非共享庫中,一部分代碼簡單地附加到呼叫的程式上,如果兩個程式呼叫同一個子程式,就會出現兩份那段代碼,相反,許多應用共享的代碼能夠切分到一個DLL中,在硬碟上存為一個檔案,在記憶體中使用一個實體(instance),DLL的廣泛應用使得早期的視窗能夠在緊張的記憶體條件下運行,
DLL提供了如模塊化這樣的共享庫的普通好處,模塊化允許僅僅更改幾個應用程式共享使用的一個DLL中的代碼和資料而不需要更改應用程式自身,這種模塊化的基本形式允許如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身這樣大的應用程式使用較為緊湊的補丁和服務包,
盡管有這么多的優點,使用DLL也有一個缺點:DLL地獄,也就是幾個應用程式在使用同一個共享DLL庫發生版本沖突,這樣的沖突可以通過將不同版本的問題DLL放到應用程式所在的檔案夾而不是放到系統檔案夾來解決;但是,這樣將抵消共享DLL節約的空間,目前,Microsoft .NET將解決DLL hell問題當作自己的目標,它允許同一個共享庫的不同版本并列共存,由于現代的計算機有足夠的磁盤空間和記憶體,這也可以作為一個合理的實作方法,
靜態庫與動態庫
靜態庫和動態庫是兩種共享程式代碼的方式,它們的區別是:靜態庫在程式的鏈接階段被復制到了程式中,和程式運行的時候沒有關系;動態庫在鏈接階段沒有被復制到程式中,而是程式在運行時由系統動態加載到記憶體中供程式呼叫,使用動態庫的優點是系統只需載入一次動態庫,不同的程式可以得到記憶體中相同的動態庫的副本,因此節省了很多記憶體,
靜態編譯
靜態編譯,就是編譯器在編譯可執行檔案的時候,將可執行檔案需要呼叫的對應靜態庫(.a或.lib)中的部分提取出來,鏈接到可執行檔案中去,使可執行檔案在運行的時候不依賴于元件,
靜態編譯、動態編譯區別
動態編譯的可執行檔案需要附帶一個的元件,在執行時,需要呼叫其對應元件中的命令,所以其優點一方面是縮小了執行檔案本身的體積,另一方面是加快了編譯速度,節省了系統資源,缺點一是哪怕是很簡單的程式,只用到了鏈接庫中的一兩條命令,也需要附帶一個相對龐大的鏈接庫;二是如果其他計算機上沒有安裝對應的運行庫,則用動態編譯的可執行檔案就不能運行,
靜態編譯就是編譯器在編譯可執行檔案的時候,將可執行檔案需要呼叫的對應靜態庫(.a或.lib)中的部分提取出來,鏈接到可執行檔案中去,使可執行檔案在運行的時候不依賴于元件,所以其優缺點與動態編譯的可執行檔案正好互補,
Go 語言中采用靜態編譯,所以不用擔心在系統庫更新的時候沖突
查漏補缺
型別別名
語言自身的型別別名包括:\(byte\ rune\)
還可以使用 type 關鍵字來定義型別別名
type Alias = Origin
需要注意別名和原型別之間的等號,源代碼舉例
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
別名和原型別是等同的,不需要顯式轉換
Channel
無快取的通道要求goroutine和接收goroutine同時準備好,才能完成發送和接收操作,如果兩個goroutine沒有同時準備好,通道會導致先執行發送或接收操作的gotourine阻塞等待,

-
在第一步,兩個goroutine都到達通道,但哪個都沒有開始執行發送或者接收,
-
在第二步,左側的goroutine將它的手伸進了通道,這模擬了向通道發送資料的行為,這時,這個goroutine會在通道中被鎖住,直到交換完成,
-
在第三步,右側的goroutine將它的手放入通道,這模擬了從通道里接收資料,這個goroutine一樣也會在通道中被鎖住,直到交換完成,
-
在第四步和第五步,進行交換,并最終,在第六步,兩個goroutine都將他們的手從通道里拿出來,這模擬了被鎖住的goroutine得到釋放,兩個goroutine現在都可以去做別的事情了,
第二條和第三條看不懂可跳過,第三條原文是無快取,我將它改為了有快取,因為我覺得沒有快取Channel的len、cap應該是 0,后面陳述句就不成立了,第二條相當于在做一個判空操作,
- 有快取 Channel:
ch <- v發生在v <- ch之前 - 有快取 Channel:
close(ch)發生在v <- ch && v == isZero(v)之前 - 有快取 Channel: 如果
len(ch) == C,則從 Channel 中收到第 k 個值發生在 k+C 個值得發送完成之前 - 無快取 Channel:
v <- ch發生在ch <- v之前
直觀上我們很好理解他們之間的差異: 對于有快取 Channel 而言,內部有一個緩沖佇列,資料會優先進入緩沖佇列,而后才被消費, 即向通道發送資料 ch <- v 發生在從通道接受資料 v <- ch 之前; 對于無快取 Channel 而言,內部沒有緩沖佇列,即向通道發送資料 ch <- v 一旦出現, 通道接受資料 v <- ch 會立即執行, 因此從通道接受資料 v <- ch 發生在向通道發送資料 ch <- v 之前, 我們隨后再根據實際實作來深入理解這一記憶體模型,
Go 語言還內建了 close() 函式來關閉一個 Channel
但語言規范規定了一些要求:
- 讀寫 nil Channel 會永遠阻塞,關閉 nil Channel 會導致 panic
- 關閉一個已關閉的 Channel 會導致 panic
- 向已經關閉的 Channel 發送資料會導致 panic
- 向已經關閉的 Channel 讀取資料不會導致 panic,但讀取的值為 Channel 傳遞的資料型別的零值,可以通過接受陳述句第二個回傳值來檢查 Channel 是否關閉且排空
v, ok := <- ch
if !ok {
... // 如果是非緩沖 Channel ,說明已經關閉;如果是帶緩沖 Channel ,說明已經關閉,且其內部緩沖區已經排空
}
錯誤處理
Go 語言的錯誤處理被設計為值型別,錯誤以介面的形式在語言中進行表達:
type error interface {
Error() string
}
任何實作了 error 介面的型別均可以作為 error 型別,對于下面的 CustomErr 結構而言:
type CustomErr struct {
err error
}
func (c CustomErr) Error() string {
return fmt.Sprintf("err: %v", c.err)
}
由于其實作了 Error() 方法,于是可以以 error 型別回傳給上層呼叫:
func foo() error {
return CustomErr{errors.New("this is an error")}
}
func main() {
err := foo()
if err != nil { panic(err) }
}
除了錯誤值以外,還可以使用 panic 與 recover 內建函式來進行錯誤的傳播:
func panic(v interface{})
func recover() interface{}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/463468.html
標籤:其他
