banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Go語言初上手(一) 環境配置與基礎語法 | 青訓營

字節第三屆青訓營是後端專場,開課了,高高興興寫筆記啦
課上很詳細的講了 Go 的基本語法,以及再加上自己閱讀 Go 語言聖經的一些總結,得出了這一篇文章,感覺跟 JS 和 c/c++ 還是有很多共通之處的。

內容來源於:Go 語言聖經 以及 第三屆青訓營課程
課程源碼 wangkechun/go-by-example

Go 語言簡介及安裝#

什麼是 Go 語言#

  • 高性能、高並發
  • 豐富的標準庫
  • 完善的工具鏈
  • 靜態鏈接
  • 快速編譯
  • 跨平台
  • 垃圾回收

總而言之,兼顧 c/c++ 的性能,並具有 python 等語言的簡潔、完善的標準庫

安裝#

  1. 訪問 https://go.dev/ ,點擊 Download ,下載對應平台安裝包,安裝即可
  2. 如果無法訪問上述網址,可以改為訪問 https://studygolang.com/dl 下載安裝
  3. 如果訪問 github 速度比較慢,建議配置 go mod proxy,參考 https://goproxy.cn/ 裡面的描述配置,下載第三方依賴包的速度可以大大加快

IDE 推薦#

  • vscode 安裝 Go 插件
  • GoLand JetBrains 系列的新 IDE,dddd
    image.png

可以通過 Github 很方便的登錄體驗該課程的示例項目 Dashboard — Gitpod (真好,我哭死)

基礎數據類型#

整型#

與 c++ 中類似,整型分有符號和無符號類型,有符號整數

  • int8、int16、int32 和 int64
  • 對應 8 位、16 位、32 位、64 位大小的有符號整數
  • uint8、uint16、uint32 和 uint64 則對應無符號整數
  • 另外的還有兩種對應特定 CPU 平台機器字大小的有符號和無符號整數intuint,其中int也是應用最廣泛的數值類型,這兩種類型都有同樣的大小: 32 或 64bit
    • 不同的編譯器即使在相同的硬體平台上可能產生不同的大小。
  • Unicode 字符 rune 類型是和 int32等價的類型,通常用於表示一個 Unicode 碼點。這兩個名稱可以互換使用。
  • byteuint8 類型的等價類型,byte 類型一般用於強調數值是一個原始的數據而不是一個小的整數。
  • uintptr 類型,沒有指定具體的 bit 大小但是足以容納指針。只有在底層編程時才需要,特別是 Go 語言和 C 語言函數庫或操作系統接口相交互的地方。我們將在第十三章的 unsafe 包相關部分看到類似的例子

可通過 Printf 函數的 %b 參數打印二進制格式的數字,用%d%o%x參數控制輸出的進制格式,這部分與 c 中的格式化輸出類似,

var x uint8 = 1<<1 | 1<<5

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"

x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

ascii := 'a'
unicode := ''
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 國 '國'"

上面的例子中,一般情況下 Printf 格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是 % 之後的[1] 副詞告訴Printf函數再次使用第一個操作數

  • %後的#副詞告訴Printf在用%o%x%X輸出時生成00x0X前綴。
  • 字符使用%c參數打印,或者使用 %q 參數打印帶單引號的字符

內置的 len 函數返回一個有符號的int,可以像下面例子那樣處理逆序循環。

medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

浮點數#

Go 中的浮點型有 float32float64

其範圍極限值可以在 math 包找到。

  • 常量 math.MaxFloat32 表示 float32 能表示的最大數值,大約是 3.4e38;對應的 math.MaxFloat64 常量大約是 1.8e308。它們分別能表示的最小值近似為 1.4e-454.9e-324
  • 使用Printf函數的 %g 參數打印浮點數,將採用更緊湊的表示形式打印,並提供足夠的精度,但是對應表格的數據,使用 %e(帶指數)或 %f 的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控制打印精度
for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
// x = 0       e^x =    1.000
// x = 1       e^x =    2.718
// x = 2       e^x =    7.389
// x = 3       e^x =   20.086
// x = 4       e^x =   54.598
// x = 5       e^x =  148.413
// x = 6       e^x =  403.429
// x = 7       e^x = 1096.633

