二、基本数据类型

go

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。

整形

分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型

uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 表示 v 的值等于 123456。

浮点型

float32和float64

布尔值 bool

布尔类型变量的默认值为false。
Go 语言中不允许将整型强制转换为布尔型.
布尔型无法参与数值运算,也无法与其他类型进行转换。

复数

complex64和complex128

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
complex64的实部和虚部为32位,complex128的实部和虚部为64位。

字符串

s1 := "hello"
s2 := "你好"
Go语言中要定义一个多行字符串时,就必须使用反引号字符:
s1 := `第一行
第二行
第三行
`
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出

len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
var a = '中'
var b = 'x'
rune类型实际是一个int32


修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))

s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}

指针类型(Pointer)

在 Go 中,指针类型用于存储变量的内存地址。与 C 和 C++ 不同,Go 指针不能进行偏移和运算,因此它们更安全且更易于使用。指针类型以 * 开头,用于指定指针类型的底层类型。例如,*int 表示指向整数类型的指针。在 Go 中,通过 & 运算符可以获取一个变量的内存地址,而通过 * 运算符可以获取指针所指向的变量的值,指针类型的零值为 nil,表示指针不指向任何有效的内存地址。因此,在使用指针之前,应该先进行空指针检查

var x int = 42
var ptr *int = &x // 获取 x 的地址,将其赋值给 ptr
fmt.Println(*ptr) // 输出指针所指向的变量的值,即 42

Array(数组)

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

// 定义一个长度为3元素类型为int的数组a   数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变
var 数组变量名 [元素数量]T
var a [3]int

数组的初始化

var testArray [3]int                        //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化

一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}

指定索引值的方式来初始化数组
a := [...]int{1: 1, 3: 5}

数组的遍历

var a = [...]string{"北京", "上海", "深圳"}

// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}

多维数组

多维数组只有第一层可以使用...来让编译器推导数组长度。

二维数组
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}

for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}

数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
  2. [n]*T表示指针数组,*[n]T表示数组指针 。

切片(slice)

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片声明

切片是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

  1. 切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
  2. 切片长度=high-low,容量等于得到的切片的底层数组的容量。0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)
  3. 要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。
  4. 切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素
var name []T

简单切片表达式
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
a[2:] // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]

完整切片表达式
a[low : high : max]
它会将得到的结果切片的容量cap设置为max-low

使用make()函数构造切片

make([]T, size, cap)
  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

多维切片

四维数组
var Sites [][][][]int
for i := 0; i < DataInfo.X; i++ {
var first [][][]int
for j := 0; j < DataInfo.Y; j++ {
var secend [][]int
for k := 0; k < DataInfo.Z; k++ {

third := make([]int, 2)
secend = append(secend, third)
}
first = append(first, secend)
}
Sites = append(Sites, first)
}

切片操作

切片遍历
for index, value := range s {
fmt.Println(index, value)
}
append()方法为切片添加元素
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

copy(destSlice, srcSlice []T)

srcSlice: 数据来源切片
destSlice: 目标切片


a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)

扩容

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

map

map声明

map[KeyType]ValueType
  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100

判断某个键是否存在

value, ok := map[key]

v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}

map操作

遍历
for k, v := range scoreMap {
fmt.Println(k, v)
}
遍历map时的元素顺序与添加键值对的顺序无关


delete(map, key)
delete(scoreMap, "小明")//将小明:100从map中删除

元素为map类型的切片
var mapSlice = make([]map[string]string, 3)
mapSlice[0]["address"] = "沙河"

值为切片类型的map
var sliceMap = make(map[string][]string, 3)
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}

结构体Struct

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

自定义类型

//将MyInt定义为int类型
type MyInt int

//类型别名
type TypeAlias = Type
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

//在Go语言中并没有类的概念,而是使用结构体来实现类似的功能
type MyClass struct {
x int
}
func (m *MyClass) setX(val int) {
m.x = val
}

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

