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 mainimport "fmt"// 声明全局变量
var gA int = 100
var gB = "gBtest"func main() {// 方式一:指定变量类型,声明后若不赋值,默认值是0。var a intfmt.Println("a = ", a)fmt.Printf("type of a = %T\n", a)// 方式二:声明变量同时初始化值var b int = 10fmt.Println("b = ", b)fmt.Printf("type of b = %T\n", b)// 方式三:省略数据类型,自动匹配类型var c = 20fmt.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.14fmt.Println("d = ", d)fmt.Printf("d = %f\n", d)// 使用全局变量fmt.Println("gA = ", gA, ", gB = ", gB)
}

多变量声明

package mainimport "fmt"func main() {// 多变量声明 - 单行写法var xx, yy int = 100, 200fmt.Println("xx = ", xx, ", yy = ", yy)var kk, ll = 100, "lltest"fmt.Println("kk = ", kk, ", ll = ", ll)// 多变量声明 - 多行写法// 但这种分解的写法一般用于声明全局变量var (vv int  = 100jj 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 = 0Female = 1Male = 2
)

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

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

3.3 iota

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

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

自增长

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

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

表达式

type Allergen int
const (IgEggs Allergen = 1 << iota         // 1 << 0 which is 00000001IgChocolate                         // 1 << 1 which is 00000010IgNuts                              // 1 << 2 which is 00000100IgStrawberries                      // 1 << 3 which is 00001000IgShellfish                         // 1 << 4 which is 00010000
)type ByteSize float64
const (_           = iota                   // ignore first value by assigning to blank identifierKB 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:2Cherimoya, Durian    // Cherimoya:2 Durian:3Elderberry, Fig    // Elderberry:3 Fig:4
)

demo

package mainimport "fmt"// const 来定义枚举类型
const (// 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0BEIJING  = 10 * iota // iota = 0SHANGHAI             // iota = 1SHENZHEN             // iota = 2
)const (a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2c, d                      // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3e, f                      // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9i, k                      // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)func main() {// 常量(只读属性)const length int = 10fmt.Println("length = ", length)// 常量不允许修改// length = 100fmt.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 mainimport "fmt"// 例1 简单的
func foo1(a string, b int) int {fmt.Println("a = ", a)fmt.Println("b = ", b)c := 100return 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 = 1000r2 = 2000return
}// 例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 mainimport "fmt"func changeValue(p int) {p = 10
}
func main() {var a int = 1changeValue(a)fmt.Println("a =", a) //a= 1
}

引用传递

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

3.5 import导包与init方法

请添加图片描述

│  main.go
│  
├─lib1
│      Lib1.go
│      
└─lib2Lib2.go

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

Lib1.go

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

Lib2.go

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

Main.go

package mainimport ("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 mainimport "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 mainimport "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 mainimport "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 =  0fmt.Println("index = ", index, "value = ", value)}// myArray1 types = [10]int// myArray2 types = [6]intfmt.Printf("myArray1 types = %T\n", myArray1)fmt.Printf("myArray2 types = %T\n", myArray2)
}

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

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

动态数组(切片slice)

切片是对数组的抽象。

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

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

package mainimport "fmt"func printArray(myArray []int) {// 引用传递// _ 表示匿名的变量for _, value := range myArray {fmt.Println("value = ", value)}// 引用传递,修改起作用myArray[0] = 100
}func main() {// 动态数组,切片 slicemyArray := []int{1, 2, 3, 4}// myArray type is []intfmt.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 mainimport "fmt"func main() {// 1 - 声明slice1是一个切片,并初始化,默认值是1,2,3。 长度len是3// slice1 := []int{1, 2, 3}// 2 - 声明slice1是一个切片,但没给分配空间。一个切片在未初始化时默认为 nil,长度为 0var 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是否为0if 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 mainimport "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 = 5numbers = 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 = 5numbers = 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 mainimport "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 mainimport ("fmt"
)func main() {// 声明方式1// 声明myMap1是一种map类型 key是string,value是stringvar myMap1 map[string]stringif 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)// 声明方式2myMap2 := make(map[int]string)myMap2[1] = "java"myMap2[2] = "c++"myMap2[3] = "python"fmt.Println(myMap2)// 声明方式3myMap3 := 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 mainimport ("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 stringauth  string
}

方法

