Go学习笔记(一)语法

标准库文档:Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国

B站课程:8小时转职Golang工程师(如果你想低成本学习Go语言)

课程作者语雀(首页有更多内容):8小时转职Golang工程师 · 语雀

代码仓库:GitHub - aceld/golang: 《Golang修养之路》本书针对Golang专题性热门技术深入理解,修养在Golang领域深入话题,脱胎换骨。

参考笔记:https://github.com/spongehah/golang-note/blob/main/Golang%E5%85%A5%E9%97%A8/Go%E5%85%A5%E9%97%A8.md

1. 环境

下载编译器

Go官网下载地址:https://golang.org/dl/

Go官方镜像站(推荐):https://golang.google.cn/

# 打印Go语言的环境信息。包括Go的版本、GOROOT(Go的安装位置)、GOPATH(工作目录)等等
go env

修改配置

Go1.14版本之后,都推荐使用go mod模式来管理依赖了,也不再强制我们把代码必须写在GOPATH下面的src目录了,可以在电脑的任意位置编写go代码。

默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct,国内访问不到 https://proxy.golang.org 所以需要换一个PROXY

这里推荐使用https://goproxy.iohttps://goproxy.cn

可以执行下面的命令修改GOPROXY

go env -w GOPROXY=https://goproxy.cn,direct

2. Golang语言特性

优势

极简单的部署方式

  • 可直接编译成机器码

  • 不依赖其他库

  • 直接运行即可部署

静态类型语言

  • 编译的时候检查出来隐藏的大多数问题

语言层面的并发

  • 天生的基因支持

  • 充分的利用多核

强大的标准库

  • runtime系统调度机制

  • 高效的GC垃圾回收

  • 丰富的标准库

进程、线程、Goroutine

底层库、加解密、email、应用构建、文本、debug、输入输出、数据结构算法、日期和时间、数学、文件系统、压缩、测试、数据持久存储与交换、同步机制、网络通信

简单易学

  • 25个关键字

  • C语言简洁基因,内嵌C语法支持

  • 面向对象特征(继承、多态、封装)

  • 跨平台

劣势

1、包管理,大部分包都在github

2、无泛化类型(Golang 1.18+已经支持泛型)

3、所有Excepiton都用Error来处理(比较有争议)

4、对C的降级处理,并非无缝,没有C降级到asm那么完美(序列化问题)

Golang适合做什么

(1)、云计算基础设施领域

代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。

(2)、基础后端软件

代表项目:tidb、influxdb、cockroachdb等。

(3)、微服务

代表项目:go-kit、micro、monzo bank的typhon、bilibili等。

(4)、互联网基础设施

代表项目:以太坊、hyperledger等。

3. 语法

Hello world

// 定义包名。在源文件中非注释的第一行指明这个文件属于哪个包
// package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
package main

// 需要使用 fmt 包(的函数,或其他元素),该包实现了格式化 IO(输入/输出)的函数。
import "fmt"

// main 函数是每一个可执行程序所必须包含的,一般是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
func main() { // go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行。
    /* 简单的程序 万能的hello world */
    fmt.Println("Hello Go")
}

终端运行

# 直接编译并执行
go run 文件名.go
# 也可以 先编译再执行
go build test1_hello.go 
./test1_hello

3.1 变量

一般使用 var 关键字

单变量声明

package main

import "fmt"

// 声明全局变量
var gA int = 100
var gB = "gBtest"

func main() {
    // 方式一:指定变量类型,声明后若不赋值,默认值是0。
    var a int
    fmt.Println("a = ", a)
    fmt.Printf("type of a = %T\n", a)

    // 方式二:声明变量同时初始化值
    var b int = 10
    fmt.Println("b = ", b)
    fmt.Printf("type of b = %T\n", b)

    // 方式三:省略数据类型,自动匹配类型
    var c = 20
    fmt.Println("c = ", c)
    fmt.Printf("type of c = %T\n", c)
    var cc = "abcd"
    fmt.Printf("cc = %s, type of c = %T\n", cc, cc)

    // 方式四(最常用):省略var关键字,但只能用于在函数体内声明变量,不可用于声明全局变量
    d := 3.14
    fmt.Println("d = ", d)
    fmt.Printf("d = %f\n", d)

    // 使用全局变量
    fmt.Println("gA = ", gA, ", gB = ", gB)
}