var 结构体实例 结构体类型

type person struct {
name string
city string
age int8
}

func main() {
var p1 person
p1.name = "沙河娜扎"
p1.city = "北京"
p1.age = 18
}

在定义一些临时数据结构等场景下还可以使用匿名结构体
var user struct{Name string; Age int}


创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针,需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法糖。

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。结构体占用一块连续的内存.* 空结构体是不占用空间的。


//使用键值对初始化,,当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p5 := person{
name: "小王子",
city: "北京",
age: 18,
}

//使用值的列表初始化
p8 := &person{
"沙河娜扎",
"北京",
28,
}

构造函数

Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

p9 := newPerson("张三", "沙河", 90)

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。
//Person 结构体
type Person struct {
name string
age int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}

//Dream Person做梦的方法
//值类型的接收者
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}

//指针类型的接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}


什么时候应该使用指针类型接收者
  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

嵌套结构体

//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
}



//嵌套匿名字段
Address //匿名字段
user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名

结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

//Animal 动物
type Animal struct {
name string
}

func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}

结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问).

结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)

//JSON反序列化:JSON格式的字符串-->结构体
err = json.Unmarshal([]byte(str), c1)

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}

func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

结构体和方法补充知识点

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意.

正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。

函数Func

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

Go语言中定义函数使用func关键字,具体格式如下:

func 函数名(参数)(返回值){
函数体
}

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。
  • 函数的参数和返回值都是可选的
  • 函数的参数中如果相邻变量的类型相同,则可以省略类型
可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

注意:可变参数通常要作为函数的最后一个参数。

返回值

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

返回值命名 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

return nil // 没必要返回[]int{}
函数进阶

局部变量和全局变量重名,优先访问局部变量。

函数类型与变量

定义函数类型

我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x, y int) int {
return x + y
}

func sub(x, y int) int {
return x - y
}

var c calculation
c = add

fmt.Println(c(1, 2)) // 像调用add一样调用c
函数作为参数

函数可以作为参数:

func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
函数作为返回值

函数也可以作为返回值:

func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
匿名函数和闭包

匿名函数多用于实现回调函数和闭包

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){
函数体
}

func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数

//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}

var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60

变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}

func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例3:

func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}

sub := func(i int) int {
base -= i
return base
}
return add, sub
}

func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用环境

defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}

func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}


A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4



内置函数介绍
内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

接口interface

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节

在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。

接口类型

type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}
  • 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

实现接口的条件

接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。

我们定义的Singer接口类型,它包含一个Sing方法。

// Singer 接口
type Singer interface {
Sing()
}

我们有一个Bird结构体类型如下。

type Bird struct {}

因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求。

// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}

这样就称为Bird实现了Singer接口。

只要实现了Say()方法都能当成Sayer类型的变量来处理

type Sayer interface {
Say()
}

// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
s.Say()
}

var c cat
MakeHungry(c)
var d dog
MakeHungry(d)

Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码

面向接口编程

只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。

我们可以将具体的支付方式抽象为一个名为Payer的接口类型,即任何实现了Pay方法的都可以称为Payer类型

// Payer 包含支付方法的接口类型
type Payer interface {
Pay(int64)
}

type ZhiFuBao struct {
// 支付宝
}

// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}

type WeChat struct {
// 微信
}

// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}

func Checkout(obj *Payer) {
// 支付100元
obj.Pay(100)
}

func main() {
Checkout(&ZhiFuBao{}) // 之前调用支付宝支付

Checkout(&WeChat{}) // 现在支持使用微信支付
}


接口类型变量

var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say() // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say() // 汪汪汪

值接收者和指针接收者

// Mover 定义一个接口类型
type Mover interface {
Move()
}

// Dog 狗结构体类型
type Dog struct{}

//值接收者实现接口
// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}

//指针接收者实现接口