math 包中除了提供大量常用的數學函數外,還提供了 IEEE754 浮點數標準中定義的特殊值的創建和測試:正無窮大和負無窮大Inf -Inf,分別用於表示太大溢出的數字和除零的結果;還有 NaN 非數,一般用於表示無效的除法操作結果,如 0/0 或 Sqrt (-1)

var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
  • Go 中的 NaN 與 JS 中類似,跟任何數都是不相等的,包括其自身,可以用math.IsNaN 用於測試一個數是否是非數 NaN
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

复数#

Go 語言提供了兩種精度的複數類型:complex64complex128,分別對應float32float64兩種浮點數精度。內置的 complex 函數用於構建複數,內建的 realimag 函數分別返回複數的實部虛部

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

如果一個浮點數面值或一個十進制整數面值後面跟著一個 i,例如3.141592i2i,它將構成一個複數的虛部,複數的實部是 0

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

一個複數常量可以正常加到另一個普通數值常量

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

math/cmplx 包提供了複數處理的許多函數,例如求複數的平方根函數和求幂函數。

fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"

布爾型#

true or false,這一點沒什麼好說的。

字符串#

Go 中的字符串類型string不可變字符串,與 JS 一樣,與 c++ 不同。

不變性意味著如果兩個字符串共享相同的底層數據的話也是安全的,這使得複製任何長度的字符串代價是低廉的。同樣,一個字符串 s 和對應的子字符串切片 s [7:] 的操作也可以安全地共享相同的內存,因此字符串切片操作代價也是低廉的。在這兩種情況下都沒有必要分配新的內存。

字符串中的第 i 個字節並不一定是字符串的第 i 個字符,因為對於非 ASCII 字符的 UTF8 編碼會要兩個或多個字節。

s[i:j] 基於原始的 s 字符串的第 i 個字節開始到第 j 個字節(不包含 j 本身)生成一個新字符串。生成的新字符串將包含 j-i 個字節。

  • ij 都可以被忽略,當它們被忽略時將採用0作為開始位置,採用len(s)作為結束的位置。
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:])  // "hello, world"

+ 操作符將兩個字符串連接構造一個新字符串:

fmt.Println("goodbye" + s[5:]) // "goodbye, world"

字符串的比較是通過逐個字節比較完成的,比較結果是字符串自然編碼的順序。

Go 語言源文件總是用 UTF8 編碼,並且 Go 語言的文本字符串也以 UTF8 編碼的方式處理,因此我們可以將 Unicode 碼點也寫到字符串面值中。

一個原生的字符串面值形式如下,使用反引號代替雙引號。

const GoUsage = `Go is a tool for managing Go source code.
Usage:
    go command [arguments]
...`

在原生的字符串面值中,沒有轉義操作;全部的內容都是字面的意思,包含退格和換行,因此一個程序中的原生字符串面值可能跨越多行

  • 在原生字符串面值內部是無法直接寫・反引號的,可以用八進制或十六進制轉義或 +"`" 連接字符串常量完成)。
  • 唯一的特殊處理是會刪除回車以保證在所有平台上的值都是一樣的,包括那些把回車也放入文本文件的系統

Windows 系統會把回車和換行一起放入文本文件中

以下是一些字符串方法

package main

import (
	"fmt"
	"strings"
)

func main() {
	a := "hello"
	fmt.Println(strings.Contains(a, "ll"))                // true
	fmt.Println(strings.Count(a, "l"))                    // 2
	fmt.Println(strings.HasPrefix(a, "he"))               // true
	fmt.Println(strings.HasSuffix(a, "llo"))              // true
	fmt.Println(strings.Index(a, "ll"))                   // 2
	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
	fmt.Println(strings.Repeat(a, 2))                     // hellohello
	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
	fmt.Println(strings.ToLower(a))                       // hello
	fmt.Println(strings.ToUpper(a))                       // HELLO
	fmt.Println(len(a))                                   // 5
	b := "你好"
	fmt.Println(len(b)) // 6
}

在 go 語言裡面的話,可以很輕鬆地用 %v 來打印任意類型的變量,而不需要區分數字字符串,也可以用 %+v 打印詳細結果,%#v 則更詳細。