多变量声明

package main

import "fmt"

func main() {
    // 多变量声明 - 单行写法
    var xx, yy int = 100, 200
    fmt.Println("xx = ", xx, ", yy = ", yy)
    var kk, ll = 100, "lltest"
    fmt.Println("kk = ", kk, ", ll = ", ll)

    // 多变量声明 - 多行写法
    // 但这种分解的写法一般用于声明全局变量
    var (
        vv int  = 100
        jj bool = true
    )
    fmt.Println("vv = ", vv, ", jj = ", jj)

}

注意

    _, value := 7, 5 // 实际上7的赋值被废弃,变量 _ 不具备读特性
    //fmt.Println(_) // 变量 _ 是读不出来的
    fmt.Println(value) //5

3.2 常量

使用 const 关键字

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

// 常量的定义格式,可省略类型说明符 [type],因为编译器可根据变量的值来推断其类型
const identifier [type] = value
// 显式定义
const b string = "abc"
// 隐式定义
const b = "abc"
// 多重赋值
const a, b, c = 1, false, "str"

常量可以用作枚举

const (
    Unknown = 0
    Female = 1
    Male = 2
)

常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过

package main

import "unsafe"

const (
    a = "abc"
    b = len(a)
    // unsafe.Sizeof(a)的结果是16。字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
    c = unsafe.Sizeof(a)
)

func main(){
    // 输出结果为:abc, 3, 16
    println(a, b, c)
}

3.3 iota

iota [aɪˈəʊtə] 极少量;微量;希腊字母表的第9个字母

iota 只能够配合const() 一起使用, iota只有在const进行累加效果。

自增长

使用iota标示符,可简化常量用于增长数字的定义。

const (
    CategoryBooks = iota // 0
    CategoryHealth       // 1
    CategoryClothing     // 2
)

表达式

type Allergen int
const (
    IgEggs Allergen = 1 << iota         // 1 << 0 which is 00000001
    IgChocolate                         // 1 << 1 which is 00000010
    IgNuts                              // 1 << 2 which is 00000100
    IgStrawberries                      // 1 << 3 which is 00001000
    IgShellfish                         // 1 << 4 which is 00010000
)

type ByteSize float64
const (
    _           = iota                   // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)       // 1 << (10*1)
    MB                                   // 1 << (10*2)
    GB                                   // 1 << (10*3)
    TB                                   // 1 << (10*4)
    PB                                   // 1 << (10*5)
    EB                                   // 1 << (10*6)
    ZB                                   // 1 << (10*7)
    YB                                   // 1 << (10*8)
)

// 把两个常量定义在一行的时
const (
    Apple, Banana = iota + 1, iota + 2    // Apple:1 Banana:2
    Cherimoya, Durian    // Cherimoya:2 Durian:3
    Elderberry, Fig    // Elderberry:3 Fig:4
)

demo

package main

import "fmt"

// const 来定义枚举类型
const (
    // 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0
    BEIJING  = 10 * iota // iota = 0
    SHANGHAI             // iota = 1
    SHENZHEN             // iota = 2
)

const (
    a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2
    c, d                      // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3
    e, f                      // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4

    g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9
    i, k                      // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)

func main() {
    // 常量(只读属性)
    const length int = 10

    fmt.Println("length = ", length)

    // 常量不允许修改
    // length = 100

    fmt.Println("BEIJIGN = ", BEIJING)
    fmt.Println("SHANGHAI = ", SHANGHAI)
    fmt.Println("SHENZHEN = ", SHENZHEN)

    fmt.Println("a = ", a, "b = ", b)
    fmt.Println("c = ", c, "d = ", d)
    fmt.Println("e = ", e, "f = ", f)

    fmt.Println("g = ", g, "h = ", h)
    fmt.Println("i = ", i, "k = ", k)

    // iota 只能够配合const() 一起使用, iota只有在const进行累加效果。
    // var a int = iota

}

3.4 函数

使用 func 关键字定义函数,Go的函数可以有多个返回值

使用 func() {}() 来声明并调用一个匿名函数

package main

import "fmt"

// 例1 简单的
func foo1(a string, b int) int {
    fmt.Println("a = ", a)
    fmt.Println("b = ", b)

    c := 100

    return c
}

// 例2 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
    fmt.Println("a = ", a)
    fmt.Println("b = ", b)

    return 666, 777
}

