編程基礎 0001_基礎教程

go

什麼是 Go
是一門併發支持、垃圾加收的編譯型系統編程語言,具有靜態編譯語言的高性能和動態語言的,主要特點如下

  • 類型安全和內存安全
  • 以非常直觀和極低代價的方案實現高併發
  • 高效的垃圾回收機制
  • 快速編譯(同時解決了 C 語言中頭文件太多的問題)
  • UTF-8 支持

安裝

  • 源碼安裝
  • 標準包安裝
  • 第三方安裝

標準包安裝,一路下一步。安裝完後,會自動添加如下環境變量,使用go env查看

GOARCH="amd64" # cup架構
GOBIN="/usr/local/go/bin"
GOCACHE="/Users/willzhao/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/willzhao/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/3q/z9zfyn256718rwvw071831tr0000gn/T/go-build200798760=/tmp/go-build -gno-record-gcc-switches -fno-common"

根據約定,GOPATH 下需要建立 3 個目錄

  • bin (存放編譯後生成的可執行文件,這個文件裏生成的文件一般拿到上一級去執行,要不可能會提示找不到依賴包)
  • pkg (存放編譯後生成的包文件)
  • src (存放項目源碼)

常用命令

  • go get 獲取遠程包(如果在 git 上獲取要先安裝 git,如果是在 google code 上下載遠程包的話需要安裝 hg
  • go run 直接運行程序
  • go build 測試編譯,檢查是否有編譯錯誤(什麼樣的源碼可以編譯成可執行文件呢? package 名稱爲 main 的,如果不是就不會生成可執行文件)
  • go fmt 格式化源碼(部分 ide 在保存時自動調用)
  • go install 編譯包文件並編譯整個程序(會找 main 的包,在 bin 文件夾中生成可執行文件)
  • go install是建立在 GOPATH 上的,無法在獨立的目錄裏使用go install
  • GOPATH下的 bin 目錄放置的是使用go install 生成的可執行文件,可執行文件的名稱來自於編譯時的包名
  • go install輸出目錄始終爲 GOPATH 下的 bin 目錄,無法使用-o 附加參數進行自定義
  • GOPATH下的 pkg 目錄放置的是編譯期間的中間文件
  • go test 運行測試文件,名字形如 xxx_test.go,正常的文件不在命名成這樣
  • godoc 查看文檔 godoc fmt,可以建立本地的網站來查看 godoc -http:8080

hello world

package main
import (
    "fmt"
)
func main(){
    fmt.Print("Hello World! 你好世界!") // 注意Print
}
// 好多的教程沒提,這裏最好是使用idear的開發環境來做 它可以設置一個project path
// 目錄結構 如上面所提src pkg bin
// 通過查看idear的run configuration確認,編譯命令執行如下
/*
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/willzhao/Documents/Develop/go/hello_world:/usr/local/go #gosetup
/usr/local/go/bin/go build -i -o /Users/willzhao/Documents/Develop/go/hello_world/bin/HelloWorldRun main #gosetup 注意main是包名
/Users/willzhao/Documents/Develop/go/hello_world/bin/HelloWorldRun #gosetup
*/

Go 的內置關鍵字(25 個)

25 個,並且都是小寫

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

註釋

  • // 單行
  • /* */ 多行

程序一般結構

  • Go 程序是通過 package 來組織(與 Python 類似) 必須放在非註釋的第一行。
  • 只有 package 名稱爲 main 的包可以包含 main 函數,一般建議 package 的名稱和目錄名一致 package 名稱不要使用駝峯標記法,使用下劃線
  • 別名調用 如import std "fmt"
  • 省略調用 如 import . "fmt" 使用時可以直接使用Print來調用(不建議這樣使用,導致功能不知道是哪個包導入的)
  • 省略調用和別名調用不能同時使用
  • 一個可執行程序有且僅有一個 main 包
  • 通過 import 關鍵字來導入其它非 main 包,導入多個包時可以用一個 import 加括號,導入包如果沒有用,編譯時會報錯
  • 通過 const 關鍵字來進行常量定義
  • 通過函數體外部使用 var 關鍵字來進行全局變量的聲明與賦值
  • 通過 type 關鍵字來進行結構 struct 或接口 interface 聲明
  • 通過 func 關鍵字來進行函數聲明

可見性規則

Go 語言中,使用大小寫來決定常量、變量、類型、接口、結構或函數是否可以被外部包所調用,可以快速確定是否調用錯誤

  • 首字母小寫即爲 private
  • 首字母大寫即爲 public

聲明多個常量變量

// 常量定義
const (
    PI = 3.14
    const1 = "1"
    const2 = 2
    const3 = 3
)
// 全局變量聲明與賦值,不能在函數體內聲明
var (
    name = "gopher"
    name1 = "1"
    name2 = 2
    name3 = 3
)
// 一般類型聲明
type (
    newType int
    type1 float32
    type2 string
    type3 byte
)

數據類型

  • 布爾型 bool
  • 長度 1 個字節
  • 取值範圍 true, false
  • 不能用其它值代表 true 或 false
  • 整型 int/uint
  • 根據平臺可能爲 32 或 64 位
  • 8 位整型 int8/uint8
  • 長度 1 字節
  • -128~127/0~255
  • 字節型 byte(uint8 別名)
  • 16 位整型 int16/uint16
  • 長度 2 字節
  • -32768~32767/0~65536
  • 32 位整型 int32(rune)/uint32
  • 長度 4 字節
  • 64 位整型 int64/uint64
  • 浮點型 float32/float64
  • 長度 4/8 字節
  • 小數位精確到 7/15 小數位
  • 複數 complex64/complex128
  • 長度 8/16 字節
  • 足夠保存指針的 32 位或 64 位整數型 uintptr
  • 其它值類型
  • array/struct/string
  • 引用類型
  • slice/map/chan
  • 接口類型 inteface
  • 函數類型 func

以上類型的默認類型

// 數值爲0 bool是false string爲"" 數組則根據它所指定的類型
// math.MinInt8 math有一些常量來定義值範圍邊界
package main
import (
    "fmt"
    "math"
)
type (
    byte int8
    rune int32
)

單個變量的聲明與賦值

  • 變量聲明格式:var 變量名 變量類型
  • 變量賦值格式:變量名稱=表達式
  • 聲明的同時賦值:var 變量名稱 變量類型 = 表達式 這裏的變量類型可以省略,編譯器可以推斷出來類型
package main

import (
    "fmt
)
func main(){
    var b int;
    // 或 變量聲明與賦值的最簡寫法
    a:=1
}

多個變量的聲明與賦

  • 全局變量的聲明可以使用 var()的方式進和簡寫
  • 全局變量的聲明不可以省略 var,但可以並行方式 即 var ()
  • 所有變量都可以使用類型推斷
  • 局部變量不可以使用 var()的方式簡寫,只能使用並行方式
package main

import (
    "fmt"
)
var (
    aaa = "hello"
    sss,bbb= 1,2
    // ccc := 3 這方式是錯的
)

func Test(){
    // 多變量聲明
    var a,b,c,d int
    // 多個變量的賦值
    a,b,c,d = 1,2,3,4
    // 多個變量聲明的同時賦值
    var e,f,g,h int = 5,6,7,8
    // 省略變量類型,由系統推斷
    var i,j,k,l = 9,10,11,12
    // 多個變量聲明與賦值的最簡寫法
    i,m,n,o :=13,14,15,16
    aa,_,cc,dd = 1,2,3,4 //_是忽略符號
}

類型轉換:不存在隱式轉換,所有類型轉換必須顯式聲明,轉換必須在兩咱兼容的之間

類型與變量

package main
import (
    "fmt"
    "strconv"
)
func main(){
    var a int = 65
    b:=string(a)
    fmt.Print(b) // A
    b = strconv.Itoa(a)
    a = strconv.Atoi(b)
    fmt.Print(b)
    fmt.Print(a)
}

常量定義

  • 常量的值在編譯時就已經確定
  • 常量定義格式與變量基本相同
  • 等號右側必須是常量或者常量表達式
  • 常量表達式中的函數必須是內置函數
const a int = 1
const b = 'A'
const (
    c = a
    d = b
)
const (
    e = 1
    f
    g
)
// f g 等一上行定義的值
const (
    a,b = 1,"2"
    c,d
)
// c = 1 d="2" 格式要一致(與上一行)
// iota 從第一個計數
const (
    a = 'A'
    b = iota
    c = 'B'
    d = iota
)
// 65
// 1
// 66
// 3
// 在每個組中都是從0開始加1的
// 常量不想被外包使用,則定義時不要以大寫母開頭,比如使用劃線或小寫字母如c
// 可以做一個常量枚舉
const (
    Monday = iota
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

運算符

  • ^ ! (一元)
  • * / % \<\< >> & &^
  • + - | ^ (二元)
  • == != \< \<= >= >
  • \<- (專門用於 channel)
  • 位運算符 & | ^異或 &^(位清空 and not)
  • &&
  • ||

這裏說明一下^&

// 計算x&^y 首先我們先換算成2進制  0000 0010 &^ 0000 0100 = 0000 0010 如果ybit位上的數是0則取x上對應位置的值, 如果ybit位上爲1則取結果位上取0
package main

import (
    "fmt"
)

func main(){
   var x,y int = 2,4
   fmt.Print(x^&y) // 2
}

實現一個計算機存儲單位枚舉

const (
    _ = iota // B = iota
    KB float64 = 1 << (iota*10)
    MB float64 = 1 << (iota*10)
    GB float64 = 1 << (iota*10)
    TB float64 = 1 << (iota*10)
    PB float64 = 1 << (iota*10)
    EB float64 = 1 << (iota*10)
    ZB float64 = 1 << (iota*10)
    YB float64 = 1 << (iota*10)
)

指針

Go 雖然保留了指針,但與其它編程語言不同的是,在 Go 當中不支持指針運算”->“運算符,而直接採用"."選擇符來操作指針目標對象的成員

  • 操作符”&“取變量地址,使”*“通過指針間接訪問目標對象
  • 默認值爲 nill 而 NULL
package main

import (
    "fmt"
)

func main(){
    a:=1;
    var b *int = &a
    fmt.Print(*b) // 1
}

遞增遞減

++ —— 在 Go 語言中是不能作爲表達式而是作爲語句,所以只能寫在單獨一行

條件表達式

  • if 沒有括號,可以使用初始化語句,並且是塊級作用域
  • 左括號必須放在 if 語句同一行
package main

import (
    "fmt"
)

func main(){
    a := 10
    if a :=4; a > 0 {
        fmt.Println(a)
    }
    fmt.Println(a)
}

循環語句 for

只有 for 一個循環語句,但它支持 3 種形式,

  • 初始化和步進表達式可以是多個值
  • 條件語句每次都會被重新檢查,因此不建議在條件語句中使用函數,儘量提前計算好並以變量或常量代替
  • 左大括號必須和條件語句一行
func main(){
    a:=1
    for {
        a++
        if a>3 {
            break
        }
    }
    fmt.Printf(a)
}
// 2
func main() {
    a := 1
    for a <= 3 {
        a++
    }
    fmt.Println(a)
}
// 3
func main() {
    a := 1
    for i := 0;i<3;i++ {
        a++
    }
    fmt.Println(a)
}

switch

  • 可以使用任何類型或表達式作爲條件語句
  • 不需要寫 break,一旦條件符合自動終止
  • 如希望繼續執行下一個 case,需要使用 fallthrough 語句
  • 支持一個初始化表達式(可以是並行方式),右側需要跟分號
  • 左大括號必須和條件語句在同一行
func main() {
    a := 1
    switch a {
        case 0:
            fmt.Println("a=0")
        case 1:
            fmt.Println("a=1")
    }
    fmt.Println(a)
}
func main() {
    a := 1
    switch {
        case a>=0:
            fmt.Println("a=0")
            fallthrough
        case a>=1:
            fmt.Println("a=1")
        default:
            fmt.Println("None")
    }
    fmt.Println(a)
}
func main() {
    switch a := 1; { //注意右側的分號,a的作用域範圍只在這個switch塊中
        case a>=0:
            fmt.Println("a=0")
            fallthrough
        case a>=1:
            fmt.Println("a=1")
    }
}

跳轉語句

goto break continue

  • 三個語法都可以配合標籤使用
  • 標籤名區分大小寫,若不使用會造成編譯錯誤
  • break 和 continue 配合標籤可以用於多層循環的跳出,跳出 LABEL 標識的
  • goto 是調整執行位置,與其它 2 語句配合標籤的結果並不相同
func main() {
    LABEL:
        for i:=0;i<10;i++ {
            if i>2 {
                break LABEL
            } else {
                fmt.Println(i)
            }
        }
}

func main() {
    LABEL:
        for i:=0;i<10;i++ {
            for {
                fmt.Println(i)
                continue LABEL
            }
        }
}

func gotest() {
    i := 0
LABEL:
    for i < 10 {
        for {
            fmt.Println(i)
            i++
            //goto LABEL
            continue LABEL
        }
    }
}

數組

  • 定義var <varName>[n]<type> = 0
  • 長度也是類型的一部分,因此具有不同長度的數組爲不同類型
  • 注意區分指向數組的指針和指針數組
  • 數組在 Go 中爲值類型
  • 數組之間可以使用==或!=進行比較,但不能使用
  • 可以使用 new 來創建數組,此方法返回一個指向數組的指針
  • 支持多維數組
var a [2]int
var b [2]int
var c [2]string
a := [2]{1,2}
b := [20]{19:1} //第20個位置設置成1,其它設置成0值
c := [...]int{1,2,3,4,5} //長度爲5的數組
d := [...]int{0:1,1:2,2:3}
e := [...]int(19:1) //長度20
// 指向數組的指針
var p *[20]int = &e
// 指針數組
x,y := 1,2
a := [...]*int{&x,&y} //保存的是指針
// 類型的不同不能進行直接比較
// 如果用new 創建的數組它返回的就是一個指向數組的指針
p := new([10]int) //p可以直接用索引來操作數組
p[1] = 2
fmt.Println(p)
// 多維數組
a :=[2][3]{
    {1,1,1},
    {2,2,2},
}
fmt.Println(a)
// 冒泡排序
func main() {
    a := [...]int(5,6,3,9)
    fmt.Println(a)
    num := len(a)
    for i:=0;i<num;i++ {
        for j:=i+1;j<num;j++ {
            if a[i]<a[j] {
                tmp:=a[i]
                a[i] = a[j]
                a[j] = tmp
            }
        }
    }
    fmt.Println(a)
}

切片

  • 本身並不是數組,它指向底層的數組
  • 作爲爲變長數組的替代方案,可以關聯底層數組的局部或全部
  • 爲引用類型
  • 可以直接創建或從底層數組獲取生成
  • 使用 len()獲取元素個數,cap()獲取容量
  • 一般使用 make()創建
  • 如果多個 slice 指向相同的底層數組,其中一個的值改變會影響全部
  • make([]T,len,cap),其中 cap 可以省略,則和 len 值相同
  • len 表示存數的元素個數,cap 表示容量
func main() {
    //這樣就是一個slice
    var s1 []int
    fmt.Println(s1)
    a:=[10]int{}
    fmt.Println(a)
    s2:=a[5:10] //索引開始(包含)結束(不包含)
    s3:=a[5:len(a)] //索引開始(包含)結束(不包含)
    s4:=a[5:] //索引開始(包含)結束(不包含)
    fmt.Println(s2)
    fmt.Println(s3)
    fmt.Println(s4)
    // make
    s5 := make([]int,22,1) //參數2是多少個,參數3 初始容量,當容量超出初始容量,它每次都會增加一倍
}

func t1() {
    s1:=make([]int,3,10)
    fmt.Println(len(s1),cap(s1))
    a:=[]byte{'a','b','c','d','e','f','g','h','i','j','k'}
    sa:=a[2:5]
    fmt.Println(string(sa))
    sb:=a[3:5]
    fmt.Println(string(sb))
}
  • reslice
  • reslice 時索引被 slice 的切片爲準
  • 索引不可以超過被 slice 切片的容量 cap()值
  • 索引越界不會導致底層數組的重新分配而是引發錯誤
  • append
  • 可以在 slice 尾部追回元素
  • 可以將一個 slice 追回在另一個 slice 尾部
  • 如果最終長度未超過追回到 slice 容量則返回原始 slice
  • 如果超過追回到的 slice 容量則將重新分配數組並拷貝原始數據
  • copy
// reslice
 a:=[]byte{'a','b','c','d','e','f','g','h','i','j','k'}
 sa:=a[2:5] //cde
 sb:=sa[1:3] //de
 fmt.Println(string(sb))
//  append
s1:=make([]int,3,6)
fmt.Printf("%p\n",s1)
s1 = append(s1,1,2,3)
fmt.Printf("%v %p\n",s1,s1)
s1 = append(s1,1,2,3)
fmt.Printf("%v %p\n",s1,s1)
/*
0xc000018120
[0 0 0 1 2 3] 0xc000018120
[0 0 0 1 2 3 1 2 3] 0xc00006e060
*/
// copy

map

  • 類似其它語言中的哈希或者字典,以 key-value 形式存儲數據
  • key 必須是支持==或!==比較運算類型,不可以是函數,map 或 slice
  • map 查找比線性搜索快很多,但比使用索引訪問數據的類型慢 100 倍
  • map 使用 make()創建,支持:=這種簡寫方式
  • make([keyType]valueType,cap) cap 表示容量,可省略
  • 超出容量時會自動擴容,但儘量提供一個合理的初始值
  • 使用 len()獲取元素個數
  • 鍵值對不存在時自動添加,使用 delete()刪除某鍵值對
  • 使用用for range對 map 和 slice 進行迭代操作
func main() {
    var m map[int]string
    m = make(map[int]string)
    // var m map[int]string = make(map[int]string)
    // m:= make(map[int]string)
    m[1] = "OK"
    a = m[1]
    delete(m,1)
    // 更復雜的map
    var m = map[int]map[int]string //m 的value又是一個map
    m = make(map[int]map[int]string)
    a,ok := m[2][1]
    fmt.Println(ok)
    if !ok {
        m[2] = make(map[int]string)
    }
    m[2][1] = "GOOD"
    a, ok = m[2][1]
    fmt.Println(ok)
    // for range
    for k,v := range map {

    }
}

func t1() {
    sm:=make([]map[int]string,5) //聲明一個slice 值是map[int]string
    for _,v:=range sm {
        v=make(map[int]string,1) //v每次都是一個copy,所以不能這樣寫
        fmt.Println(v)
    }
    fmt.Println(sm)
}

func t2() {
    sm:=make([]map[int]string,5) //聲明一個slice 值是map[int]string
    for i :=range sm {
        sm[i]=make(map[int]string,1) //v每次都是一個copy,所以不能這樣寫
        sm[i][1] = "OK"
        fmt.Println(sm[i])
    }
    fmt.Println(sm)
}
// 對map的間接排序

對 map 的間接排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    m:=map[int]string{1:"a",2:"b",3:"c",4:"d",5:"e"}
    s:=make([]int,len(m))
    i:=0
    for k,_ := range m {
        s[i] = k
        i++
    }
    // sort.Ints(s) // 沒有這句,可以看一下打印結構
    fmt.Println(s)
}
// map 鍵值對調
func main() {
    m1:=map[int]string{1:"a",2:"b",3:"c",4:"d",5:"e"}
    fmt.Println(m1)
    m2:=make(map[string]int)
    for k,v := range m1 {
        m2[v] = k
    }
    fmt.Println(m2)
}

函數 function

  • Go 函數不支持嵌套,重載,默認參數
  • 定義函數使用關鍵字 func,且左大括號不能另起一行
  • 函數也可以作爲一種類型使用
  • 支持以以下特性
  • 無需聲明原型
  • 不定長變參
  • 多返回值
  • 命名返回值參數
  • 匿名函數
  • 閉包
func A(a ...int)(int,int,int){
    // ...不定長可變參
    a,b,c = 1,2,4
    return //默認返回a b c // 返回參數可以寫成a,b,c int
    // 注意傳入的時候都是拷貝 一種情況是拷貝值 另一種情況是拷貝地址(還是指向同一個位置)
}
// 匿名函數
func main(){
    a:=func(){
        fmt.Println("func A")
    }
    a()
    // 調用閉包
    f:=closure(10)
    fmt.Println(f(1))
    fmt.Println(f(2))
}
// 閉包
func closure(x int) func(int)int{
    return func (y int)int {
        return x*y
    }
}

defer

類似析構,執行方式類似其它語言中的析構函數,在函數體執行結束後按照調用順序的相反順序逐個執行。

  • 即使函數發生嚴重錯誤也會執行
  • 支持匿名函數的調用
  • 常用的資源清理、文件關閉、解鎖以及記錄時間等操作
  • 通過與匿名函數配合可在 return 之後修改函數計算結果
  • 如果函數體內某變量作爲 defer 時匿名函數的參數,則在定義 defer 即已經獲得了拷貝,否則則是引用某個變量的地址
  • Go 沒有異常機制,但有 panic/recover 模式來處理錯誤
  • panic 可以在任何地方引發,但 recover 只有在 defer 調用的函數中有效
func main() {
    for i:=0;i<3;i++ {
        // 當main函數執行完閉後纔開始執行,而且i是(因爲是閉包,是地址的拷貝)每次輸出3
        defer func() {
            fmt.Println(i)
        }()
    }
    // 如果在傳入i會怎麼樣?
    for i:=0;i<3;i++ {
        defer func(i int) {
            fmt.Println(i)
        }(i)
    }
}
// panic之前註冊defer來恢復到正常執行
func main() {
    A()
    B()
    C()
}
func A() {
    fmt.Println("Func A")
}
func B() {
    defer func(){
        if err:=reocver(); err!=nil {
            fmt.Println("Recover in B")
        }
    }()
    panic("Panic in B")
}
func C() {
    fmt.Println("func C")
}

閉包,最外層不能是一個匿名函數

// 分析結果
package main

import (
    "fmt"
)

func main() {
    var fs = [4]func(){}
    for i:=0;i<4;i++ {
        defer fmt.Println("defer i=",i)
        defer func(){
           fmt.Println("defer_closure i=", i) // 閉包 引用值拷貝
        }()
        fs[i] = func(){
            fmt.Println("closure i = ",i) // 閉包 引用值拷貝
        }
    }
    for _,f:=range fs {
        f()
    }
}

結構 struct

  • Go 中的 struct 與 c 中的 struct 非常相似,並且 Go 沒有 class
  • 使用type <name> struct{}定義結構,名稱遵循可見規則
  • 支持指向自身的指針類型成員
  • 支持匿名結構,可用作成員或定義成員變量
  • 匿名結構也可以用於 map 的值
  • 可以使用字面值對結構進行初始化
  • 允許直接通過指針來讀寫結構成員
  • 相同類型的成員可進行直接拷貝賦值
  • 支持==!=比較運算符,但不支持><
  • 支持匿名字段,本質上是定義了某個類型名爲名稱字段
  • 嵌入結構作爲匿名字段看起來像繼承,但不是繼承
  • 可以使用匿名指針
// 定義結構
type test struct{}

// 定義人
type person struct{
    Name string,
    Age int
}

func main() {
    // a:=test{}
    a:=person{}
    a.Name = "joe"
    a.Age = 19
    fmt.Println(a)
    // 可以使用字面值初始化
    b:=person{
        Name:"Will",
        Age:18
    }
    fmt.Println(b)
    A(b) // 值的拷貝
    fmt.Perintln(b) //age 18
    B(&b) //第次都要對b取地址是不是很麻煩,我可以在定義b時就寫成指針 如b:=&person{Name:"Will",Age:18}, b就是指向結構的指針
    // 使用屬性時也不用像c下樣*b.Name 而是直接寫b.Name="OK"
    fmt.Perintln(b) //age 13
}
func A(per person) {
    per.Age=13
    fmt.Println("A",per)
}
// 如果想修改傳入的結構類型,則需要傳入地址拷貝
func B(per *person) {
    per.Age=13
    fmt.Println("A",per)
}
// 匿名結構
func main() {
    a: = struct {
        Name string
        Age int
    }{
        Name:"Joe",
        Age:19
    }
    fmt.Println(a)
}
// 如下
type person struct {
    Name string
    Age int
    // 匿名結構
    Contact struct {
        Phone,City string
    }
}
func main() {
    a:=person{Name:"joe",Age:18}
    a.Contact.Phone = "12xxx"
    a.Contact.City = "Bei Jing"
    fmt.Println(a)
}
// 字段匿名,必須嚴格按照聲明的順序
type person struct {
    string,
    int
}
func main(){
    a:=person{"joe",19} //調換順序會報錯
    fmt.Println(a)
    // 相同結構可以賦值 相同類型之間
    b:=person{}
    b=a
    fmt.Println(b)
}
// 怎樣實現類似繼承的功能 嵌入
type human struct{
    Sex int
}
type teacher struct {
    human
    Name string
    Age int
}
type student struct{
    human
    Name string
    Age int
}
func main() {
    a:=teacher{Name:"Joe",Age:18,human:human{Sex:0}}
    b:=student{Name:"Jay",Age:19,human:human{Sex:1}}
    a.Name = "joe2"
    a.Age=13
    a.human.Sex = 2
    // a.Sex=3 可以直接使用
    fmt.Println(a,b)
    // 嵌入結構怎麼賦值
}

如果匿名字段和外層結構有同名字段,應該如何進行操作?

type A struct {
    B
    // C 會報錯 編譯器無法定你要使用哪個Name
    Name string
}
type B struct {
    Name string
}
type C struct {
    Name string
}
func main(){
    a:=A{Name:"A",B:B{Name:"B"}}
    fmt.Println(a.Name,a.B.Name)
}

https://gowalker.org/

方法 method

  • Go 中雖然沒有 class,但依舊有 method
  • 通過顯示說明 receiver 來實現與某個類型的組合
  • 只能爲同一個包中的類型定義方法
  • receiver 可以是類型的值或指針
  • 不存在方法重載
  • 可以使用值或指針來調用方法,編譯器會自動完成轉換
  • 從某種意義上說,方法是函數的語法糖,因爲 receiver 其實就是方法所接收的第一個參數
  • 如果外部結構和嵌入結構存在同名方法,則優先調用外部結構方法
  • 類型別名不會擁有底層類型所附帶的方法
  • 方法可以調用結構中的非公開字段
type A struct {
    Name string
}
type B struct {
    Name string
}
func main() {
    a:=A{}
    a.Print()
    fmt.Println(a.Name)
    b:=B{}
    b.Print()
    fmt.Println(b.Name)
}
// 定義方法,先寫括號定義了A這個類型有一個方法Pring
func (a *A) Print(){ //引入拷貝
    a.Name="AA"
    fmt.Println("A")
}
func (b B) Print(){
    fmt.Println("A")
}
// 類型別名和方法的組合
type TZ int
func main(){
    var a TZ
    a.Print()
}
// 第一個括號就是receiver 就是誰來接收它
func (a *TZ) Print(){
    fmt.Println("TZ")
}

方法和函數

type TZ int
func main(){
    var a TZ
    a.Print() //method value
    (*TZ).Print(&a) // method expression
}
func (a *TZ) Print(){
    fmt.Println("TZ")
}
// 方法是可以訪問結構的私有字段,大寫小寫只對package的限制的。在本包內都是可見的
// 根據爲結構增加方法的知識,嘗試聲明一個底層類型爲int的類型,並實現調用某個方法就遞增100
// 如 a:=0 調用a.Increase()之後,a從0變成100
type TZ int
func (tz *TZ) Increase(num int){
    *tz+=TZ(num) //要做強制類型轉換
}
func main() {
    // 因爲不是結構類型所以要使用var
    var a TZ
    a.Increase(100)
    fmt.Println(a)
}

接口 interface

  • 不需要顯式說明你實現了哪個接口,只要實現了相同簽名的功能,編譯器就會認是實現了相應的接口
  • 接口是一個或多個方法簽名的集合
  • 只要某個類型擁有該接口的所有方法簽名,即算實現該接口,無需顯式聲明實現了哪個接口,這稱爲 Structural Typing
  • 接口可以匿名嵌入其它接口,或嵌入到結構中
  • 將對象賦值給接口時,會發生拷貝,而接口內部存儲的是指向這個複製品的指針,既無法修改複製器的狀態,也無法獲取指針
  • 只有當接口存儲的類型和對象都爲 nil 時,接口才等於 nil
  • 接口調用不會做 receiver 的自動轉換
  • 接口同樣支持匿名字段方法
  • 接口也可以實現類似 OOP 中的多態
  • 空接口可以作爲任何類型數據的容器
type USB interface {
    Name() string
    Connect()
}
type PhoneConnector struct{
    name string
}

func (pc PhoneConnector) Name() string {
    return pc.name
}
func (pc PhoneConnector) Connect() {
    fmt.Println("Connect:", pc.name)
}
func main() {
    var a USB
    a = PhoneConnect{"PhoneConnector"}
    // 可以簡寫成a:= PhoneConnect{"PhoneConnector"} , 但這樣寫不能直觀的看出來是否是實現了接口USB
    a.Connect()
    Disconnect(a)
}

func Disconnect(usb USB){
    // 判斷類型
    if pc,ok:=usb.(PhoneConnect);ok {
        fmt.Println("Disconnected")
        return
    }
    fmt.Println("Unknown device")
}
// 可以允許傳遞空接口
func Disconnect(usb interface{}){
    // 判斷類型
    if pc,ok:=usb.(PhoneConnect);ok {
        fmt.Println("Disconnected")
        return
    }
    fmt.Println("Unknown device")
    // type switch
    switch v:=usb.(type){
        case PhoneConnector:
        fmt.Println("Disconnected",v.name)
        default:
        fmt.Println("Unknown device.")
    }
}
// 嵌入接口
type USB interface {
    Name() string
    Connecter
}
type Connecter interface {
    Connect()
}
// 類型轉換 USB可以轉換成Connecter 但Connecter不能轉換成USB

反射 reflection

  • 反射可大大提高程序的靈活性,使得 interface{}有更大的發揮餘地
  • 反射使用 TypeOf 和 ValueOf 函數從接口中獲取目標對象信息
  • 反射會將匿名字段作爲獨立字段(匿名字段本質)
  • 想要利用反射修改對象狀態,前提是 interface.data 是 settable,即 point-interface
  • 通過反射可以“動態”調用方法
package main
import (
    "fmt"
    "reflect"
)
type User struct {
    Id int
    Name string
    Age int
}
func (u User) Hello() {
    fmt.Println("Hello world.")
}
type Manger struct{
    User
    title string
}
func main() {
   u:=User(1,"OK",12)
   Info(u)
   // 取manager的匿名字段
   m:=Manager{User:User{1,"ok",32},title:"boss"1}
   t:=reflect.TypeOf(m)
   fmt.Printf("%#v\n",t.Field(0))
   fmt.Printf("%#v\n",t.Field(1))
   //也可以使用下面這種方法
   fmt.Printf("%#v\n",t.FieldByIndex([]int{0,1})) //取匿名字段的第二個字段
}
func Info(o interface{}) {
    t:=reflect.TypeOf(o)
    fmt.Println("Type:",t.Name())
    // 判斷類型
    if k:=t.Kind();k!=reflect.Struct{
        fmt.Println("XX")
        return
    }
    v:=reflect.ValueOf(o)
    fmt.Println("Fields:")
    for i:=0;i<t.NumField();i++ {
        f:=t.Field(i)
        val:=v.Field(i).Interface()
        fmt.Printf("%6s:%v=%v\n",f.Name,f.Type,val)
    }
    // 打印方法
    for i:=0;i<t.NumMethod();i++{
        m:=t.Method(i)
        fmt.Printf("%6s:%v\n",m.Name,m.Type)
    }
}
type User struct {
    Id int
    Name string
    Age int
}
func (u User) Hello(name string){
    fmt.Println("Hello",name," my name is ",u.Name)
}
func main() {
    x:=123
    v:=reflect.ValueOf(x)
    v.Elem().SetInt(999)
    fmt.Println(x)
    // 處理複雜結構
    u:=User{1,"OK",12}
    Set(&u)
    fmt.Println(u)
    // 反射調用方法
    v:=reflect.ValueOf(u)
    mv:=v.MethodByName("Hello")
    args:=[]reflect.Value{reflect.ValueOf("joe")}
    mv.Call(args)
}
func Set(o interface{}){
    v:=reflect.ValueOf(o)
    if v.Kind()==reflect.Ptr&&!v.Elem().CanSet() {
        fmt.Println("不能修改")
        return
    }else{
        v=v.Elem()
    }
    f:=v.FieldByName("Name")
    if !f.IsValid() {
        fmt.Println("BAD")
        return
    }
    if f.Kind()==reflect.String {
        f.SetString("HaHa")
    }
}

併發 concurrency

併發不是並行,併發主要由切換時間片來實現“同時”運行,在並行則是直接利用多核實現多線程的運行,但 go 可以設置使用核數,以發揮多枋計算機的能力,Goroutine 奉行通過通信來共享內存,而不是共享內存來通信;
定義一個函數,使用 go 命令

// 怎麼運行一個go routine
package main

import (
    "fmt"
    "time"
)

func main() {
    go Go()
    time.Sleep(2*time.Second)
}
func Go() {
    fmt.Println("Go! go! go!")
}
// 以上的是最簡單的go routine
// 以上這種方式無法做一Go()執行完畢後,通知main函數,“我執行完畢了,可以結束了”
// 要做到這種通信的能力,我們使用channel

channel

  • channel 是 goroutine 的溝通橋樑,大都是阻塞同步的
  • 通過 make 創建,close 關閉
  • 可以使用 for range 來迭代不斷操作 channel
  • 可以設備單向或雙向通道
  • 可以設置緩存大小,在未被填滿前不會發生阻塞
func main() {
    c:=make(chan bool)
    go func(){
        fmt.Println("GO GO GO!!!")
        c <- true //執行go go go 打印,把channel的一個返回值通知main可以結束
    }()
    <-c //main函數執行到這個等待channel的返回值,當有返回值結束
}
// 使用for range來迭代
func main() {
    c:=make(chan bool)
    go func(){
        fmt.Println("go go go")
        c <- true
        close(c)
    }()
    for v :=range c {
        fmt.Println(v)
    }
}
// 使用channel迭代時,必須有一個地方明確關閉它,並且是關閉成功的。否則程序會變成死鎖的
// 有緩存與無緩存的主要區別
//   無緩存時 是阻塞的
//   有緩存時
func main() {
    c:=make(chan bool)
    go func() {
        fmt.Println("Go Go Go")
        <-c
    }()
    c <- true //無緩存時它會等到這個值被讀掉,再結束
}
// 而有緩存時,
func main() {
    c:make(chan bool,1)
    go func(){
        fmt.Println("Go Go Go")
        <-c
    }()
     c <- true //有緩存時它會把值放到緩存中,愛讀不讀:);不會執行匿名函數,
}
// 即有緩存是異步的,無緩存是同步阻塞的
package main

import (
    "fmt"
    "runtime"
)
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) //使用多核,提升效率
    c:=make(chan bool)
    for i:=0;i<10;i++ {
        go Go(c,i)
    }
    <-c
}
func Go(c chan bool,index int) {
    a:=1
    for i:=0;i<100000000;i++ {
        a+=i
    }
    fmt.Prinln(i,a)
    if index == 9 {
       c <- true
    }
}
// 上例中,在使用多核提交效率時,用最後一個channel的值來判斷結束是不對的
// 導致有些channel沒有執行就退出了
// 要解決這個問題可以使用緩存 ,如下
package main