package main

import "fmt"

type point struct {
	x, y int
}

func main() {
	s := "hello"
	n := 123
	p := point{1, 2}
	fmt.Println(s, n) // hello 123
	fmt.Println(p)    // {1 2}

	fmt.Printf("s=%v\n", s)  // s=hello
	fmt.Printf("n=%v\n", n)  // n=123
	fmt.Printf("p=%v\n", p)  // p={1 2}
	fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
	fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

	f := 3.141592653
	fmt.Println(f)          // 3.141592653
	fmt.Printf("%.2f\n", f) // 3.14
}

字符串和數字轉換#

go 語言當中,關於字符串和數字類型之間的轉換都在 strconv 這個包下,這個包是 string convert 這兩個單詞的縮寫。可以用 ParseInt 或者 ParseFloat 來解析一個字符串。也可以用 Atoi 把一個十進制字符串轉成數字。可以用 Itoa 把數字轉成字符串。

  • 如果輸入不合法,那麼這些函數都會返回 error 除了 Itoa
package main

import (
	"fmt"
	"strconv"
)

func main() {
	f, _ := strconv.ParseFloat("1.234", 64)
	fmt.Println(f) // 1.234

	n, _ := strconv.ParseInt("111", 10, 64)
	fmt.Println(n) // 111

	n, _ = strconv.ParseInt("0x1000", 0, 64)
	fmt.Println(n) // 4096

	n2, _ := strconv.Atoi("123")
	fmt.Println(n2) // 123

	n2, err := strconv.Atoi("AAA")
	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax

	n3 := strconv.Itoa(123) // 這個不返回error
	fmt.Println(n3) // 123
}

常量#

同其他語言的常量一樣,常量的值不可修改,且必須被初始化,若批量聲明常量時其除第一個其他的初始化表達式可被省略,若省略則使用前面的常量表初始化表達式,如下:

const pi = 3.14159 // approximately; math.Pi is a better approximation
const (
    e  = 2.71828182845904523536028747135266249775724709369995957496696763
    pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
const (
    a = 1
    b
    c = 2
    d
)

iota 常量生成器#

類似 c/c++ 中的枚舉類型 Enum!!

常量聲明可以使用iota常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個 const 聲明語句中,在第一個聲明的常量所在的行,iota 將會被置為 0,然後在每一個有常量聲明的行加一。

type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
// Sunday 對應 0 
// Monday 對應 1 
// ....
// Saturday 對應 6

也可以結合複雜的表達式使用 itoa,如下例:每個常量對應表達式1 << iota,是連續的 2 的幂

type Flags uint

const (
    FlagUp Flags = 1 << iota // is up
    FlagBroadcast            // supports broadcast access capability
    FlagLoopback             // is a loopback interface
    FlagPointToPoint         // belongs to a point-to-point link
    FlagMulticast            // supports multicast access capability
)

fmt.Println(FlagUp, FlagBroadcast, FlagLoopback, FlagPointToPoint, FlagMulticast)
// 1 2 4 8 16

無類型常量#

許多常量並沒有一個明確的基礎類型。Go 的編譯器為這些沒有明確基礎類型的數字常量提供比基礎類型更高精度的算術運算;你可以認為 至少有 256bit 的運算精度 。這裡有六種未明確類型的常量類型,分別是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。

只有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,無類型的常量將會被隱式轉換為對應的類型,如果轉換合法的話。

  • 對於沒有顯式類型的變量聲明(包括簡短變量聲明),常量的形式將隱式決定變量的默認類型,
    • 無類型整數常量轉換為 int,它的內存大小是不確定的,無類型浮點數和複數常量則轉換為內存大小明確float64complex128

程序結構#

https://books.studygolang.com/gopl-zh/ch2/ch2.html

聲明與變量#

var#

一般語法如下

var 變量名 類型 = 表達式

類型省略則根據表達式自動推導,如果表達式為空,則用 零值 初始化該變量(因此在 Go 語言中不存在未初始化的變量

類型零值
數值0
布爾false
字符串""
陣列或結構體等聚合類型nil

可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明並初始化一組變量。

var i, j, k int     // int int int
var b, f, s = true, 2.3, "hello" // bool float64 string

一組變量也可以通過調用一個函數,由函數返回的多個返回值初始化:

var f, err = os.Open(name) // os.Open returns a file and an error

簡短變量聲明 :=#

名字 := 表達式的形式聲明變量,變量的類型根據表達式來自動推導

  • 因為其簡潔和靈活的特點,簡短變量聲明被廣泛用於大部分的局部變量的聲明和初始化。
  • 而 var 形式的聲明語句往往是用於需要顯式指定變量類型的地方,或者因為變量稍後會被重新賦值而初始值無關緊要的地方。
i := 100                  // int
i, j := 0, 1              // int int
var boiling float64 = 100 // a float64
var names []string
var err error
  • 簡短變量聲明語句對在同級詞法域已經聲明過的變量只會進行賦值行為
  • 如果變量是在外部詞法域聲明的,那麼簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量

指針#

與 c 語言中類似,通過 &操作符取址,通過 * 取值

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

任何類型的指針的零值都是 nil

  • p指向某個有效變量,那麼 p != nil 測試為真。
  • 當兩指針指向同一個變量或全部是 nil 時才相等。

new 函數#

new(T) 將創建一個 T 類型的匿名變量,初始化為 T類型的零值,然後返回變量地址,返回的指針類型為 *T

  • Go 語言中的new是個預定義的函數,不是關鍵字!所以可以重新定義。
p := new(int)   // p, *int 類型, 指向匿名的 int 變量
fmt.Println(*p) // "0"
*p = 2          // 設置 int 匿名變量的值為 2
fmt.Println(*p) // "2"

自增 / 自減運算#

自增語句i++i加 1;這和i += 1以及i = i + 1都是等價的。對應的還有i--i減 1。它們是語句,而不像 C 系的其它語言那樣是表達式。

  • 所以j = i++ 非法,而且 ++ 和 -- 都只能放在變量名後面,因此--i也非法。

類型 type#

類似於 c++ 中的 typeof 的加強版,形式如下

type 類型名 底層類型

如下,聲明了兩種類型:CelsiusFahrenheit 分別對應不同的溫度單位。

  • 底層數據類型決定其內部結構和表達方式
  • 它們雖然有著相同的底層類型 float64,但是它們是不同的數據類型,因此它們不可以被相互比較或混在一個表達式運算。
  • 類型轉換不會改變值本身,但是會使它們的語義發生變化。
import "fmt"

type Celsius float64    // 攝氏溫度
type Fahrenheit float64 // 華氏溫度

const (
    AbsoluteZeroC Celsius = -273.15 // 絕對零度
    FreezingC     Celsius = 0       // 結冰點溫度
    BoilingC      Celsius = 100     // 沸水溫度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

比較運算符 == 和 < 也可以用來比較一個命名類型的變量和另一個有相同類型的變量,或有著相同底層類型的未命名類型的值之間做比較。但是如果兩個值有著不同的類型,則不能直接進行比較:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"! 類型轉換操作不會改變值

命名類型還可以為該類型的值定義新的行為。這些行為表示為一組關聯到該類型的函數集合,我們稱為類型的方法集 (在第六章會詳細講)

下面的聲明語句,Celsius 類型的參數 c 出現在了函數名的前面,表示聲明的是 Celsius 類型的一個名叫 String 的方法,該方法返回該類型對象 c 帶著 °C 溫度單位的字符串:

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

許多類型都會定義一個 String 方法,因為當使用 fmt 包的打印方法時,將會優先使用該類型對應的 String 方法返回的結果打印

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String

循環 for#

命令行參數・Go 語言聖經

Go 中的循環沒有 while、do while 等,只有一種 for循環
寫法如下:

for initialization; condition; post {
    // zero or more statements
}

for 循環三個部分不需括號包圍。大括號強制要求,左大括號必須和post語句在同一行。

  • initialization 語句是可選的,在循環開始前執行initalization 如果存在,必須是一條簡單語句(simple statement),即短變量聲明、自增語句、賦值語句或函數調用。
  • condition是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為true則執行循環體語句。
  • post語句在每次循環體執行結束後執行,之後再次對condition求值。condition值為false時,循環結束。

for 循環的這三個部分每個都可以省略,如果省略initializationpost,就是 while 循環,分號也可以省略,如果省略三個部分,則為永真循環,可通過 break 跳出:

i := 1
for {
        fmt.Println("loop")
        break
}
for j := 7; j < 9; j++ {
        fmt.Println(j)
}

for n := 0; n < 5; n++ {
        if n%2 == 0 {
                continue
        }
        fmt.Println(n)
}
for i <= 3 {
        fmt.Println(i)
        i = i + 1
}

分支結構#

if else#

Go 中的 if 類似 python,沒有括號,但後面必須跟大括號

if 7%2 == 0 {
        fmt.Println("7 is even")
} else {
        fmt.Println("7 is odd")
}

if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
}

if num := 9; num < 0 {
        fmt.Println(num, "is negative")
} else if num < 10 {
        fmt.Println(num, "has 1 digit")
} else {
        fmt.Println(num, "has multiple digits")
}

switch#

go 語言裡面的 switch 分支結構類似 c++。但也有很多不同:

  • switch 後面的那個變量名,也不要括號
  • c++ 中的 switch case 如果不加 break 的話會然後會繼續往下跑完所有的 case, 在 go 語言裡面的話是不需要加 break
  • go 語言裡面的 switch 功能更強大,可以使用任意的變量類型,甚至可以用來取代任意的 if else 語句。

你可以在 switch 後面不加任何的變量,然後在 case 裡面寫條件分支。這樣代碼相比你用多個 if else 代碼邏輯會更為清晰。

package main

import (
	"fmt"
	"time"
)

func main() {
	a := 2
	switch a {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	case 4, 5:
		fmt.Println("four or five")
	default:
		fmt.Println("other")
	}

	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("It's before noon")
	default:
		fmt.Println("It's after noon")
	}
}

進程信息#

在 go 裡面,我們能夠用 os.argv 來得到程序執行的時候的指定的命令行參數。比如我們編譯的一個 二進制文件,command。 後面接 abcd 來啟動,輸出就是 os.argv 會是個長度為 5slice , 第一個成員代表二進制自身的名字。我們可以用 so.getenv來讀取環境變量。exec

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// go run example/20-env/main.go a b c d
	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
	fmt.Println(os.Setenv("AA", "BB"))

	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf)) // 127.0.0.1       localhost
}