// 例3 返回多个返回值,有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
    fmt.Println("---- foo3 ----")
    fmt.Println("a = ", a)
    fmt.Println("b = ", b)

    // r1 r2 初始化默认的值是0
    // r1 r2 作用域空间 是foo3 整个函数体的{}空间
    fmt.Println("r1 = ", r1)
    fmt.Println("r2 = ", r2)

    // 给有名称的返回值变量赋值
    r1 = 1000
    r2 = 2000

    return
}

// 例4 在例3的基础上 返回类型都是int可以合并
func foo4(a string, b int) (r1, r2 int) {
    fmt.Println("---- foo4 ----")
    fmt.Println("a = ", a)
    fmt.Println("b = ", b)

    // 给有名称的返回值变量赋值时也可以这样直接返回
    return 1000, 2000
}

func main() {
    c := foo1("abc", 555)
    fmt.Println("c = ", c)

    ret1, ret2 := foo2("haha", 999)
    fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)

    ret1, ret2 = foo3("foo3", 333)
    fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)

    ret1, ret2 = foo4("foo4", 444)
    fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
}

值传递

Go 语言默认使用的是值传递,即在调用过程中不会影响到实际参数。

package main

import "fmt"

func changeValue(p int) {
    p = 10
}
func main() {
    var a int = 1
    changeValue(a)
    fmt.Println("a =", a) //a= 1
}

引用传递

package main

import "fmt"

func changeValue(p *int) {
    *p = 10
}
func main() {
    var a int = 1
    changeValue(&a)
    fmt.Println("a =", a)
}

3.5 import导包与init方法

请添加图片描述

│  main.go
│  
├─lib1
│      Lib1.go
│      
└─lib2
       Lib2.go

新版本需要关闭go mod:go env -w GO111MODULE=off

Lib1.go

package lib1

import "fmt"

// 模块中要导出的函数,必须首字母大写
// 当前lib1包提供的API
func Lib1Test() {
    fmt.Println("lib1Test()...")
}

func init() {
    fmt.Println("lib1.init() ...")
}

Lib2.go

package lib2

import "fmt"

// 模块中要导出的函数,必须首字母大写
// 当前lib2包提供的API
func Lib2Test() {
    fmt.Println("lib2Test()...")
}

func init() {
    fmt.Println("lib2.init() ...")
}

Main.go

package main

import (
    "GolangStudy/5-init/lib1"
    "GolangStudy/5-init/lib2"
)

func main() {
    lib1.Lib1Test()
    lib2.Lib2Test()
}

import导包的三种方式

import _ "fmt":给fmt包起一个别名,匿名,无法使用该包的方法,但是会执行该的包内部的init()方法

import aa "fmt":给fmt包起一个别名,aa,aa.println()直接调用

import . "fmt":将fmt包中的全部方法,导入到当前本包的作用域中,fmt包中的全部的方法可以直接使用API来调用,不需要fmt.API来调用。但遇到两个包内方法同名的情况会报错。

3.6 defer延迟调用

defer用于注册延迟调用,这些调用在 return 后被执行。

可以用做【资源清理(关闭文件、释放锁、断开连接等)、捕捉处理异常、输出日志】等。

多个defer的执行顺序:LIFO(先进后出,栈的特性)。

package main

import "fmt"

// 多个defer的执行顺序
func main() {
    defer fmt.Println("main::end1")
    defer fmt.Println("main::end2")

    fmt.Println("hello! go1")
    fmt.Println("hello! go2")
}
// 输出顺序
// hello! go1
// hello! go2
// main::end2
// main::end1

recover错误拦截

运行时panic异常一旦被引发就会导致程序崩溃。

Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。