// Cat 猫结构体类型
type Cat struct{}

// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
ar c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()

由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意

类型与接口的关系

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer接口和Mover接口,具体代码示例如下

// Sayer 接口
type Sayer interface {
Say()
}

// Mover 接口
type Mover interface {
Move()
}

//Dog既可以实现Sayer接口,也可以实现Mover接口。
//同一个类型实现不同的接口互相不影响使用。
var d = Dog{Name: "旺财"}

var s Sayer = d
var m Mover = d

s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法

一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}

接口组合

接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例。

// src/io/io.go

type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}

// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}

// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}

对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。

接口也可以作为结构体的一个字段,我们来看一段Go标准库sort源码中的示例。

// src/sort/sort.go

// Interface 定义通过索引对元素排序的接口类型
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}


// reverse 结构体中嵌入了Interface接口
type reverse struct {
Interface
}

// Less 为reverse类型添加Less方法,重写原Interface接口类型的Less方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}

在这个示例中还有一个需要注意的地方是reverse结构体本身是不可导出的(结构体类型名称首字母小写),sort.go中通过定义一个可导出的Reverse函数来让使用者创建reverse结构体实例。

func Reverse(data Interface) Interface {
return &reverse{data}
}

这样做的目的是保证得到的reverse结构体中的Interface属性一定不为nil,否者r.Interface.Less(j, i)就会出现空指针panic。

空接口

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。

package main

import "fmt"

// 空接口

// Any 不包含任何方法的空接口类型
type Any interface{}

// Dog 狗结构体
type Dog struct{}

func main() {
var x Any

x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}

通常我们在使用空接口类型时不必使用type关键字声明,可以像下面的代码一样直接使用interface{}

var x interface{}  // 声明一个空接口类型变量x
  1. 空接口作为函数的参数 func show(a interface{})
  2. 空接口作为map的值 var studentInfo = make(map[string]interface{})

接口值

由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

类型type nil
值 value nil
m = &Dog{Name: "旺财"}        type  = *dog       value = 旺财

var c *Car
m = c type = *car value = nil

//接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。

类型断言

我们可以借助标准库fmt包的格式化打印获取到接口值的动态类型 ,而fmt包内部其实是使用反射的机制在程序运行时获取到动态类型的名称

var m Mover

m = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dog

m = new(Car)
fmt.Printf("%T\n", m) // *main.Car


//接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。
x.(T)

x:表示接口类型的变量
T:表示断言x可能是的类型。

v, ok := n.(*Dog)

switch v := x.(type)

Error 接口

Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式。

type error interface {
Error() string
}

error 是一个接口类型,默认零值为nil。所以我们通常将调用函数返回的错误与nil进行比较,以此来判断函数是否返回错误.

创建错误

errors.New("无效的id")

fmt.Errorf("查询数据库失败,err:%v", err)

错误结构体类型