複合數據類型#

陣列#

陣列是一個由固定長度特定類型元素組成的序列,一個陣列可以由零個或多個元素組成。因為陣列的長度是固定的,因此在 Go 語言中很少直接使用陣列。和陣列對應的類型是Slice(切片),它是可以增長和收縮的動態序列,slice 功能也更靈活,但是要理解 slice 工作原理的話需要先理解陣列。

package main

import "fmt"

func main() {

	var a [5]int
	a[4] = 100
	fmt.Println("get:", a[2])
	fmt.Println("len:", len(a))

	b := [5]int{1, 2, 3, 4, 5}
	fmt.Println(b)

	var twoD [2][3]int
	for i := 0; i < 2; i++ {
		for j := 0; j < 3; j++ {
			twoD[i][j] = i + j
		}
	}
	fmt.Println("2d: ", twoD)
}

切片 Slice#

切片不同於陣列,可以任意更改長度,也有更多豐富的操作。

  • make創建一個切片,可以像陣列一樣去取值
  • 使用 append 來追加元素。注意 append 的用法與 js 中的concat相似,返回一個新陣列,把 append 的結果賦值為原陣列。
  • slice 初始化的時候也可以動態的指定長度。 len(s)
  • slice 擁有像 python 一樣的切片操作,比如s[2:5]代表取出第二個到第五個位置的元素,不包括第五個元素。不過不同於 python,這裡不支持負數索引
package main

import "fmt"

func main() {
	s := make([]string, 3)
	s[0] = "a"
	s[1] = "b"
	s[2] = "c"
	fmt.Println("get:", s[2])   // c
	fmt.Println("len:", len(s)) // 3

	s = append(s, "d")
	s = append(s, "e", "f")
	fmt.Println(s) // [a b c d e f]

	c := make([]string, len(s))
	copy(c, s)
	fmt.Println(c) // [a b c d e f]

	fmt.Println(s[2:5]) // [c d e]
	fmt.Println(s[:5])  // [a b c d e]
	fmt.Println(s[2:])  // [c d e f]

	good := []string{"g", "o", "o", "d"}
	fmt.Println(good) // [g o o d]
}