注意:recover只有在defer调用的函数中有效。

package main

import "fmt"

func Demo(i int) {
    // 定义10个元素的数组
    var arr [10]int
    // 错误拦截要在产生错误前设置
    defer func() {
        // 设置recover拦截错误信息
        err := recover()
        // 产生panic异常 打印错误信息
        if err != nil {
            fmt.Println(err)
        }
    }()
    // 根据函数参数为数组元素赋值
    // 如果i的值超过数组下标 会报错误:数组下标越界
    arr[i] = 10

}

func main() {
    Demo(10)
    //产生错误后 程序继续
    fmt.Println("程序继续执行...")
}

输出结果

runtime error: index out of range [10] with length 10
程序继续执行...

3.7 slice

固定长度的数组

package main

import "fmt"

func main() {
    // 固定长度的数组
    var myArray1 [10]int
    // for i := 0; i< 10; i++ {
    for i := 0; i < len(myArray1); i++ {
        // 会打印出10个为0的元素
        fmt.Println(myArray1[i])
    }

    // 如果是数组或切片这种动态数组,range会返回两个值,第一个值是当前元素所在的下标,第二个值是当前元素的值本身
    myArray2 := [6]int{1, 2, 3, 4}
    for index, value := range myArray2 {
        // index =  0 value =  1
        // index =  1 value =  2
        // index =  2 value =  3
        // index =  3 value =  4
        // index =  4 value =  0
        // index =  5 value =  0
        fmt.Println("index = ", index, "value = ", value)
    }

    // myArray1 types = [10]int
    // myArray2 types = [6]int
    fmt.Printf("myArray1 types = %T\n", myArray1)
    fmt.Printf("myArray2 types = %T\n", myArray2)
}

range会根据遍历的不同的集合来返回不同的值

固定长度的数组在用函数形参接收时,长度也得一样func printArray(myArray [6]int),并且是值拷贝

动态数组(切片slice)

切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组")

与数组相比,切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

package main

import "fmt"

func printArray(myArray []int) {
    // 引用传递
    // _ 表示匿名的变量
    for _, value := range myArray {
       fmt.Println("value = ", value)
    }

    // 引用传递,修改起作用
    myArray[0] = 100
}

func main() {
    // 动态数组,切片 slice
    myArray := []int{1, 2, 3, 4}
    // myArray type is []int
    fmt.Printf("myArray type is %T\n", myArray)

    printArray(myArray)

    fmt.Println(" ==== ")

    for _, value := range myArray {
       fmt.Println("value = ", value)
    }
}

定义切片

var identifier []type

  1. 直接声明并初始化赋值:slice1 := []int{1, 2, 3}
  2. 只声明:var slice1 []int,一个切片在未初始化时默认为nil,长度为 0
  3. 声明并分配空间:var slice1 []int = make([]int, 3)
  4. 声明并分配空间,通过:=推导:slice1 := make([]int, 3)
package main

import "fmt"

func main() {
    // 1 - 声明slice1是一个切片,并初始化,默认值是1,2,3。 长度len是3
    // slice1 := []int{1, 2, 3}

    // 2 - 声明slice1是一个切片,但没给分配空间。一个切片在未初始化时默认为 nil,长度为 0
    var slice1 []int
    // slice1 = make([]int, 3) // 开辟3个空间 ,默认值是0

    // 3 - (将方式2合并步骤)声明slice1是一个切片,同时给slice分配3个空间,初始化值是0
    // var slice1 []int = make([]int, 3)

    // 4 - 声明slice1是一个切片,同时给slice分配空间,3个空间,初始化值是0, 通过:=推导出slice是一个切片
    // slice1 := make([]int, 3)

    fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)

    // 判断一个silce是否为0
    if slice1 == nil {
        fmt.Println("slice1 是一个空切片")
    } else {
        fmt.Println("slice1 是有空间的")
    }
}

切片追加append()与扩容(涉及len和cap)

make(type, len, capacity)这个函数,当只传两个参数时,capacity = len。

numbers := make([]int, 3, 5)声明一个长度为3,容量为5的切片。