此外我们还可以自己定义结构体类型,实现``error`接口。

// OpError 自定义结构体类型
type OpError struct {
Op string
}

// Error OpError 类型实现error接口
func (e *OpError) Error() string {
return fmt.Sprintf("无权执行%s操作", e.Op)
}

补充

new和make

使用 newmake 可以创建新的对象或数据结构

new 函数用于分配一块新的内存,并将其初始化为零值,返回一个指向这块内存的指针。可以用于任何数据类型,但是并没有给这块内存赋值,因此在使用前需要进行赋值。

// 声明一个指向 int 类型的指针
var p *int
p = new(int) // 分配一块新的内存,并将其初始化为 0,p 指向这块内存
*p = 123 // 给这块内存赋值

make 函数则用于分配并初始化一个引用类型的对象(如 slicemapchannel)。返回的是一个该类型的对象而非指针,因为这些对象本身就是引用类型,即指向某个底层数据结构的指针。make 函数会为这些对象分配内存,初始化其内部字段,最后返回该对象。

// 声明一个长度为 5 的 int 类型切片,初始值为 0
s := make([]int, 5)

// 声明一个容量为 10 的 int 类型切片,长度为 2,初始值为 0
s := make([]int, 2, 10)

// 声明一个 map,初始化为空
m := make(map[string]int)

// 声明一个 channel,容量为 10
c := make(chan int, 10)
  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

python

在Python中常见的数据类型有以下8个类型,分别是:int,整数类型(整形)、float,浮点类型(浮点型)、bool,布尔类型、str,字符串类型、list,列表类型、tuple,元组类型、dict,字典类型、set,集合类型

1  int
Python 中的整数没有长度限制,不像其他编程语言有 int,smallint,short,long,longint,long 等

十进制就不说了,正常的写法
十六进制写法:加前缀 0x,出现 0-9 和 A-F 的数字和字母组合
八进制写法:加前缀 0o,出现 0-7 数字组合
二进制写法:加前缀 0b,只有 0 和 1 数字组合

2 浮点数 float

a. 浮点数只能以十进制表示,不能加前缀,否则会报语法错误
浮点数 有长度限制 边界值为:
max=1.7976931348623157e+308 min=2.2250738585072014e-308

3 布尔值 bool

布尔值就是我们常说的逻辑,可以理解为对或错
print(100 == 100.0)

4 复数 complex

# Python 中的复数这样来表示: 1 + 1j 虚部为 1,仍不可省略
print((1 + 2j).real) # 输出实部 float 类型
print((1 + 2j).imag) # 输出虚部 float 类型

5 字符串 str
通俗来说,字符串就是字符组成的一串内容,Python 中用成对的单引号或双引号括起来,用三个单引号或双引号可以使字符串内容保持原样输出,可以包含回车等特殊字符,在 Python 中字符串是不可变对象
Python 中用反斜杠 “\” 来转义字符

6 列表 list
ls = [1, 2, 3, 4, 'a', 'b', [8, 5, 7]]
for i in ls:
print(i)

检查列表中是否存在某个元素
使用 in 关键字,返回值为布尔值
del ls

类类型

class MyClass:
x = 0
def setX(self, val):
self.x = val

c++ 类型

typedef 声明

typedef type newname; 

例如,下面的语句会告诉编译器,feet 是 int 的另一个名称:

typedef int feet;

现在,下面的声明是完全合法的,它创建了一个整型变量 distance:

feet distance;

整型

int: 整数类型,通常为 32 位,可表示范围为 -2^312^31-1
short: 短整数类型,通常为 16 位,可表示范围为 -2^152^15-1
long: 长整数类型,通常为 32 位或 64 位,可表示范围为 -2^312^31-1-2^632^63-1
long long: 长长整数类型,通常为 64 位,可表示范围为 -2^632^63-1
unsigned int: 无符号整数类型,通常为 32 位,可表示范围为 02^32-1
unsigned short: 无符号短整数类型,通常为 16 位,可表示范围为 02^16-1
unsigned long: 无符号长整数类型,通常为 32 位或 64 位,可表示范围为 02^32-102^64-1
unsigned long long: 无符号长长整数类型,通常为 64 位,可表示范围为 02^64-1

浮点型

float: 单精度浮点型,通常为 32 位,可表示范围为 1.17549e-38 到 3.40282e+38,精度为 6 位小数
double: 双精度浮点型,通常为 64 位,可表示范围为 2.22507e-308 到 1.79769e+308,精度为 15 位小数
long double: 长双精度浮点型,通常为 80 位或 128 位,可表示范围和精度比 double 更高

字符型

char: 字符类型,通常为 8 位,可表示 ASCII 码的字符,例如 'A'、'B'、'C' 等
char16_t: Unicode 字符类型,通常为 16 位
char32_t: Unicode 字符类型,通常为 32 位
wchar_t: 宽字符类型,通常为 16 位或 32 位,用于支持多语言字符集

布尔型

bool: 布尔类型,通常为 1 位,可表示 true 或 false

复数

在 C++ 标准库中,复数类型是通过 std::complex 实现的,它定义在 <complex> 头文件中。std::complex 是一个模板类,它接受一个模板参数表示元素类型,可以是 float、double、long double 等

#include <complex>
std::complex<double> z1(1.0, 2.0); // 定义并初始化一个复数
// 访问实部和虚部
std::cout << "real(z1) = " << z1.real() << std::endl;
std::cout << "imag(z1) = " << z1.imag() << std::endl;

枚举类型(enum)

枚举类型是一种用户自定义的类型,用于定义一些有限的命名值。例如,我们可以使用枚举类型定义一些颜色:

enum Color {
Red,
Green,
Blue
};

Color c = Green;

指针类型(pointer)

指针类型是一种保存了内存地址的变量类型。指针变量通常用于动态内存分配、函数调用等方面。例如,下面的代码中,我们定义了一个指针变量 p,并将它指向一个整型变量 x 的地址:

int x = 10;
int* p = &x;

引用(reference)

在 C++ 中,引用是一种轻量级的指针,它提供了访问变量的另一种方式,它是某个变量的别名。引用通常用于函数参数、返回值和赋值。引用的语法使用 & 符号。引用和指针类似,它们都提供了对变量的间接访问。但是,引用比指针更加安全,因为它们不会出现空指针的情况。在定义引用时必须初始化它,否则会出现编译错误。另外,引用一旦初始化后,就不能再指向其他变量,因此,引用可以被视为常量指针。

int a = 5;
int& b = a; // 声明 b 为 a 的引用

b = 10; // 修改 b 也会修改 a
cout << a << endl; // 输出 10

数组类型(array)

数组类型用于保存一组相同类型的数据,可以用下标访问数组中的元素。例如,下面的代码中,我们定义了一个数组 a,包含了三个整型元素:

int a[3] = {1, 2, 3};

int x = a[0]; // x = 1

结构体类型(struct)

结构体类型可以用于组合多个不同类型的变量,形成一个新的类型。例如,下面的代码中,我们定义了一个结构体 Person,包含了两个成员变量 nameage

struct Person {
std::string name;
int age;
};

Person p = {"Tom", 18};

共用体类型(union)

共用体类型可以让多个不同的变量共用一段内存空间,用于节省内存。例如,下面的代码中,我们定义了一个共用体 Number,可以表示一个整型数、一个浮点数或一个字符:

union Number {
int i;
float f;
char c;
};

类类型(class)

在C++中,类是一种用户自定义的数据类型,可以包含数据成员、成员函数等元素。使用class关键字定义类

类是一种用户自定义的数据类型,它可以封装数据和方法。类定义了一组相关的数据和方法,它们通常是一些有意义的操作的集合。C++中的类可以看作是一种数据类型的定义方式,类的实例化(对象)是具体的这种数据类型的实现.

类是面向对象编程(OOP)的基础,其中面向对象的思想主要体现在封装、继承和多态性方面。类可以使用访问修饰符(public、protected、private)来限制成员变量和成员函数的访问权限。类还可以包含构造函数、析构函数、静态成员、常量成员函数等特殊成员函数。

class Person {
private:
int age;
public:
void setAge(int a) {
age = a;
}
int getAge() {
return age;
}
};

Person p;
p.setAge(25);
int age = p.getAge(); // age = 25

继承权限
Shape
public 成员:任何地方都可以访问,包括类的外部和派生类。
protected 成员:只能在类内部和派生类中访问,不能在类外部访问。
private 成员:只能在类内部访问,不能在类外部和派生类中访问。

class Rectangle: public Shape{}

public:派生类可以访问基类中的公共成员,但不能访问基类的私有成员和受保护成员。
protected:派生类可以访问基类中的公共成员和受保护成员,但不能访问基类的私有成员。
private:派生类不能直接访问基类中的任何成员,包括公共成员、受保护成员和私有成员。

模板类型

模板类型(template)是 C++ 中非常重要的一种数据类型,它可以用来定义通用的数据类型或函数。模板类型分为类模板和函数模板两种。

类模板

类模板可以用来定义通用的类,例如标准库中的容器类模板 std::vector 和 std::map,它们可以用来存储任何类型的数据,类模板是用来定义类的蓝图其中某些成员的类型不确定,而是用类型参数来表示。类型参数可以在使用类模板时指定,从而让编译器根据指定的类型生成对应的类代码如下所示:

template<typename T>
class Stack {
private:
T* data;
int top;
int capacity;
public:
Stack(int capacity) : data(new T[capacity]), top(-1), capacity(capacity) {}
~Stack() { delete[] data; }
void push(const T& value) { data[++top] = value; }
T pop() { return data[top--]; }
bool empty() const { return top == -1; }
bool full() const { return top == capacity - 1; }
};


Stack<int> intStack(10);
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << intStack.pop() << std::endl; // 输出 3

在这里,我们实例化了一个 Stack 类,并将其元素类型指定为 int,然后调用了它的 push 和 pop 函数。
函数模板

函数模板可以用来定义通用的函数,例如标准库中的算法函数 std::sort 和 std::find,它们可以用来操作任何类型的数据,函数模板则是用来定义函数的蓝图,其中某些参数或返回值的类型不确定,而是用类型参数来表示,如下所示

template<typename T>
void sort(T* first, T* last) {
// ...
}

template<typename T>
T max(const T& a, const T& b) {
return a > b ? a : b;
}

std::cout << max(1, 2) << std::endl; // 输出 2
std::cout << max(3.14, 2.71) << std::endl; // 输出 3.14

这是因为编译器根据参数的类型生成了对应的函数代码

类型转换

类型转换是将一个数据类型的值转换为另一种数据类型的值。

C++ 中有四种类型转换:静态转换、动态转换、常量转换和重新解释转换。

静态转换(Static Cast)

静态转换是将一种数据类型的值强制转换为另一种数据类型的值。

静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。

静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。

int i = 10;
float f = static_cast<float>(i); // 静态将int类型转换为float类型
动态转换(Dynamic Cast)

动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。

class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针
常量转换(Const Cast)

常量转换用于将 const 类型的对象转换为非 const 类型的对象。

常量转换只能用于转换掉 const 属性,不能改变对象的类型。

const int i = 10;
int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
重新解释转换(Reinterpret Cast)

重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。

int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型

补充

  1. 多态性:C++ 支持静态多态和动态多态,静态多态通过函数重载和运算符重载实现,动态多态通过虚函数实现。
  2. 抽象类:C++ 中可以定义纯虚函数,一个类如果包含纯虚函数,该类就是抽象类,抽象类不能实例化对象,只能被其他类继承。
  3. 友元函数:C++ 中的友元函数可以访问类的私有成员,但不是类的成员函数,友元函数可以定义在类内或类外。
  4. 内联函数:C++ 中的内联函数在函数调用处直接展开,减少函数调用的开销,可以在函数前加 inline 关键字将其声明为内联函数。
  5. 类模板:C++ 中可以定义类模板,用来创建具有不同数据类型的类,类模板可以具有成员函数和成员变量。
  6. 构造函数和析构函数:C++ 中的构造函数用来初始化类的对象,析构函数用来清理对象所占用的资源,构造函数和析构函数都是特殊的成员函数,一个类可以有多个构造函数,但只能有一个析构函数。
  7. 拷贝构造函数和移动构造函数:C++ 中的拷贝构造函数用来复制一个对象到另一个对象,移动构造函数用来移动一个对象到另一个对象,移动构造函数可以更高效地将对象转移,避免了不必要的复制操作。