Map#

map 是實際使用過程中最頻繁用到的數據結構。

  • 可以用 make 來創建一個空 map ,這裡會需要兩個類型,keyvalue 的類型
    • map[string]int 表示 key 類型為stringvalue 類型為 int
  • map 的取值與插入類似 c++ 中 STL 的 map,可直接進行。 m[key] m[key] = value
  • 可以用 delete 從裡面刪除鍵值對
  • Go 中的map完全無序的,遍歷的時候不會按照字母順序,也不會按照插入順序輸出,而是隨機順序
package main

import "fmt"

func main() {
	m := make(map[string]int)
	m["one"] = 1
	m["two"] = 2
	fmt.Println(m)           // map[one:1 two:2]
	fmt.Println(len(m))      // 2
	fmt.Println(m["one"])    // 1
	fmt.Println(m["unknow"]) // 0

	r, ok := m["unknow"]
	fmt.Println(r, ok) // 0 false

	delete(m, "one")

	m2 := map[string]int{"one": 1, "two": 2}
	var m3 = map[string]int{"one": 1, "two": 2}
	fmt.Println(m2, m3)
}

range#

對於一個 slice 或者一個 map 的話,我們可以用 range 來快速遍歷,這樣代碼能夠更加簡潔。 range 遍歷的時候,對於陣列會返回兩個值,第一個是索引,第二個是對應位置的值。如果我們不需要索引的話,我們可以用下劃線 _ 來忽略。

Go 語言不允許使用無用的局部變量(local variables),因為這會導致編譯錯誤。用空標識符(blank identifier),即_(也就是下劃線)。空標識符可用於在任何語法需要變量名但程序邏輯不需要的時候(如:在循環裡)丟棄不需要的循環索引,並保留元素值。

package main

import "fmt"

func main() {
	nums := []int{2, 3, 4}
	sum := 0
	for i, num := range nums {
		sum += num
		if num == 2 {
			fmt.Println("index:", i, "num:", num) // index: 0 num: 2
		}
	}
	fmt.Println(sum) // 9

	m := map[string]string{"a": "A", "b": "B"}
	for k, v := range m {
		fmt.Println(k, v) // b 8; a A
	}
	for k := range m {
		fmt.Println("key: ", k) // key:  a; key:  b
	}
	for _, v := range m {
		fmt.Println("value:", v) // value: A; value: B
	}
}

結構體#

結構體的話是帶類型的字段的集合。比如這裡 user 結構包含了兩個字段,namepassword

  • 可以用結構體的名稱去初始化一個結構體變量,構造的時候需要傳入每個字段的初始值
  • 也可以用鍵值對的方式指定初始值,這樣可以只對一部分字段進行初始化
  • 同樣的結構體也支持指針,這樣能夠實現直接對於結構體的修改,可以在某些情況下避免一些大結構體的拷貝開銷
package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	a := user{name: "wang", password: "1024"}
	b := user{"wang", "1024"}
	c := user{name: "wang"}
	c.password = "1024"
	var d user
	d.name = "wang"
	d.password = "1024"

	fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
	fmt.Println(checkPassword(a, "haha"))   // false
	fmt.Println(checkPassword2(&a, "haha")) // false
}

func checkPassword(u user, password string) bool {
	return u.password == password
}

func checkPassword2(u *user, password string) bool {
	return u.password == password
}

JSON#

go 語言中的 JSON 操作非常簡單

  • 對於一個已有的結構體,只要保證每個字段的第一個字母是大寫,也就是是公開字段。那麼這個結構體就能用 JSON.marshaler 去序列化,變成一個 JSON 的字符串。

JSON.marshaler 返回序列化值和 error,如下例
這樣默認序列化出來的字符串,是大寫字母開頭。可以在後面用 json tag 等語法來去修改輸出 JSON 結果裡面的字段名。

  • 序列化之後的字符串可以用 JSON.unmarshaler反序列化到一個空的變量裡面。