通过numbers = append(numbers, 1)向切片中追加元素,直到容量cap。

容量cap已满后,若继续向切片中追加元素,cap将会扩容为原来的2倍

append()可以同时追加多个元素append(numbers, 2,3,4)

package main

import "fmt"

func main() {
    var numbers = make([]int, 3, 5)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

    // 向numbers切片追加一个元素1, numbers len = 4, [0,0,0,1], cap = 5
    numbers = append(numbers, 1)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

    // 向numbers切片追加一个元素2, numbers len = 5, [0,0,0,1,2], cap = 5
    numbers = append(numbers, 2)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

    // 向一个容量cap已经满的slice 追加元素
    numbers = append(numbers, 3)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

    fmt.Println("--------")
    var numbers2 = make([]int, 3)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
    numbers2 = append(numbers2, 1)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
}

切片的截取

package main

import "fmt"

func main() {
    s := []int{1, 2, 3} //len = 3, cap = 3, [1,2,3]

    // 截取[0, 2)
    s1 := s[0:2]
    fmt.Println(s1)

    s1[0] = 100
    // s和s1的值都会被改掉
    fmt.Println(s)
    fmt.Println(s1)

    // copy 可以将底层数组的slice一起进行拷贝
    s2 := make([]int, 3) // s2 = [0,0,0]

    // 将s中的值 依次拷贝到s2中
    copy(s2, s)
    fmt.Println(s2)
}

其他例子

// 使用make()函数来创建切片
var slice1 []type = make([]type, len)
// 也可以简写为
slice1 := make([]type, len)
// 也可以指定容量,其中capacity为可选参数。len 是数组的长度并且也是切片的初始长度
make([]T, length, capacity)

// 切片初始化
// 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3,其cap=len=3
s := []int {1,2,3}
// 初始化切片s,是数组arr的引用
s := arr[:]
// 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
// 缺省endIndex时将表示一直到arr的最后一个元素
s := arr[startIndex:]
// 缺省startIndex时将表示从arr的第一个元素开始
s := arr[:endIndex]
// 通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
// 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s := make([]int,len,cap)

3.8 map

map和slice类似,只不过是数据结构不同。

map内部是哈希,不是顺序排序的。

定义map

package main

import (
    "fmt"
)

func main() {
    // 声明方式1

    // 声明myMap1是一种map类型 key是string,value是string
    var myMap1 map[string]string
    if myMap1 == nil {
        fmt.Println("myMap1 是一个空map")
    }

    // 在使用map前,需先用make给map分配数据空间
    myMap1 = make(map[string]string, 10)

    myMap1["one"] = "java"
    myMap1["two"] = "c++"
    myMap1["three"] = "python"

    fmt.Println(myMap1)

    // 声明方式2
    myMap2 := make(map[int]string)
    myMap2[1] = "java"
    myMap2[2] = "c++"
    myMap2[3] = "python"

    fmt.Println(myMap2)

    // 声明方式3
    myMap3 := map[string]string{
        "one":   "php",
        "two":   "c++",
        "three": "python",
    }
    fmt.Println(myMap3)
}

打印结果

myMap1 是一个空map
map[one:java three:python two:c++]
map[1:java 2:c++ 3:python]
map[one:php three:python two:c++]

map基本操作

package main

import (
    "fmt"
)

// 遍历map:引用传递
func printMap(cityMap map[string]string) {
    for k, v := range cityMap {
        fmt.Println("k = ", k, ", v = ", v)
    }
}

func main() {
    // 声明
    cityMap := make(map[string]string)

    // 添加
    cityMap["China"] = "Beijing"
    cityMap["Japan"] = "Tokyo"
    cityMap["USA"] = "New York"

    // 遍历
    printMap(cityMap)

    // 删除
    delete(cityMap, "China")

    // 修改
    cityMap["USA"] = "DC"

    fmt.Println("--------")
    printMap(cityMap)

}

3.9 OOP

结构体

type关键字给类型声明别名,声明一种新的数据类型 myint:type myint int

type结合struct关键字可以定义一个结构体,结构体对函数参数的传递默认是值传递,只有显示使用指针时,才是引用传递