import (
    "fmt"
    "runtime"
)
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) //使用多核,提升效率
    c:=make(chan bool,10) //有10個channel就創建10個緩存
    for i:=0;i<10;i++ {
        go Go(c,i)
    }
    for i:=0;i<10;i++{
        <-c //取10次channel的值就結束了
    }
}
func Go() {
    a:=1
    for i:=0;i<100000000;i++ {
        a+=i
    }
    fmt.Prinln(i,a)
     c <- true
}
// 第二種解決方案,通過sync包來解決的,通過wait group
package main

import (
    "fmt"
    "runtime"
    "sync"
)
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) //使用多核,提升效率
    // 創建waitGroup來實現,main函數主要是來判斷waitgroup的完成數來結束
    wg:=sync.WaitGroup{}
    wg.Add(10)
    for i:=0;i<10;i++ {
        go Go(wg,i)
    }
   wg.Wait()
}
func Go(wg *sync.WaitGroup,index int) {
    a:=1
    for i:=0;i<100000000;i++ {
        a+=i
    }
    fmt.Prinln(i,a)
    wg.Done() //標記完成一個
}

select

  • 可以處理一個或多個 channel 的發送與接收
  • 同時有多個可用的 channel 時按隨機順序處理
  • 可用空的 select 來阻塞 main 函數
  • 可設置超時