package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"`
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
	buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

	buf, err = json.MarshalIndent(a, "", "\t")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

時間處理#

go 語言最常用的就是 time.now() 來獲取當前時間,然後你也可以用 time.date 去構造一個帶時區的時間,有很多方法來獲取這個時間點的年月日小時分鐘秒,

  • 可以用 Sub方法對兩個時間進行減法,得到一個時間段。
  • 時間段又可以得到它有多少小時,多少分鐘、多少秒。
  • 在和某些系統交互的時候,我們經常會用到時間戳。那可以用 .UNIX 來獲取時間戳。time.format time.parse
package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now) // 2022-05-07 13:12:03.7190528 +0800 CST m=+0.004990401
	t := time.Date(2022, 5, 7, 13, 25, 36, 0, time.UTC)
	t2 := time.Date(2022, 8, 12, 12, 30, 36, 0, time.UTC)
	fmt.Println(t)                                                  // 2022-05-07 13:25:36 +0000 UTC
	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-05-07 13:25:36
	diff := t2.Sub(t)
	fmt.Println(diff)                           // 2327h5m0s
	fmt.Println(diff.Minutes(), diff.Seconds()) // 139625 8.3775e+06
	t3, err := time.Parse("2006-01-02 15:04:05", "2022-05-07 13:25:36")
	if err != nil {
		panic(err)
	}
	fmt.Println(t3 == t)    // true
	fmt.Println(now.Unix()) // 1651900531
}

函數#

Go 和其他很多語言不一樣的是,函數參數變量類型是後置的。Go 中的函數原生支持返回多個值

  • 在實際的業務邏輯代碼裡面幾乎所有的函數都返回兩個值,第一個是返回值,第二個值是一個錯誤信息。 如下例中的 exists
package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func add2(a, b int) int {
	return a + b
}

func exists(m map[string]string, k string) (v string, ok bool) {
	v, ok = m[k]
	return v, ok
}

func main() {
	res := add(1, 2)
	fmt.Println(res) // 3

	v, ok := exists(map[string]string{"a": "A"}, "a")
	fmt.Println(v, ok) // A True
}

錯誤處理#

go 中的錯誤處理就是使用一個單獨的返回值來傳遞錯誤信息

  • 在函數返回值類型後面加一個 error, 代表這個函數可能會返回錯誤。那么在函數實現的時候, return 需要同時 return 兩個值
  • 出現錯誤時,可以 return nil 和一個 error。如果沒有的話,那麼返回原本的結果和 nil
package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil
		}
	}
	return nil, errors.New("not found")
}

func main() {
	u, err := findUser([]user{{"wang", "1024"}}, "wang")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(u.name) // wang

	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
		fmt.Println(err) // not found
		return
	} else {
		fmt.Println(u.name)
	}
}

工具推薦#

在課堂中提到的幾個代碼生成工具

課後練習#

  1. 修改第一個例子猜謎遊戲裡面的最終代碼,使用 fmt.Scanf 來簡化代碼實現
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(maxNum)
	// fmt.Println("The secret number is ", secretNumber)

	fmt.Println("Please input your guess")
	//reader := bufio.NewReader(os.Stdin)
	for {
		//input, err := reader.ReadString('\n')
		var guess int
		_, err := fmt.Scanf("%d", &guess)
		fmt.Scanf("%*c")    // 吃回車
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again", err)
			continue
		}
		//input = strings.TrimSuffix(input, "\n")
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}
		fmt.Println("You guess is", guess)
		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct, you Legend!")
			break
		}
	}
}
  1. 修改第二個例子命令行詞典裡面的最終代碼,增加另一種翻譯引擎的支持
  1. 在上一步驟的基礎上,修改代碼實現並行請求兩個翻譯引擎來提高響應速度

總結及心得#

課上很詳細的講了 Go 的基本語法,以及再加上自己閱讀 Go 語言聖經的一些總結,得出了這一篇文章,感覺跟 JS 和 c/c++ 還是有很多共通之處的。

內容來源於:Go 語言聖經 以及 第三屆青訓營課程

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。