// 定义一个结构体
type Book struct {
    title string
    auth  string
}

方法

package main

import "fmt"

// 声明一种行的数据类型 myint, 是int的一个别名
type myint int

// 定义一个结构体
type Book struct {
    title string
    auth  string
}

func changeBook(book Book) {
    // 值传递:传递一个book的副本
    book.auth = "666"
}

func changeBook2(book *Book) {
    // 引用传递:指针传递
    book.auth = "777"
}

func main() {
    var book1 Book
    book1.title = "Golang"
    book1.auth = "zhang3"

    fmt.Printf("%v\n", book1)

    changeBook(book1)
    fmt.Printf("%v\n", book1)

    changeBook2(&book1)
    fmt.Printf("%v\n", book1)
}

方法值和方法表达式

方法值

我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法"值"一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器,只要传入函数的参数即可:

类的封装

上面的结构体其实就是一个类class,只是声明了它的属性,但是没有封装它的方法

  • func (thisClass Class) funcName:thisClass是值传递,是对象的值克隆/拷贝
  • func (thisClass *Class) funcName:thisClass是引用传递,传递的是对象的地址

类名、属性名、方法名 首字母大写表示对外(其他包)开放访问,否则只能在本包内访问。

当调用hero.SetName1()时相当于SetName1(hero),实参和形参都是类型Hero,可以接受。此时在SetName1()中的thisHero只是参数hero的值拷贝,所以SetName1()的修改不影响main中的hero变量。

当调用hero.SetName2()=>SetName2(hero),这是将Hero类型传给了*Hero类型,go可能会取Hero的地址传进去:SetName2(&hero)。所以 SetName2() 的修改可以影响main中的hero变量。

Hero类型的变量这两个方法都是拥有的。

package main

import "fmt"

// 类名首字母大写,表示其他包也能够访问
type Hero struct {
    // 类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
    Name  string
    Ad    int
    level int
}

func (thisHero Hero) Show() {
    // 值传递:thisHero 只是调用该方法的对象的一个副本(拷贝)
    fmt.Println("Name = ", thisHero.Name)
}

// (thisHero Hero)表示绑定到当前对象的Hero结构体
// 方法名大写,代表其它包也能访问
func (thisHero Hero) SetName1(newName string) {
    // 值传递:thisHero 只是调用该方法的对象的一个副本(拷贝)
    thisHero.Name = newName
}

func (thisHero *Hero) SetName2(newName string) {
    // 引用传递:thisHero指向hero对象的地址
    thisHero.Name = newName
}

func main() {
    // 创建一个对象
    hero := Hero{Name: "zhangsan", Ad: 100}
    hero.Show()

    // 调用方法
    hero.SetName2("lisi")
    hero.Show()
}

类的继承

继承可以重写父类的方法,也可以新增额外的属性和方法

package main

import "fmt"

type Human struct {
    name string
    sex  string
}

func (this *Human) Walk() {
    fmt.Println("Human.Walk()...")
}

func (this *Human) Eat() {
    fmt.Println("Human.Eat()...")
}

//=================

type SuperMan struct {
    Human // SuperMan类继承了Human类的方法

    level int
}

// 重定义父类的方法Eat()
func (this *SuperMan) Eat() {
    fmt.Println("SuperMan.Eat()...")
}

//=================

func main() {
    h := Human{"zhang3", "female"}

    h.Eat()
    h.Walk()

    // 定义一个子类对象
    // s := SuperMan{Human{"li4", "female"}, 88}
    var s SuperMan
    s.name = "li4"
    s.sex = "male"
    s.level = 88

    s.Eat() // 子类的方法
}

类的多态

Go语言中,多态性只能通过 接口interface 来实现,不能通过 类class 来实现

Go中interface本质是一个指针,会自动指向对应的实现类

一个类实现了某个接口的条件:该类重写了接口中声明的所有方法,则自动实现了该接口

因为接口本质是一个指针,所以在 声明接口变量后,对接口变量进行实现类的赋值,需要赋值的是对象地址

package main

import "fmt"

// interface本质是一个指针
type AnimalIF interface {
	Sleep()
	GetColor() string // 获取动物的颜色
}