package main

import(
    "fmt"
)

func main() {
    c1,c2:=make(chan int),make(chan string)
    o:=make(chan bool)
    go func(){
        for{
            select{
                case v,ok:=<-c1:
                if !ok {
                    o<-true
                    break
                }
                fmt.Println("c1",v)
                case v,ok:=<-c2
                if !ok{
                    o<-true
                    break
                }
                fmt.Println("c2",v)
            }
        }
    }()
    c1<-1
    c2<-"Hi"
    c1<-3
    c2<-"hello"

    close(c1)
    close(c2)

    <-o
}

將 select 作爲發送者來實現一個應用

package main

import (
    "fmt"
)

func main() {
    c:=make(chan int)
    go func(){
        for v:=range c{
            fmt.Println(v)
        }
    }()
    for{
        select {
            case c<-0:
            case c<-1:
        }
    }
}
// 超時 c一直沒有輸入值
func main() {
    c:=make(chan bool)
    select{
        case v:<-c:
        fmt.Println(v)
        case <-time.After(3*time.Second):
        fmt.Println("")
    }
}

項目

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6714

(0)
Walker的頭像Walker
上一篇 12小時前
下一篇 2025年3月27日 15:01

相關推薦

  • Go工程師體系課 012

    Go 中集成 Elasticsearch 1. 客戶端庫選擇 1.1 主流 Go ES 客戶端 olivere/elastic:功能最全面,API 設計優雅,支持 ES 7.x/8.x elastic/go-elasticsearch:官方客戶端,輕量級,更接近原生 REST API go-elasticsearch/elasticsearch:社區維護的官…

  • Go工程師體系課 005

    微服務開發 創建一個微服務項目,所有的項目微服務都在這個項目中進行,創建joyshop_srv,我們無創建用戶登錄註冊服務,所以我們在項目目錄下再創建一個目錄user_srv 及user_srv/global(全局的對象新建和初始化)user_srv/handler(業務邏輯代碼)user_srv/model(用戶相關的 model)user_srv/pro…

  • Go工程師體系課 007

    商品微服務 實體結構說明 本模塊包含以下核心實體: 商品(Goods) 商品分類(Category) 品牌(Brands) 輪播圖(Banner) 品牌分類(GoodsCategoryBrand) 1. 商品(Goods) 描述平臺中實際展示和銷售的商品信息。 字段說明 字段名 類型 說明 name String 商品名稱,必填 brand Pointer …

  • 編程基礎 0003_Web_beego開發

    Web 開發之 Beego 使用 go get 安裝 bee 工具與 beego Bee Beego 使用 bee 工具初始化 Beego 項目 在$GOPATH/src 目錄下執行 bee create myapp 使用 bee 工具熱編譯 Beego 項目 在$GOPATH/src/myapp 目錄下執行 bee start myapp // hello…

    後端開發 15小時前
    200
  • Go工程師體系課 008

    訂單及購物車 先從庫存服務中將 srv 的服務代碼框架複製過來,查找替換對應的名稱(order_srv) 加密技術基礎 對稱加密(Symmetric Encryption) 原理: 使用同一個密鑰進行加密和解密 就像一把鑰匙,既能鎖門也能開門 加密速度快,適合大量數據傳輸 使用場景: 本地文件加密 數據庫內容加密 大量數據傳輸時的內容加密 內部系統間的快速通…

簡體中文 繁體中文 English