package mainimport "fmt"// 声明一种行的数据类型 myint, 是int的一个别名
type myint int// 定义一个结构体
type Book struct {title stringauth  string
}func changeBook(book Book) {// 值传递:传递一个book的副本book.auth = "666"
}func changeBook2(book *Book) {// 引用传递:指针传递book.auth = "777"
}func main() {var book1 Bookbook1.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 mainimport "fmt"// 类名首字母大写,表示其他包也能够访问
type Hero struct {// 类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问Name  stringAd    intlevel 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 mainimport "fmt"type Human struct {name stringsex  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 SuperMans.name = "li4"s.sex = "male"s.level = 88s.Eat() // 子类的方法
}

类的多态

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

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

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

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

package mainimport "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 mainimport "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 mainimport "fmt"func main() {var a string// pair<statictype:string, value:"aceld">a = "aceld"var allType interface{}// pair<type:string, value:"aceld">allType = astr, _ := allType.(string)fmt.Println(str)
}

demo2

package mainimport ("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 mainimport "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 = br.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 mainimport ("fmt""reflect"
)func main() {var num float64 = 1.2345// type:  float64fmt.Println("type: ", reflect.TypeOf(num))// value:  1.2345fmt.Println("value: ", reflect.ValueOf(num))
}

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

package mainimport ("fmt""reflect"
)type User struct {Id   intName stringAge  int
}func (this User) Call() {fmt.Println("user is called ..")fmt.Printf("%v\n", this)
}func DoFiledAndMethod(input interface{}) {// 获取input的typeinputType := reflect.TypeOf(input)fmt.Println("inputType is :", inputType.Name())// 获取input的valueinputValue := reflect.ValueOf(input)fmt.Println("inputValue is:", inputValue)// 通过 type 获取里面的字段// 1. 获取interface的reflect.Type,通过Type得到NumField, 即字段的个数 ,进行遍历// 2. 通过下标 i 得到每个field,数据类型Type,可以获得该Type的 类型 和 字段名// 3. 通过Filed有一个Interface()方法得到 对应的valuefor 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 mainimport ("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 mainimport ("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"}}// 编码的过程  结构体---> jsonjsonStr, 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)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/410338.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于武汉芯景科技有限公司的多协议收发芯片XJ526(第二篇RS422模式)开发指南(兼容SP526)

一、设置芯片为RS422模式 SP526 包含高度集成的串行收发器。SP526 提供 RS-232 &#xff08;V.28&#xff09;、RS-423 &#xff08;V.10&#xff09;、RS-422 &#xff08;V.11&#xff09; 和 RS-485 的硬件接口模式。接口模式选择通过两个控制引脚D0、D1完成。 我们将D0接…

『功能项目』摄像机跟随角色【07】

我们打开上一篇06新输入系统项目&#xff0c; 本章要做的事情是摄像机跟随主角移动&#xff0c; 给主角增加一个Player标签方便主摄像机查找主角对象 在编辑场景调好角度&#xff0c;选择Main Camera对象按键盘Ctrl Shift F使运行场景与编辑场景相同 新建CameraCtrl脚本代码 …

玄机又成国漫首创!IP与AI融合,凭实力火出圈

现在国漫越来越卷了&#xff0c;不仅卷制作质量&#xff0c;还卷各种花式联动。最近玄机科技和百度文库联合举办的AI漫画大赛圆满结束&#xff0c;这还是国内的IP第一次和AI技术融合&#xff0c;而且产出了不少好作品。下面就一起来看看吧&#xff01; 提到玄机科技&#xff0c…

若依权限控制前端+后端实现思路梳理(PreAuthorize、hasPermi、v-hasPermi)

一、权限控制引发的思考 引言 最近接手了公司的一个项目&#xff0c;实施反馈说&#xff0c;客户那边要求对不同的权限的用户操作权限做限制。场景就是&#xff0c;比如一个项目列表&#xff0c;这部分数据有可能是针对某个公司某个部门的&#xff0c;对应不同的部门用户能看…

【Kotlin设计模式】Kotlin实现装饰器模式

前言 装饰器模式&#xff08;Decorator Pattern&#xff09;&#xff0c;用于动态地为对象添加新功能&#xff0c;而无需修改其结构&#xff0c;通过使用不用装饰类及这些装饰类的排列组合&#xff0c;可以实现不同的功能和效果&#xff0c;但是这样的效果就是会增加很多类&…

Cypress第二次安装遇到的问题

问题一&#xff1a;吐血&#xff0c;谁会想到node.js的官网访问不了呢&#xff01; 中文网站&#xff1a;http://url.nodejs.cn/download/ 官网&#xff1a;https://nodejs.org/zh-cn nodejs安装的两种方法(官网、NVM安装-node版本切换)不知道这种方式是否可行&#xff0c;还…

62. 不同路径 -dp6

. - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/unique-paths/ 输入&#xff1a;m 3, n 2 输出&#xff1a;3 解释&a…

汽车功能安全--TC3xx LBIST触发时机讨论

目录 1. LBIST架构 2. LBIST寄存器配置 3. LBIST触发时机 LBIST&#xff0c;全称Logic Built-in Self Test。 在TC3xx中&#xff0c;LBIST是一种硬件功能安全机制&#xff0c;目的是为了探测MCU内部逻辑电路的潜伏故障(latent faults)。 从使用者角度来看&#xff0c;只需…

基于x86 平台opencv的图像采集和seetaface6的图像质量评估功能

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、开发流程3.1 编写测试3.2 配置资源文件3.3 验证功能一、概述 本文档是针对x86 平台opencv的图像采集和seetaface6的图像质量评估功能,opencv通过摄像头采集视频图像,将采集的视频图像送给seetaface6的图像质量评估模块…

63. 不同路径 II -dp7

63. 不同路径 IIhttps://leetcode.cn/problems/unique-paths-ii/ 输入&#xff1a;obstacleGrid [[0,0,0],[0,1,0],[0,0,0]] 输出&#xff1a;2 解释&#xff1a;3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径&#xff1a; 1. 向右 -> 向右 ->…

对各项数据的统计汇总,集中展示,便于查看厂区情况的智慧物流开源了。

智慧物流视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。构建基于Ai技术的…

关于kafka的分区和消费者之间的关系

消费者和消费者组 当生产者向 Topic 写入消息的速度超过了消费者&#xff08;consumer&#xff09;的处理速度&#xff0c;导致大量的消息在 Kafka 中淤积&#xff0c;此时需要对消费者进行横向伸缩&#xff0c;用多个消费者从同一个主题读取消息&#xff0c;对消息进行分流。 …

yolov8 安装流程

1、克隆远端代码 git clone https://gitcode.com/gh_mirrors/ul/ultralytics.git 2、配置pyshon环境安装 3.10的版本&#xff0c;注意3.12后期会出现标注都在顶部的问题 使用idea可以在项目结构中直接添加python SDK 3、下载依赖&#xff0c;安装8.0210&#xff0c;注意新依赖…

脑靶向肽 ;SHp ;CLEVSRKNC ;缺血归巢肽

【脑靶向肽 SHp 简介】 SHp多肽是一种抗肿瘤多肽&#xff0c;它可以通过激活P53基因&#xff0c;调节细胞凋亡相关基因的蛋白表达&#xff0c;从而抑制肿瘤细胞的增殖并诱导细胞凋亡。在最新的研究中&#xff0c;SHp多肽被发现可以促进T细胞对肿瘤细胞的杀伤作用&#xff0c;显…

Vue3源码调试-第三篇

前言 上两篇已经调试完packages/runtime-dom/src/index.ts下的createApp函数的第一行了&#xff0c;接下来我们看下一行 injectNativeTagCheck 首先说下这个__DEV__估计也是定义在dev.js下&#xff0c;又或者是哪里的&#xff0c;这里控制台输出是true&#xff0c;那我估计是…

深入解析css-学习小结

绪论 盒模型 层叠 优先级 继承 层叠 层叠指规则冲突时&#xff0c;如何选择规则。规则冲突解决顺序&#xff1a; 样式表来源 用户代理样式 用户代理样式&#xff1a;浏览器默认样式 作者样式表&#xff1a;你自己写的css样式 作者样式表会覆盖用户代理样式&#xff0c;因…

Java 入门指南:Java IO流 —— 字节流

何为Java流 Java 中的流&#xff08;Stream&#xff09; 是用于在程序中读取或写入数据的抽象概念。流可以从不同的数据源&#xff08;输入流&#xff09;读取数据&#xff0c;也可以将数据写入到不同的目标&#xff08;输出流&#xff09;。流提供了一种统一的方式来处理不同…

2024.8.27 作业

1> 提示并输入一个字符串&#xff0c;统计该字符串中字母个数、数字个数、空格个数、其他字符的个数 #include <iostream>using namespace std;int main() {string s;cout << "请输入字符串>>>";getline(cin,s);int letter0,digit0,blank0,…

华为eNSP:静态路由配置、浮动路由配置

静态路由&#xff1a; 一、拓扑图 二、路由器配置 2.1&#xff1a;配置接口 R1&#xff1a; [r1]int g0/0/0 [r1-GigabitEthernet0/0/0]ip add 192.168.1.254 24 [r1-GigabitEthernet0/0/0]qu [r1]int g0/0/1 [r1-GigabitEthernet0/0/1]ip add 10.1.1.1 24 [r1-GigabitEth…

CMake之PUBLIC、PRIVATE、INTERFACE

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~ 个人主页&#xff1a; rainInSunny | 个人专栏&#xff1a; C那些事儿、 Qt那些事儿 文章目录 写在前面抽象版解释头文件和链接库传递测试代码结构PUBLIC传递PRIVATE传递INTERFACE传递 写在前面 使用CMake必然离不开target_include_dir…