// 具体的类
type Cat struct {
	color string // 猫的颜色
}

func (this *Cat) Sleep() {
	fmt.Println("Cat is Sleep")
}

func (this *Cat) GetColor() string {
	return this.color
}

// 具体的类
type Dog struct {
	color string
}

func (this *Dog) Sleep() {
	fmt.Println("Dog is Sleep")
}

func (this *Dog) GetColor() string {
	return this.color
}

func showAnimal(animal AnimalIF) {
	animal.Sleep() // 多态
	fmt.Println("color = ", animal.GetColor())
}

func main() {

	var animal AnimalIF // 接口的数据类型,父类指针

	animal = &Cat{"Green"}
	animal.Sleep() // 调用的就是Cat的Sleep()方法,多态的现象

	animal = &Dog{"Yellow"}
	animal.Sleep() // 调用Dog的Sleep方法,多态的现象

	// ---------

	cat := Cat{"Green"}
	dog := Dog{"Yellow"}

	showAnimal(&cat)
	showAnimal(&dog)
}

3.10 interface{}和断言

在Go中,所有数据类型都会实现这个接口:万能数据类型interface{},它可以通过多态传递任何一种数据类型

在Go1.18中,新引入关键字 any,any == interface{}

interface{}、any 类似于Java中的Object

断言 类似于Java中的强制类型转换

断言:

  • 使用 any类型的数据 .(string),可以获得两个变量 value, ok ,分别代表 值 和 是否断言成功
  • 可以在switch中使用 .(type) 来判断其类型

注:断言后,变量的类型并未改变,反射的pair部分会详细讲解

package main

import "fmt"

// interface{}是万能数据类型
// func myFunc(arg interface{}) {
func myFunc(arg any) {
	fmt.Println("myFunc is called...")
	fmt.Println(arg)

	// interface{} 如何区分此时引用的底层数据类型是什么?
	// interface{} 提供了 “类型断言” 的机制
    // 但若断言失败一般会导致panic的发生,所以要在断言前进行一定的判断 value, ok := arg.(string)。ok的值就代表断言成功与否
	value, ok := arg.(string)
	if !ok {
		fmt.Println("arg is not string type")
	} else {
		fmt.Println("arg is string type, value = ", value)
		fmt.Printf("value type is %T\n", value)
	}
}

type Book struct {
	auth string
}

func main() {
	book := Book{"Golang"}

	myFunc(book)
	myFunc(100)
	myFunc("abc")
	myFunc(3.14)
}

断言配合switch使用

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T", t)       // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

3.11 反射 //todo 待补充

基础概念pair

Golang关于类型设计的一些原则

  • 变量:一个变量是一个pair(type, value)

    • type 实际变量类型
      • static type:编码时看见的类型(如int、string)
      • concrete type:运行时系统看见的类型(interface类型)
    • value 实际变量值
  • 类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer

static type在创建变量的时候就已经确定,而concrete type才与反射有关

Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,

反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

interface{}类型的变量,在赋值过程中,pair中的type始终保持不变

即断言后,变量的类型并未改变

以下三个demo说明:pair中的type在赋值过程中保持不变

demo1

package main

import "fmt"

func main() {
	var a string
	// pair<statictype:string, value:"aceld">
	a = "aceld"

	var allType interface{}

	// pair<type:string, value:"aceld">
	allType = a

	str, _ := allType.(string)
	fmt.Println(str)
}

demo2

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 创建类型为*os.File的变量tty。【/dev/tty】表示linux终端,【os.O_RDWR】可读可写
	// tty: pair<type:*os.File, value:"/dev/tty"文件描述符>。tty不管赋值给谁,其pair是不变的。
	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
	if err != nil {
		fmt.Println("open file error", err)
		return
	}

	// r: pair<type: , value: >
	var r io.Reader
	// 将tty赋给一个接口变量r,它的pair在接口变量的连续赋值过程中是不变的
	// r: pair<type:*os.File, value:"/dev/tty"文件描述符>
	r = tty

	// w: pair<type: , value: >
	var w io.Writer
	// 接口变量w的pair与r的pair相同,即使w是空接口类型,pair也是不变的。
	// w: pair<type:*os.File, value:"/dev/tty"文件描述符>
	w = r.(io.Writer)

	w.Write([]byte("HELLO!\n"))
}

demo3

package main

import "fmt"

type Reader interface {
	ReadBook()
}

type Writer interface {
	WriteBook()
}

// 具体类型
type Book struct {
}

func (this *Book) ReadBook() {
	fmt.Println("Read a book.")
}

func (this *Book) WriteBook() {
	fmt.Println("Write a book.")
}

func main() {

	// b: pair<type:Book, value:Book{}地址>
	b := &Book{}

	// r: pair<type: , value: >
	var r Reader
	// r: pair<type:Book, value:Book{}地址>
	r = b

	r.ReadBook()

	var w Writer
	// w: pair<type:Book, value:Book{}地址>
	// 断言有两步:得到动态类型 type,判断 type 是否实现了目标接口。这里断言成功是因为 type 是 Book,而 Book 实现了 Writer 接口
	w = r.(Writer)
	w.WriteBook()
}

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

反射reflect

想直接获取到变量内部的信息,Golang的reflect反射包中提供了reflect.ValueOf()reflect.TypeOf()两种类型(或者说两个方法)可以轻松访问接口变量内容

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}

// ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

// TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

    // type:  float64
	fmt.Println("type: ", reflect.TypeOf(num))
    // value:  1.2345
	fmt.Println("value: ", reflect.ValueOf(num))
}

也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (this User) Call() {
	fmt.Println("user is called ..")
	fmt.Printf("%v\n", this)
}

func DoFiledAndMethod(input interface{}) {
	// 获取input的type
	inputType := reflect.TypeOf(input)
	fmt.Println("inputType is :", inputType.Name())

	// 获取input的value
	inputValue := reflect.ValueOf(input)
	fmt.Println("inputValue is:", inputValue)

	// 通过 type 获取里面的字段
	// 1. 获取interface的reflect.Type,通过Type得到NumField, 即字段的个数 ,进行遍历
	// 2. 通过下标 i 得到每个field,数据类型Type,可以获得该Type的 类型 和 字段名
	// 3. 通过Filed有一个Interface()方法得到 对应的value
	for i := 0; i < inputType.NumField(); i++ {
		field := inputType.Field(i)
		value := inputValue.Field(i).Interface()

		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)

		// 调用方法
		m.Func.Call([]reflect.Value{inputValue})
	}
}

func main() {
	user := User{1, "Aceld", 18}
	DoFiledAndMethod(user)
}

3.12 结构体标签

package main

import (
	"fmt"
	"reflect"
)

type resume struct {
	Name string `info:"name" doc:"我的名字"`
	Sex  string `info:"sex"`
}

func findTag(input any) {
	// 当前结构体的全部元素
	// elem := reflect.TypeOf(input).Elem()
	elem := reflect.TypeOf(input)
	for i := 0; i < elem.NumField(); i++ {
		tagInfo := elem.Field(i).Tag.Get("info")
		tagDoc := elem.Field(i).Tag.Get("doc")
		fmt.Println("tagInfo: ", tagInfo, ", tagDoc: ", tagDoc)
	}
}

func main() {
	var re resume
	// 若使用reflect.TypeOf(input).Elem(),此处要传地址
	// findTag(&re)
	findTag(re)
}

结构体标签在json中的应用

package main

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title  string   `json:"title"`
	Year   int      `json:"year"`
	Price  int      `json:"rmb"`
	Actors []string `json:"actors"`
}

func main() {
	movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "zhangbozhi"}}

	// 编码的过程  结构体---> json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return
	}

	fmt.Printf("jsonStr = %s\n", jsonStr)

	// 解码的过程 jsonstr ---> 结构体
	// jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["xingye","zhangbozhi"]}
	myMovie := Movie{}
	err = json.Unmarshal(jsonStr, &myMovie)
	if err != nil {
		fmt.Println("json unmarshal error ", err)
		return
	}

	fmt.Printf("%v\n", myMovie)
}
  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值