Golang语法

小凯   |     |   Golang学习笔记   |   142分钟   |   151浏览  

循环与分支

循环模块

根据循环条件重复执行一段代码。有 4 种方式:
方式一(和java中fori一样):

for 初始变量; 循环条件; 步长 {
//循环体代码
}

方式二(和java中while一样):

for 条件 {
//循环体代码
}

方式三(无限循环,类似于java中while(true),需要break):

for {
//循环体代码
}

方式四(使用**range**关键字,迭代循环slice、map、数组、字符串(forr))遍历字符串时,index是字符起始字节的索引。

for index, item := range dataList{
//循环体代码
}

fmt.Printf("当前循环方式为 %v\n", "for init; condition; post { }")
for i := 0; i < 10; i++ {
        fmt.Printf("当前循环%v\n", i)
    }
fmt.Printf("当前循环方式为 %v\n", "for condition { }")
var post2 = 0
for post2 <= 10 {
        fmt.Printf("当前循环%v\n", post2)
        post2++
    }
fmt.Printf("当前循环方式为 %v\n", "for { }")
var post3 = 0
for {
        if post3 > 10 {
            break
        }
        fmt.Printf("当前循环%v\n", post3)
        post3++
    }
fmt.Printf("当前循环方式为 %v\n", "for的range格式")
var str string = "石头是非常硬的,到底多硬呢?坚如磐石。"
for index, item := range str {
        fmt.Printf("循环:%v, %c\n", index, item)
    }
var strArr []string = []string{"数组1", "数组2"}
    for index, item := range strArr {
        fmt.Printf("索引: %v 元素: %v\n", index, item)
    }

goto关键字

跳转至指定行。一般用于嵌套循环,跳转至指定循环行处。与java中break类似。

for i := 1; i <= 9; i++ {
        for j := 1; j <= 9; j++ {
            if j > i {
                fmt.Println("goto跳转")
                goto loop2
            }
            fmt.Printf("%v * %v = %v    ", j, i, i*j)
        }
        loop2:
        fmt.Println()
    }

switch模块

整个结构中,除了使用fallthrough情况下,只会执行一个case目标,若满足其他case不会执行。
switch指定变量时(方式一):

switch 目标 {
case 选项1:
//执行代码1
fallthrouth //此处只会强制执行下一个case或则default代码,无论是否满足条件;其他满足且不属于下一个的case代码不会执行(default在内)
case 选项2,选项3:
//执行代码2、3
default:
//执行默认代码
}

switch无指定变量时(方式二):

switch {
case 布尔表达式1:
//执行代码1
case 布尔表达式2:
//执行代码2
}

type-switch:用于判断某个interface变量中实际存储的变量类型

var animal Animal = new(Dog)
switch animal.(type) {
case Dog:
//执行代码1
case Cat:
//执行代码2
}

select语句

类似于switch的一种控制结构;

  • 只能用于通道操作,每个case必须是一个通道操作,要么发送要么接收
  • select语句会监听所有指定通道上的操作,一旦其中一个通道准备好就会执行相应的代码块
  • 如果多个通道都准备好,那么select会随机执行一个通道操作;如果所有通道都没有准备好,那么select会执行default块中代码,如果没有default块,select会被阻塞,直到有通道能运行。
  • 所有cannel表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果任意某个通道可以进行,它就执行,其他被忽略

语法:
image.png

var c1 chan int16 = make(chan int16)   //创建通道c1
var c2 chan string = make(chan string) //创建通道c2
go func() {
    time.Sleep(1 * time.Second)
    c1 <- 1111
}()
go func() {
    time.Sleep(1 * time.Second)
    c2 <- "two"
}()

for i := 0; i < 2; i++ {
    select {
    case msg1 := <-c1:
        fmt.Println("received", msg1)
    case msg2 := <-c2:
        fmt.Println("received", msg2)
    }
}

分支模块

根据条件执行相应的代码,if-else结构中只会执行一个代码块内容,无特殊情况。

if 条件1 {
//执行代码1
} else if 条件2 {
//执行代码2
} else {
//执行代码3
}

变量、常量与作用域

变量的声明:
var a = 1,也可以使用短声明a := 1
一般来说准确表示变量应该注明类型var a float64 = 1.0

var aa int
var bb string
var cc float64
var dd [5]int //数组
var ee []int  //数组切片
var ff *int   //整形指针
var v1 int = 5
var v2 = 5
v3 := 5

短声明的优点:
可以在无法使用var的地方使用该变量。比如说后者可以直接在for循环、if分支条件、switch内部使用短声明。
短声明的缺点:
不能在package级别中使用声明(不能用于全局变量、常量、赋值)。
变量的输入:
fmt.Scan(&variable)

package main

import "fmt"

func main() {
    var a int
    fmt.Println("请输入a:")
    //阻塞等待用户的输入 &
    fmt.Scan( &a)
    fmt.Println("a: ", a)
}

常量的声明:
const a = 1

    const limit = 512
    const top uint16 = 1421
    const Pi float64 = 3.1415846
    const x, y int = 1, 3
    //或者批量命名,省去重复const
    const (
        Cyan  = 0
        Blank = 1
        White = 2
    )
    //在一个const修饰范围中,命名多种常量,默认为0,出现一次iota,数值+1
    const (
        //iota,只能用于常量,常量生成器,每个一行自动累加1
        a = iota
        b = iota
        c
    )
    const d = iota
    fmt.Println(a, b, c, d) // 0, 1, 2, 0
  • 一般来说,变量与常量的范围为{}内。特殊情况下,当变量与常量声明在类中,则使用范围则变成了该类,在java中本质上来说也是在{}内,在go语言里省略掉了{},作用域最大在package中。
  • 一般来说,变量和常量要求声明后必须使用。但是全局变量、全局常量可以允许仅声明不使用,但仍然会占用内存。
  • iota 在 const关键字出现时将被重置为 0,const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

fmt.printf中%v、%f 、%T、%x、%b、%c
%f格式化动词(float):由两部分组成(宽度、精度)其中宽度指显示出的最少字符个数;精度是小数点后显示位数
%v格式化动词(value):填充指定位置的内容
%T格式化动词(type):表示将对应的内容转换成对应的数据类型
%b格式化动词(binary):将对应的数值转成二进制(bit)内容
%o格式化动词(octal):将对应的数值转成八进制内容
%d格式化动词(decimal):将对应的数值转成十进制内容
%c格式化动词(code):将对应的数值转成对应的符号(unicode码对应的符号)
%x格式化动词(hexadecimal):将对应的数值转换成十六进制,如果遇到空指针,则会返回0(内存地址为0x0)
%e格式化动词(exponential):将对应的数值转换成科学计数法表示的浮点数
%s格式化动词(string):将对应的数值转换成字符串
举例:

  • %v:指将内容实际全部输出。
  • %4.2f:指宽度为4,精度为2。显示内容至少有4个字符,不足时在数字前填充空格,小数点保留2位。
  • %04.2f:指宽度为4,精度为2。显示内容至少有4个字符,不足时在数字前填充0,小数点保留2位。
  • %.3f:指不规定最少宽度,精度为3。显示内容按实际字符数为准,小数点保留3位。
  • %f:指不规定最少宽度,精度默认为6。显示内容按实际字符数为准,小数点保留6位。

数据类型

序号 类型 个数
整型类型 int8、int16、int32(rune)、int64、uint8(byte)、uint16、uint32、uint64、int、uint、uintptr 11
浮点复数类型 float32、float64、complex64、complex128 4
其他基本类型 bool、string 2
语言通用型1 数组、切片 2
语言通用型2 接口、结构体、map 3
底层型 指针、函数、非安全类型指针 3
go语言特有型 channel 1
累计 26

整数类型

类型 初始值 范围 存储字节 说明
int8 0 -128 ~ 127 8bit(1个字节) 有符号
uint8 0 0 ~ 255 无符号,别名byte(表示ascii字符)
int16 0 -2^15 ~ 2^15-1 16bit(2个字节) 有符号
uint16 0 0 ~ 65535 无符号
int32 0 -2^31 ~ 2^31-1 32bit(4个字节) 有符号,别名rune(表示字符)
uint32 0 0 ~ 4,294,967,295 无符号
int64 0 -2^63 ~ 2^63-1 64bit(8个字节) 有符号
uint64 0 0 ~ 18446744073709551615 无符号
int 0 依赖于不同平台下的实现,可以是 int32 或者 int64
uint 0 依赖于不同平台下的实现,可以是 uint32 或者 uint64
uintptr 0 一个可以恰好容纳指针值的无符号整型(对 32 位平台是 uint32, 对 64 位平台是 uint64)
  • 整数环绕:所有的整数类型都有一个取值范围,超出这个范围后,就会发生环绕,即数值变为最小值。
  • 当数值超过uint64时,可以考虑先使用float64再 或者考虑更大的big
  • 任何整数类型都可以使用%c打印字符,但是如果类型为rune意味着该变量打印本来就是一个字符,可以不用%c
  • unicode码表示范围ascii码更大:
    • ascii码只支持英文字符、数字、标点符号和控制字符
    • unicode编码支持全球范围所有语言和字符,包括汉字、日文、韩文等
    • unicode码与ascii码对应的字符是相同的

浮点型

类型 初始值 存储字节 说明
float32 0.0 32bit(4个字节) 单精度,精度为7位小数
float64 0.0 64bit(8个字节) 双精度,精度为15位小数

复数类型

类型 初始值 存储字节 说明
complex64 (0+0i) 64bit(8个字节) 存储一个实部和虚部都是float32的复数值,占用8个字节(64位)。
complex128 (0+0i) 128bit(16个字节) 存储一个实部和虚部都是float64的复数值,占用16个字节(128位)。
package main

import "fmt"

func main() {
    var tComplex complex128
    tComplex = 2.1 + 3.14i
    fmt.Println("t = ", tComplex)
    //real()、imag()是complex的内建函数,用于取出复数的实部和虚部
    fmt.Println("real(实部) = ", real(tComplex), ",imag(虚部) = ", imag(tComplex))
}

布尔类型

类型 初始值 存储字节 说明
bool false 8bit(1个字节) 不支持自动或强制的类型转换。比如1转成true,0转成false

字符串类型

类型 初始值 存储字节 说明
string “” 128bit(16个字节)
rune 0 32bit(4个字节) 之前在整型类型中提到,可以作为字符,以单引号声明

基本操作:
字符串拼接:
字符串替换:

派生类型

类型 备注 说明
指针类型(Pointer) 值传递 指向内存地址的变量
数组类型 值传递 和java类似
结构化类型(struct) 值传递 类似于java中的类,定义属性。而函数中的方法所属于该结构体
接口类型(interface) 值传递 类似java中抽象类,定义抽象方法,需要实现类实现所有抽象方法。可以实现多态
函数类型 值传递 类似于java中静态方法
通道类型(Channel) 引用传递 多协程之间进行数据共享的通道。
类似于java中阻塞队列BlockingQueue
切片类型 引用传递 本质上是基于数组封装的动态数组
Map 类型 引用传递 和java中map类似

指针(类型安全指针)

一个指针变量指向了一个值的内存地址。
golang中获取变量的内存地址需要在变量前加上&符号(即指针的值):&variable

声明语法var pointerVariable *数据类型 = &variable

  • 数据类型必须和绑定内存地址的变量类型完全一致,比如指针为int8,绑定变量不能是int
  • 指针类型变量前面加上 可以获取内存地址指向的内容,比如a := 1,&a就是指针,`&a就是指针的内容,也就是a本身的值(a = *&a`)
  • *用于变量表示解引用指针,用于类型表示声明指针类型;而&只能用于变量:
    • var a int =10

1、 + (指针)变量 = 内容:`a = 102、& + 变量 = 指针(内存地址):&a = int指针值 = 0xc000088240 &a = 103、* + 类型 = 指针类型:int = int`
4、& + 类型 = 非法使用:不存在&int的表达式
空指针

当指针类型变量被定义后却没有初始化时,值为nil。这时候如果使用%x格式化动词,那么值被渲染成十六进制时发生错误,返回内容为0

判断空指针:

if ptr == nil {
//do something
}

指针数组:

指针数组和普通的数组类似。用法和数组一样,不同点只需要在数组[]后面添加*标记即可。

  1. 指针数组在声明时需要固定长度。
  2. 指针数组声明后不能改变长度。
  3. 仅声明指针数组却没有初始化内容时,内容默认为空指针(
var ptrArray = [2]*int{nil, nil} //声明指针数组时初始化内容
var newPtrArray [3]*int          //仅声明指针数组
var tempInt = 12                 //临时准备一个变量
newPtrArray[0] = &tempInt        //给指针数组赋值
newPtrArray[1] = new(int)        //给指针数组赋值
newPtrArray[2] = new(int)        //给指针数组赋值
ptrArray = [2]*int{}             //给整个指针数组赋值
fmt.Println("ptrArray: ", ptrArray)
fmt.Println("newPtrArray: ", newPtrArray)
for index, item := range newPtrArray {
    fmt.Printf("newPtrArray数组序号%v 指针%x 指针指向内容%v\n", index, item, *item)
}

指向指针的指针:

如果一个指针变量存放的是另一个指针变量的地址,这个指针变量就是指向指针的指针变量,也叫双重指针
pointer -> pointer -> variable -> value
声明:
var pointerVariable **数据类型 = &anotherPointerVariable

在函数中传递指针:

把相应的数据类型转换成对应的指针类型即可。
建议不要向函数传递数组的指针,而是应该使用切片。
原因:

  1. **array[index] = 1****(*array)[index] = 1** 的简写形式。即***[1]int **作为参数可以使用**[]int**替换;
  2. 使用切片作为函数参数时,只会复制切片结构信息,而不需要复制整个数组信息,更节省空间。
var testInt int8 = 10
var testPointer *int8 = &testInt
fmt.Printf("声明变量testInt = %v \n声明指针testPointer = %v \n指针指向的内容*testPointer = %v\n", testInt, testPointer, *&testInt)
var ptr *bool                  //声明一个指针不初始化
fmt.Printf("这是一个空指针%v\n", ptr) //填充内容,空指针为<nil>
fmt.Printf("这是一个空指针%x\n", ptr) //十六进制填充内容,空指针会返回0
if ptr == nil {
    fmt.Println("空指针")
} else {
    fmt.Println("不是空指针")
}

结构体

结构体(struct)是一系列具有相同类型或不同类型的数据构成的数据集合。可以理解为java中的类(class)。
定义:
type structName struct {
variable 数据类型
...
}
声明:
方式一:variable = 结构体类型{属性1, 属性2, 属性3}
方式二:variable = 结构体类型{字段1 : 属性1, 字段3 : 属性3}
其中,方式二里可以忽略字段的赋值,创建后的结构体变量属性为初始值
结构体属性访问:结构体变量.属性
匿名字段:结构体中可以声明匿名字段(只有类型,没有名字),表示该结构体继承了类型的成员

//定义结构体
type TestPeopleStruct struct {
    name   string
    age    int8
    gender int8 //枚举
}
//构建一个类似于java中toString方法
func (people TestPeopleStruct) toString()  {
    fmt.Printf("TestPeopleStruct={name=%v, age=%v, gender=%v}", people.name, people.age, people.gender)
}
func main(){
    //准备枚举值
    const (
        WOMAN = iota
        MAN
        UNKNOW = -1
    )
    //结构体变量声明
    var peopleStruct1 TestPeopleStruct = TestPeopleStruct{"娜可露露", 22, WOMAN}
    peopleStruct2 := TestPeopleStruct{name: "李白", gender: MAN}
    fmt.Println("结构体1:", peopleStruct1)
    fmt.Println("结构体2:", peopleStruct2)
    fmt.Println("结构体1的name:", peopleStruct1.name)
    //结构体的方法调用
    peopleStruct1.toString()
}

数组

数组是一种数据结构,用于存储固定数量、同类型的数据元素。数组是一种顺序存储结构,它的每个元素在内存中都是连续存储的。

  1. 数组的声明必须固定长度。
  2. 数组长度声明后不能改变长度。
func main(){
    var intArray [2]int //[0 0]  声明指定长度的数组,值为对应数据类型的默认值
    fmt.Println(intArray)
    var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} //初始化数组内容
    fmt.Println(balance)
    var balance2 = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} //长度不固定时可以使用...
    fmt.Println(balance2)
    var balance3 = [5]float32{2: 50.0} //固定长度时,初始化指定位置的内容.其他位置为对应类型的默认值
    fmt.Println(balance3)
    //多维数组
    var moreArray [3][2]int //声明多维数组
    var initMoreArray = [3][2]int{{1, 2}, {3, 4}, {5, 6}}    //初始化多维数组
    fmt.Println(moreArray)
    fmt.Println(initMoreArray)
    //调用传入数组参数
    passInArrayParameters(getArray, balance)//传入数组类型必须和函数的数组声明保持一致才能正确作为参数. 不正确有:函数声明为[]int,传入的却是[5]int 这是不允许的
}


/*
*
传入数组参数
*/
func passInArrayParameters(param []int, param2 [5]float32) {
    fmt.Printf("传入数组参数为:%v %v", param, param2)
}

切片

golang中切片是对数组的抽象。
切片就是动态数组。解决了数组的长度不可变的问题,可以灵活的对容器内元素进行增删。
声明:var sliceVariable []数据类型
初始化
方式一:var sliceVariable []数据类型 = make([]数据类型, 长度, 切片最大容量) 其中切片最大容量是可选参数,元素超过最大容量时会自动扩容。
方式二:var sliceVariable []数据类型 = []数据类型{元素1, 元素2, 元素3}
切片截取:
var splitVariable []数据类型 = 切片变量[开始序号:结束序号]截取区间为前闭后开。
追加元素切片变量 = append(切片变量, 元素1, 元素2...)
拷贝切片copy(初始化后的新切片, 原来的切片),最好将新切片的初始容量设为原来的切片一样大,否则只会拷贝对应位置的元素

  1. 切片不需要说明长度
  2. make方法中,第二参数不能大于第三参数,会报错
  3. 当添加元素后的总个数超过定义的最大容量后,切片底层会进行拷贝扩容(底层首个数组内存地址会改变
  4. 切片底层扩容时,元素个数小于1024时为2n,大于1024后为n+1/4n
  5. 使用len(切片变量)``cap(切片变量)可以查看切片的元素个数和容量
  6. var splitVariable []数据类型 = 切片变量[:结束序号]表示截取0到结束序号之前的元素
  7. var splitVariable []数据类型 = 切片变量[开始序号:]表示截取开始序号到最大序号len(s)之间的元素
var sliceVariable []int
fmt.Println("声明int切片sliceVariable")
if sliceVariable == nil {
    fmt.Println("sliceVariable为nil")
}
var sliceVariable1 []int = make([]int, 1, 10) //1是初始化容量,2是最大容量,超出最大容量后会拷贝扩容(改变内存地址)
var sliceVariable2 []int = []int{0}
fmt.Println("初始化切片,初始1个元素,容量为10")
fmt.Printf("make切片的指针 底层首数组地址%p\n", &sliceVariable1[0])
sliceVariable1 = append(sliceVariable1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) //添加元素后,超过了声明时定义的最大容量。会进行拷贝扩容2n
fmt.Println("添加了11个元素后")
fmt.Printf("make切片的指针 底层首数组地址%p\n", &sliceVariable1[0])
sliceVariable2 = append(sliceVariable2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
sliceVariable = append(sliceVariable, 1)
fmt.Printf("整形切片声明: %v\n", sliceVariable)
fmt.Printf("整形切片make创建: %v\n", sliceVariable1)
fmt.Printf("整形切片初始化创建: %v\n", sliceVariable2)
fmt.Printf("获取切片的长度-len(sliceVariable1):%v\n", len(sliceVariable1))
fmt.Printf("获取切片的容量-cap(sliceVariable1):%v\n", cap(sliceVariable1))
/**
切片截取
*/
var split []int = sliceVariable1[1:4]
fmt.Printf("前闭后开 sliceVariable1[1:4] = %v\n", split)
/**
切片拷贝
*/
newSliceVariable1 := make([]int, len(sliceVariable1)) //准备新切片,长度和即将复制的切片一样大
fmt.Printf("newSliceVariable1: %v\n", newSliceVariable1)
fmt.Printf("sliceVariable1: %v\n", sliceVariable1)
copy(newSliceVariable1, sliceVariable1) 
fmt.Printf("newSliceVariable1: %v\n", newSliceVariable1)
fmt.Printf("sliceVariable1: %v\n", sliceVariable1)

Map集合

map是一种无序的key-value的集合。和java中Map结构类似。
声明:
方式一:var mapVariable = make(map[key数据类型]value数据类型, 容量) 其中容量为可选参数。当实际键值对个数超过该值时会自动扩容。扩容倍数为2n
方式二:var mapVariable map[键数据类型]值数据类型 = map[键数据类型]值数据类型{key1:value1,key2:value2...}
根据key添加/修改value:mapVariable[key] = value
根据key删除value:delete(mapVariable, key)
获取map的数量:len(mapVariable)
遍历map:for key, value := range mapVarivale {}
判断是否存在某个键key:value, exists := mapVariable[key]

package main

import (
    fmt "fmt"
    "math/rand"
    "reflect"
    "strconv"
    "strings"
)

func main() {
    // map的使用示例
    // 创建一个空的map,键是字符串,值是整数
    myMap := make(map[string]int)

    // 添加键值对到map中
    myMap["apple"] = 10
    myMap["banana"] = 5
    myMap["orange"] = 7

    // 访问和更新map中的值
    fmt.Println("apple的数量:", myMap["apple"])
    myMap["apple"] = 15
    fmt.Println("更新后的apple的数量:", myMap["apple"])

    // 检查map中是否存在某个键
    quantity, exists := myMap["grape"]
    if exists {
        fmt.Println("grape的数量:", quantity)
    } else {
        fmt.Println("grape不存在")
    }

    // 遍历map中的键值对
    for fruit, quantity := range myMap {
        fmt.Printf("水果: %s, 数量: %d\n", fruit, quantity)
    }

    // 删除map中的键值对
    delete(myMap, "banana")
    fmt.Println("删除banana后:")
    for fruit, quantity := range myMap {
        fmt.Printf("水果: %s, 数量: %d\n", fruit, quantity)
    }

    // 获取map中键的数量
    fmt.Println("键的数量:", len(myMap))

    printValueAndPtr(myMap)
    // 清空map
    myMap = make(map[string]int)
    fmt.Println("清空map后,键的数量:", len(myMap))
    printValueAndPtr(myMap)


}

接口

类似于java中抽象类。是将多个结构体共同的方法抽取出来放到一起,任何其他类型只要实现了这些方法就是实现了这个接口。
利用接口的特性,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态
声明:
type 接口名 interface {
method1(形参...) 返回类型...
method2(形参...) 返回类型...
}
实现接口:
type 结构体名称 struct {
字段1 数据类型
...
}
func (elem 结构体名称) method1(形参...) 返回类型...{
//实现接口方法1...
}
func (elem 结构体名称) method2(形参...) 返回类型...{
//实现接口方法2...
}

  1. 结构体要实现某个接口,必须实现该接口的全部方法才算实现该接口
  2. 通过接口的特性,可以类比java中多态。
  3. 推荐一个写法:
    1. 声明一个接口,在定义一个全局变量接口L
    2. 每个类中可以定义一个函数,传入接口类型,用来初始化接口对象(就是java中子类继承父类,main方法中先创建子类对象,通过构造器初始化当前main中定义好的接口对象)。
func main(){
    //多态:接口类型 = 具体实现子类型
    var a Animal = Dog{
        name:     "修狗",
        category: "犬科",
    }
    a.eat("骨头")
    a.sleep()
    a = Cat{
        name:     "小猫",
        category: "猫科",
    }
    a.eat("鱼")
    a.sleep()
}
// Animal 定义接口
type Animal interface {
    //吃东西
    eat(something string) bool
    //睡觉
    sleep()
}

// Dog 定义结构体1,实现接口(必须实现该接口的所有方法)
type Dog struct {
    //名字
    name string
    //类别
    category string
}
//实现接口方法
func (dog Dog) eat(something string) bool {
    if something == "" {
        fmt.Printf("%v没东西可吃 \n", dog.name)
        return false
    }
    fmt.Printf("%v 吃%v\n", dog.name, something)
    return true
}
func (dog Dog) sleep() {
    fmt.Printf("%v 睡觉\n", dog.name)
}

// Cat 定义结构体2,实现接口(必须实现该接口的所有方法)
type Cat struct {
    //名字
    name string
    //类别
    category string
}
//实现接口方法
func (c Cat) eat(something string) bool {
    if something == "" {
        fmt.Printf("%v 没东西可吃 \n", c.name)
        return false
    }
    fmt.Printf("%v 吃%v\n", c.name, something)
    return true
}
func (c Cat) sleep() {
    fmt.Printf("%v 睡觉\n", c.name)
}

通道类型

了解channel之前,先了解golang并发。
实现并发,只需要通过go关键字开启goroutine(轻量级线程)。
语法:go 函数名(形参...)

package goroutine

import (
    "fmt"
    "runtime"
    "time"
)

/* 通过runtime.Goexit()函数与go关键字开启协程 */
func TestGoroutineSyncByGoexit() {
    //开启一个任务,任务预计耗时100秒+ 通过go关键字开启协程后,可节省这100+的时间
    go func() {
        for i := 0; i < 100; i++ {
            fmt.Printf("新任务执行:%d\n", i)
            time.Sleep(time.Second)
        }
    }()
    //主协程执行预计耗时100秒+
    for i := 0; i < 100; i++ {
        fmt.Printf("执行操作%d\n", i)
        time.Sleep(time.Second)
        if i == 3 {
            //使用此函数,将停止此协程的执行。不影响其他协程
            runtime.Goexit()
        }
    }
}

/*  测试通过channel同步主协程与子协程 */
func TestSyncGoroutineByChannel() {
    // 创建一个缓冲通道
    intChan := make(chan int, 5)
    fmt.Printf("channel 创建成功{ len: %v, cap: %v }\n", len(intChan), cap(intChan))
    fmt.Printf("主协程启动...\n")

    // 启动一个子协程:一次性执行所有操作
    go func(boolsChan chan int) {
        fmt.Printf("子协程启动...\n")
        for i := 1; i <= 5; i++ {
            boolsChan <- i
            fmt.Printf("子协程{ %v }\n", i)
            fmt.Printf("channel { len: %v, cap: %v }\n", len(boolsChan), cap(boolsChan))
        }
        fmt.Printf("子协程结束...\n")
    }(intChan)

    //启动一个协程:每秒执行一个操作
    go func(boolsChan chan int) {
        fmt.Printf("子协程Ⅱ启动...\n")
        for i := 6; i <= 10; i++ {
            time.Sleep(4 * time.Second)
            boolsChan <- i
            fmt.Printf("子协程Ⅱ{ %v }\n", i)
            fmt.Printf("channel { len: %v, cap: %v }\n", len(boolsChan), cap(boolsChan))
        }
        fmt.Printf("子协程Ⅱ结束...\n")
    }(intChan)
    /**
    main主协程接受11次 channel 通道中的数据,
    但是前2个协程中分别发送了5次数据,
    后5次数据需要多耗时发送。
    此时主协程接受会阻塞,
    但是第11次接受时 channel 中已无协程再往里发送数据,
    所以 main主协程会出现死锁错误而停止运行
    */
    for i := 1; i <= 11; i++ {
        // 从boolsChan通道中接收数据,并赋值给b
        time.Sleep(1 * time.Second)
        var b int = <-intChan
        fmt.Printf("%v同步协程{ %v }\n", i, b)
    }
    fmt.Printf("channel { len: %v, cap: %v }\n", len(intChan), cap(intChan))
    fmt.Printf("主协程结束...\n")
}

/* 测试channel遍历 */
func TestRangeChannel() {
    // 创建一个缓冲通道
    intChan := make(chan int, 5)
    intChan <- 1
    intChan <- 2
    intChan <- 3
    intChan <- 4
    intChan <- 5

    //close(intChan)
    for i := range intChan {
        //intChan <- 5
        fmt.Printf("\n%d", i)
    }
}
/* 测试单向channel作为参数的应用 */
func TestOneWayChannelExample() {
    // 创建一个缓冲通道
    intChan := make(chan int, 5)
    go producer(intChan)
    consumer(intChan)
}
//消费者
func consumer(inChan <-chan int) {
    for i := range inChan {
        fmt.Printf("\n%d", i)
    }
}
//生产者
func producer(outChan chan<- int) {
    defer close(outChan)
    for i := 0; i < 10; i++ {
        outChan <- i
    }
}

channel:通道,用来传递数据的一个数据结构。可以解决多线程不安全问题。
可以用于两个goroutine之间传递一个指定类型的值来同步运行和通讯。<-用于指定通道的方向,发送或接收。如果没有<-指定方向,默认为双向通道。
声明格式:
var c1 chan 数据类型 = make(chan 数据类型, 缓冲区大小), 其中缓冲区大小是可选参数,默认无缓冲区。
var c1 chan<- 数据类型 声明只能发送的通道
var c1 <-chan 数据类型 声明只能接收的通道

  • 单向通道作为参数可以接受双向通道

使用格式:
发送:
通道c1 <- 需要传递变量需要传递变量发送给通道c1
接收:
接收变量, ok/false := <- 通道c1通道c1接收数据并赋值给接收变量,ok/false表示是否接收到数据
关闭:
close(通道c1)
遍历通道:range关键字。注意,需要在channel关闭后range才能获取到范围。

func main(){
    //创建一个通道,启动多个goroutine线程进行累加
    //并打印出从通道里每次累加后的结果
    //试想一下如果不使用channel,累加值sum会出现并发修改问题吗
    var c1 chan int8 = make(chan int8)
    for i := 1; i <= 10; i++ {
        go sendToChannel(int8(i), c1)
        var result int8 = <-c1
        fmt.Printf("%v 当前处理结果:%v sum值:%v\n", i, result, sum)
    }
}

// 测试通道
var sum int8 = 0
//对全局变量sum累加,并往channel中发送内容
func sendToChannel(param int8, channel chan int8) {
    sum += param
    //累加后数据发送到channel中
    channel <- sum
}
  1. 发送channel发出,必须有相应数据类型的接收channel。
  2. 默认情况下,channel不带缓冲区(阻塞通道)。
  3. 通道关闭后有以下特点:
    1. 对已经关闭的channel再次发送值会导致恐慌(panic)
    2. 关闭已经关闭的channel也会导致恐慌(panic)
    3. 已经关闭的channel仍然能够接收获取,channel没有值时就会获取零值
  4. 超出缓存区时发送内容会出现死锁错误。

image.png

指针(非类型安全指针)

首先,非类型安全指针式golang中特别的类型,使用它必须导入unsafe包。它和前面提到的类型安全指针相比,有了更少的限制,与C语言中的指针类似,功能更强大,但是更不安全
定义:
type Pointer *ArbitraryType

函数

概念

抽取的最基本的代码块,一般定义为指定的任务,方便于多处地方重复利用。go语言最少有个main函数
语法:

func 函数名(参数名 参数类型, 参数名 参数类型)(返回值类型1, 返回值类型2) {
//函数体
}

  • 函数名命名采用小驼峰风格(private),如果导出则需要首字母大写(public)且不能为下划线开头
  • 函数可以返回多个值,如果只返回一个值可以省略返回的(),如果没有返回值可以省略返回值类型定义
func main(){
    maxInt, minInt := getMaxAndMinInt(5, 1, 12, 12, 8, 132, 311331)
    fmt.Printf("最大的数为: %v\n最小的数为: %v\n", maxInt, minInt)
}

/*
*
获取传入int数组中最大的一个数和最小的一个数
*/
func getMaxAndMinInt(values ...int) (int, int) {
    var maxInt int = -1
    var minInt int = math.MaxInt64
    for _, item := range values {
        if maxInt < item {
            maxInt = item
        }
        if minInt > item {
            minInt = item
        }
    }
    return maxInt, minInt
}

func test() {
    fmt.Println("简洁的函数声明:无形参 无返回值")
}

值传递与引用传递

值传递:在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。属于值传递的类型有:基本类型()、数组、结构体、接口、函数、指针

  • 值传递中==比较,只要元素相同,结果为true,reflect深度比较为true;
  • 函数类型不能==比较,只能通过reflect深度比较,一般为false

引用传递:在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。属于引用传递的类型有:切片、channel、map
默认情况下,go语言使用的是值传递,调用函数时不会影响实际参数。
如果想要使用引用传递,需要将传入的参数设置为 指针类型。

package main

import (
    "fmt"
    "reflect"
)

func main(){
    //数组
    var intArray [2]int
    var intArray2 [2]int
    fmt.Println(intArray == intArray2)//true
    //结构体
    var cat Cat = Cat{name: "猫",animalType: "猫科"}
    var cat2 Cat = Cat{name: "猫",animalType: "猫科"}
    fmt.Println(cat == cat2)//true
    //接口与结构体
    var cat3 Animal = Cat{name: "猫",animalType: "猫科"}
    var cat4 Animal = Cat{name: "猫",animalType: "猫科"}
    fmt.Println(cat3 == cat4)//true
    fmt.Println(cat3 == cat)//true
    //函数    不能 == 比较,只能通过反射
    f := funcType(spring)
    f2 := funcType(spring)
    fmt.Println(reflect.DeepEqual(f, f2))//false
    var a = 10
    var ptr = &a
    var ptr2 = &a
    //指针
    fmt.Println(ptr == ptr2)//true

}
type funcType func()int
func spring()int{
    return 0
}
func springboot()int{
    return 0
}


type Animal interface {
    eat()()
    sleep()()
}


//结构体
type Cat struct {
    name string
    animalType string
}
func (c Cat) eat()(){

}
func (c Cat) sleep()(){

}

函数作为实参

func main(){
    testFunctionAsParameter("data111", childFunction)
}
/*
*
将函数作为实参
*/
func testFunctionAsParameter(value string, childFunc diyFunc) {
    childFunc(value)
}

// 全局定义一个参数
type diyFunc func(string) string

// 定义一个符合以上参数条件的函数
func childFunction(value string) string {
    fmt.Printf("执行了传递函数:%v", value)
    return "success"
}

闭包

匿名函数是一个没有函数名称的函数,而闭包是一个函数和引用环境变量组成的整体。
匿名函数可以是包,也可以不是。若在匿名函数中引用了外部变量,那么它就是一个闭包。注意,一个闭包必定是一个函数,但一个函数不一定是闭包。
条件:匿名函数表达式 + 外部变量
闭包是一个函数,它包含了对其外部作用域中变量的引用,使得这些变量的状态可以被保持和访问,即使在外部作用域已经退出的情况下。
语法:

package main

import "fmt"

// adder() 无参函数,返回一个闭包函数 func(int)int
func adder() func(int) int {
    // 闭包函数引用的外部变量
    sum := 0
    //返回闭包函数,内部试图改变 sum 的值
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    // 创建一个闭包函数
    add := adder()

    // 多次使用闭包函数,每次的引用结果没有重置,而是在新闭包执行中累计计算
    fmt.Println(add(1)) // 输出:1
    fmt.Println(add(1)) // 输出:2
    fmt.Println(add(1)) // 输出:3
}

方法

golang中同时存在函数和方法。一个方法就是一个包含了接受者的函数,接受者可以时命名类型或者结构体类型的一个值或指针。下面以结构体为例:

func main(){
    var c1 Circle
    c1.radius = 10
    fmt.Println("圆的面积 = ", c1.getRadius())
}

/*
*
定义一个结构体
*/
type Circle struct {
    radius float64
}

/*
*
定义一个方法, 所属于结构体
*/
func (c Circle) getRadius() float64 {
    return 3.14 * c.radius * c.radius
}
  • 一个方法只能绑定一个接收者(命名类型或结构体,命名类型可以是函数、可以是别名)

其他

Golang中常用的内置函数,不需要import

len(String):表示获取字符串的字节长度(字符串中的部分字符占用字节可能不同,常用于英语字符串)
utf8.RuneCountInString(str):表示获取字符串的rune字符长度。
unsafe.Sizeof( param ):查看传入变量的类型字节长度

错误处理

golang中错误处理不同于java。具体是通过实现内置error接口来生成错误信息。

  • 一般来说,函数会在最后一个返回值中返回错误信息。

类型转换

golang中,数据类型不能混合使用。比如说字符串拼接:”I have “ + 10 + “ books.”。这样的代码会在golang编译时报错。

  1. string转成其他类型可以使用strconv包下的函数进行操作
    1. Atoi()、ParseInt()(ASCII to integer)字符串 -> int
    2. ParseUint()字符串 -> uint
    3. ParseBool()字符串 -> bool
    4. ParseComplex()字符串 -> complex
    5. ParseFloat()字符串 -> float
    6. Itoa()、FormatInt()(integer to ASCII)int -> 字符串
    7. FormatComplex() complex -> 字符串
    8. FormatFloat() float -> 字符串
    9. FormatUint() uint -> 字符串
    10. FormatBool() bool -> 字符串
  2. 其他数值类型之间的转换可以提供内置函数,大范围转成小范围的会返回错。
    1. int8()、int16()、int32()、int64()
    2. uint8()、uint16()、uint32()、uint64()
    3. float32()、float64()
    4. int()
  3. 接口类型转换(类似不局限于java中多态的场景)
    1. 接口断言:断言接口转成另外一种接口类型,返回bool
      • 接口变量.(即将转换的接口类型)
    2. 类型转换: 接口转换成另外一种接口
      • 即将转换的接口类型(接口变量)
  4. 其他特殊转换
    1. []byte(string) string转成[]uint8切片
    2. string([]byte) []uint8切片转成string
    3. []rune(string) string转成[]rune切片
    4. string([]rune) []rune切片转成string

类型别名

golang中,允许用户自定义类型别名。语法和类型定义类似。别名存在的意义,允许对原类型扩展方法。
语法:
单个命名:
type 别名 = 数据类型
批量命名:
type(
别名1 = 数据类型1
别名2 = 数据类型2
...
)

package main

import "fmt"

func main() {
    //回调函数
    switchLogin(Oauth2)
}

// LoginEnum 登录枚举别名
type LoginEnum int

//声明登录枚举值
const (
    UsernamePassword LoginEnum = iota
    TokenRemember LoginEnum = iota
    Oauth2 LoginEnum = iota
)

//选择登录方式执行
func switchLogin(loginType LoginEnum){
    var login LoginFunc
    switch loginType{
    case 0:
        login = UsernamePasswordLogin
    case 1:
        login = TokenRememberLogin
    case 2:
        login = Oauth2Login
    default:
        login = nil
    }
    if login == nil {
        fmt.Println("未匹配登陆方式")
        return
    }
    login()
}

// LoginFunc 登录函数别名
type LoginFunc func()()

// UsernamePasswordLogin 登录方式0
func UsernamePasswordLogin(){
    fmt.Println("用户名密码登录...")
}

// TokenRememberLogin 登陆方式1
func TokenRememberLogin() {
    fmt.Println("token记住我...")
}

// Oauth2Login 登录方式2
func Oauth2Login() {
    fmt.Println("第三方授权登录...")
}

类型查询(类型断言)

我们知道interface(any)的变量里面可以存储任意类型的数值。那我们如何反向知道这个变量实际保存的时哪个类型的变量呢?这就需要类型查询(两种方式):

  1. value, ok := interfaceVariable.(int / bool / string / ... )

value为变量值,ok为是否为type类型

  1. switch value := interfaceVariable.(type) {

case int:``case bool:``case string:``...
}

package demo

import "fmt"

// TestAnyTypeAssert 测试类型断言
func TestAnyTypeAssert() {
    var anySlice []any = make([]any, 5)
    anySlice[0] = "1"
    anySlice[1] = false
    anySlice[2] = 12

    //方式一
    vale, ok := anySlice[1].(bool)
    fmt.Println("变量值:", vale, "是否为bool:", ok)

    //方式二
    fmt.Println("变量值:", vale)
    switch anySlice[1].(type) {
    case bool:
        fmt.Println("为bool")
    case string:
        fmt.Println("为string")
    case int:
        fmt.Println("为int")
    }
}

无名类型

指的是没有定义具体类型名称的类型,通常是以下情况:

  1. 匿名结构体

var 结构体变量名 = struct{ 字段名 字段类型 }{ 字段名:字段值 }

  1. 匿名函数

func(){}()

  1. 匿名接口
    1. 先要有接口 type 接口名 interface{ 定义方法1(形参)返回值 }
    2. 声明全局函数变量 type 定义方法1 func(形参)返回值
    3. 声明方法所属于函数 func (定义方法1变量名 定义方法1) 定义方法1 func(形参)返回值{ 定义方法1变量名() }
    4. 声明匿名接口 var 变量名 接口类型 = 实现方法1(匿名函数)

类型嵌套(匿名字段)

在结构体中,声明一个字段由字段名、字段类型组成。类型嵌套允许结构体可以声明字段仅有一个类型,该类字段称为内嵌字段(匿名字段)。这样做的好处是为了将被嵌套类型的功能扩展到当前结构体中。
type 结构体名称 struct {
字段名 字段类型
**字段类型 //类型嵌套**
}
内嵌字段有一个隐式字段名,为与字段类型相同的名称。
内嵌字段支持的类型有:

  1. 结构体
  2. 接口
  3. 基本类型
  4. 指针类型

选择器:
animal.dog.name 表示从结构体animal中获取内嵌结构体dogname字段。
其中**animal.dog.name**表示一个选择器。
如果需要获取内嵌字段的子字段,可以省略掉隐式字段名直接获取子字段(选择器的缩写)。
**animal.name****animal.dog.name**的缩写。
关于选择器遮挡和碰撞:
选择器的内嵌字段有很多个,每个内嵌字段中都有相同名字的字段。这个时候选择器的缩写就出现了纠结。为了解决此纠结,有以下规定:

  • 当结构体有多个内嵌字段或者有多个最终子字段名相同的字段,选择器缩写为深度最浅的(选择器遮挡)

x.y.z.namex.y.namex.name三个层级的选择器,缩写会选择x.name作为结果。
如果说多个选择器层级一样,则选择器缩写会失效(选择器碰撞)。比如x.y.namex.a.namex.b.name,此时必须写完整的层级声明进行选择。

底层类型

golang中,每个类型都有一个底层类型:

  • 内置类型的底层类型为本身
  • Map映射的底层类型是hash table
  • 结构体的底层类型不固定,根据用户自定义内部可以包含多个不同的数据类型。
  • 接口的底层结构为一个拥有2个指针组成的结构体。一个指针指向类型的代码和接口方法表;另一个指针指向实现体的值。
  • 数组是一个内置类型,底层实现是固定大小的连续内存空间,所以底层结构为本身。
  • 切片的底层实现是数组,底层结构是指向数组的指针、切片的长度两个部分。
  • channel的底层是结构体实现的。
  • 指针的底层结构可以理解为本身,据说官方文档没有说明。
  • 函数类型的底层结构可以理解为本身。

回调函数

函数中有一个参数是函数类型,这个函数就是回调函数

package main

import "fmt"

func main() {
    //登陆结果
    loginStatus := 0
    var option CallbackFunc = loginFail
    //登陆后操作
    loginAfter(loginStatus, option)
}
//登录操作后 回调函数
func loginAfter(status int, operate CallbackFunc) {
    fmt.Println("登录状态:", status)
    operate()
}

// CallbackFunc 回调函数别名
type CallbackFunc func()()


func loginSuccess() {
    fmt.Println("日志生成: 登录成功")
}

func loginFail() {
    fmt.Println("日志生成: 登录失败")
}

延迟函数调用(defer)

defer是golang中的关键字。可以用于延迟执行某个函数或方法的调用,即在当前函数或方法返回之前执行该延迟的函数。被defer延迟执行的语句会被放入一个栈中,按照先进后出(FILO)的顺序执行。
使用场景:

  1. 通常用于资源释放,比如关闭文件、释放锁、关闭数据库连接。
  2. 可以用于异常处理:当函数发生异常时,如果存在defer语句,defer中的语句会在异常抛出前执行,可以执行清理操作、日志记录等。

特别注意:

  1. defer语句的执行在函数返回之前执行的,所以不能用于影响函数的返回值,可以影响最内层函数的返回值
  2. defer的语句在执行时会保留定义refer时的变量状态

defer流程.png

package main

import "fmt"


func main(){
    //refer延迟函数 栈的思想(后进先出)
    defer fmt.Println("9")
    fmt.Println("0")

    defer fmt.Println("8")
    fmt.Println("1")

    if false {
        defer fmt.Println("不能执行")
    }

    defer func() {
        defer fmt.Println("7")
        fmt.Println("3")
        defer func() {
            fmt.Println("5")
            fmt.Println("6")
        }()
        fmt.Println("4")
    }()

    fmt.Println("2")

    return
    /*
        main 函数执行 return 后的所有代码均无法执行
     */
    //1、延迟函数:不能执行
    defer fmt.Println("不能执行")
    /*
        2、执行普通代码:不能执行
            延迟调用可以修改包含此延迟调用的最内层函数的返回值
     */
    fmt.Println(Triple(5)) // 15
}

func Triple(n int) (r int) {
    defer func() {
        r += n // 修改返回值
    }()
    return n + n // <=> r = n + n; return
}

恐慌及恢复(panic、recover)

在golang中,pannicrecover是用于处理异常的关键字。
panic:用于引发一个运行时错误。类似于java中的异常。发生panic时,函数的执行会被终止,会执行所有的defer语句,再打印错误栈堆。
recover:将捕获panic引发的异常。
函数:
panic(错误信息):触发panic异常,并携带错误信息。
错误信息 := recover(),接收panic的错误信息。当无panic执行时,会返回nil。

func main(){

    testPanic()
    fmt.Println("程序正常结束")
}

func testPanic() {
    defer func() {
        if r := recover(); r != nil {
            // 处理panic的逻辑
            fmt.Println("发生了异常:", r)
        }
    }()
    // 模拟发生异常
    panic("强制异常")
    fmt.Println("执行后续业务...")
}

值(值、对象)

一个类型的一个实例称为此类型的一个值。一个类型可以有很多不同的值,其中一个为它的零值。同一类型的不同值共享很多相同的属性。
简单的来说,可以理解为,类型就是java中的class类,值就是java中class的对象,零值就是初始值。

值尺寸

值占的内存空间大小。

值部

简单理解为java中的栈帧,用于存储函数调用时的局部变量、参数、运算结果和返回值等信息。

指针类型的基类型

表示指针的指向的值的类型
var ptr *[]int的基类型为[]intvar ptr1 *string的基类型是string

函数类型的签名

指的是在声明函数类型时的表示形式,type 函数类型名称 **func(形参...)(返回类型...)**, 后者加粗的为函数类型的签名。

类型的方法和方法集

方法在golang中作为成员函数,所属于结构体,所有的方法集合叫做该类型的方法集。
类似于java中的成员方法(非静态方法),所属于对象。

接口类型的动态类型和动态值

顾名思义,就是指接口的具体子实现的值和类型。如果该接口值没有具体子实现的值,就是零值接口值(未初始化的接口变量)。

容器类型

包括数组、切片、Map,有时候string、channel也算非正式的容器类型

可比较类型和不可比较类型

可比较类型允许使用==或者!=进行比较,反之就是不可比较类型:

  1. 切片类型
  2. Map类型
  3. 函数类型
  4. 包含有以上类型字段的结构体
  5. 包含有以上类型的数组类型

golang中对OOP思想的支持

golang不完全支持面向对象编程。但是支持部分OOP思想的元素:方法、实现、类型内嵌。

main函数与main包

main函数是go程序运行的入口,且应该放置于main包下才能够运行。

package包

golang中package声明最好只声明当前目录的名字而不是像java一样使用根路径长。package应该是简洁的小写字母

  • 包中声明的结构体、方法或函数、变量导出:标志命名时大写。
  • 所有包都包含一个无形参无返回值类型的init函数,可以执行初始化任务,该函数不需要显式调用它。

func init(){}

  • 包的初始化顺序如下:
    1. 初始化包级别的变量(全局变量)
    2. 紧挨着的init函数。包可以有多个init函数(一个文件或分布于多个文件中)
    3. 如果一个包导入了另一个包,优先初始化被导入的包
    
  • 一个包可以被导入很多次,但是只会初始化一次。(类比java中类的初始化)
  • main包的初始化顺序为:
    1. 首先初始化被导入的包
    2. 接着初始化包级别的变量(全局变量、常量)
    3. 调用init函数
    4. 最后调用main函数
    

例如,假设有一个名为main的包和另外两个包pkg1和pkg2,它们之间有如下的依赖关系:main -> pkg1 -> pkg2。
那么,初始化的顺序如下:

  1. pkg2的包级别变量初始化。
  2. pkg2的init函数的执行。
  3. pkg1的包级别变量初始化。
  4. pkg1的init函数的执行。
  5. main的包级别变量初始化。
  6. main的init函数的执行。
  7. main的其他代码执行。

泛型

golang版本1.18之前泛型只支持对内置类型和内置函数中,1.18开始支持自定义泛型。
泛型是一种编程范式,它可以让我们编写能够处理多种类型的代码,不用针对特定类型编写重复代码。
实现泛型效果,可以通过下面几种方式:

  1. 使用接口,达到多态效果
  2. 使用别名、或者空接口

反射

反射是一种在运行时检查变量类型、调用方法和动态修改变量的能力。可以在不知道具体类型的情况下动态获取该类型信息、检查和修改变量的值、调用方法等操作。
使用前需要导入reflect包。
reflect.Type是一个接口,表示接口的具体类型(具体到自定义类别,比如Cat结构体)
reflect.Value是一个结构体,表示接口的具体类型的具体值
reflect.TypeOf()返回reflect.Type
reflect.ValueOf()返回reflect.Value
reflect.Type变量.Kind()返回具体类型(具体到底层类型,比如struct)
reflect.Value变量.NumField()返回具体类型的字段数
reflect.Value变量.Field(index)根据索引获取指定字段的值

var animal Animal = Cat{
    name:     "小橘猫",
    category: "猫科",
}
printValueAndType(animal)
//开始反射
typeOf := reflect.TypeOf(animal)
printValueAndType(typeOf) //具体类型(自定义类型,Cat)
valueOf := reflect.ValueOf(animal)
printValueAndType(valueOf) //具体值
kind := typeOf.Kind()
printValueAndType(kind) //具体类型(底层结构,struct)
field := valueOf.NumField()
printValueAndType(field) //具体类型的字段数
fieldI := valueOf.Field(1)
printValueAndType(fieldI) //根据索引获取指定字段的值
vi := valueOf.String()
printValueAndType(vi) //获取属性的int64,当前valueOf是结构体,使用会出错
vs := valueOf.String()
printValueAndType(vs) //获取属性的string,当前是结构体,会打印package

字符串方法、
时间方法、
时间格式化、
时间包、
协程等待的实现方式。
any是空接口的别名、
多切片覆盖在问题(遍历切片A{1,2,3},赋值v给B,问B的元素顺序)、
函数内切片的扩容传递、
函数内有形参A,在for循环中取名A,问两个A之间有什么联系。
结构体中指针方法与非指针方法的联系区别
map删除元素
切片删除元素
map的key的类型要求

启动时获取命令行参数

运行程序时,一般使用go run xxx.go parm1 param2 param3命令启动程序,有需要时可以获取参数。在程序中通过var argsList []string = os.Args来获取。
os.Args获取的就是param1、param2、param3组成的切片。

不同作用域允许定义同名变量

使用变量时,就近原则:
使用变量的位置所在的作用域中往外层扩大过程中,遇到的第一个变量声明

package main

import (
    "fmt"
    "math"
)

var a string = "全局变量a"
func main() {
    /**
        就近原则:使用变量的位置所在的作用域中往外层扩大过程中,遇到的第一个变量声明
     */
    var a int8 = math.MaxInt8
    for i:=0; i<3;i++ {
        var a bool = false
        //就近原则:a为bool
        fmt.Printf("%T%c", a, '\n')
    }
    //就近原则:a为int8
    fmt.Printf("%T%c", a, '\n')
    //就近原则:a为全局变量
    printA()
}
func printA(){
    fmt.Printf("%T%c", a, '\n')
}

工程管理

src目录:放源代码
bin目录:通过go install命令生成的目录,存放可执行文件
pkg目录:存放编译后的包文件。无需直接操作,go工具链会自动管理
如果有多个文件或多个包
1、配置GOPATH环境变量,配置src同级目录的绝对路径
2、自动生成bin或pkg目录,需要使用go install命令
除了要配置GOPATH环境变量,还要配置GOBIN环境变量

new()得到的是零值指针而不是实际值

a := new(int8)这样的结果在java看来返回值类型应该是int8,但是在golang中并非如此。
此时a的类型为*int8的零值指针,若想改变内容,可以添加以下代码:
*a = 127表示为a指针重新指向新的内存地址(127的内存地址)

make函数用于创建slice、map、channel

创建slice:
var sliceVariable []数据类型 = make([]数据类型, 长度, 切片最大容量) 其中切片最大容量是可选参数,元素超过最大容量时会自动扩容。

  • slice := make([]int, 5)

创建一个切片,长度为5,容量也为5,初始值都是零值
创建map:
var mapVariable = make(map[key数据类型]value数据类型, 容量) 其中容量为可选参数。当实际键值对个数超过该值时会自动扩容。扩容倍数为2n

  • m := make(map[string]int)

    创建一个空映射

创建channel:
var c1 chan 数据类型 = make(chan 数据类型, 缓冲区大小), 其中缓冲区大小是可选参数,默认无缓冲区。

  • ch := make(chan int, 10)

创建一个带有10个缓冲区的整数类型通道

数组转成切片

数组和切片是不同的数据类型,但是切片基于数组进行封装的数据类型。从某种程度上是可以从数组转成切片的:
array := [...]int{10, 20, 30, 0, 0}
slice := array[0:3:5]
0:下标起点(闭区间)
3:下标终点(开区间)
5:切片最大容量

结构体标签(struct_tag)

在结构体类型声明里面,字段后面可以跟一个可选的字符串标签,也就是结构体标签(struct_tag)。go语言允许我们通过结构体字段标签给一个字段附加可以被反射获取的元信息。使用结构体标签一般遵循以下规范:

  1. 结构体标签的值由空格分隔的**key:"value"**对列表。例如:

Name stringjson:”name” xml:”name”``

  1. **key**通常表示后面的**value**被哪个包使用,如果要在**value**中传递多个信息,可以用逗号分隔指定。例如:

Name stringjson:”name,omitempty”``
常用的包:

  • json: 由encoding/json包使用,详见json.Marshal()的使用方法和实现逻辑。
  • xml : 由encoding/xml包使用,详见xml.Marshal()
  • bson: 由gobson包,和mongo-go包使用。
  • protobuf: 由github.com/golang/protobuf/proto 使用,在包文档中有详细说明。
  • yaml: 由gopkg.in/yaml.v2 包使用,详见yaml.Marshal()

gorm: 由gorm.io/gorm包使用,示例可以在GORM的文档中找到。

  1. 如果一个标签的某个**key****value**设置成了**-**,就意味着告诉对应包在处理该结构体时排除此字段。

Name stringjson:”-“** **
通过反射获取标签:
typeOf := reflect.TypeOf(student)获取结构体Type
field := typeOf.Field(i)根据索引获取字段
field.Name获取字段名
field.Tag获取Tag标签
**field.Tag.Get("tagKey")**从tag标签中获取指定key的值

定时器(timer和ticker)

golang中定时器包括:一次性定时器(timer)和周期性定时器(ticker)。
前者只执行一次,后者根据周期时间执行多次。
timer的创建方式:
time.NewTimer(Duration)time.Arter(Duration)两种。
前者返回Timer结构体,包含通道C;后者是前面的封装,直接返回包含的通道C。
time.AfterFunc(Duration, func)创建定时器后,定时执行业务函数
myTimer.C获取定时器的只读通道C,获取后可以写定时业务
myTimer.Stop()停止定时器
myTimer.Reset(Duration)重置定时器
ticker的创建方式:
time.NewTicker(Duration)
myTicker.C获取定时器的只读通道C,获取后可以写定时业务
myTicker.Stop()停止定时器
myTicker.Reset()重置定时器

package timer

import (
    "fmt"
    "time"
)

func TestTimer() {
    myTimer := time.NewTimer(time.Second * 2) // 启动定时器
    var i int = 0
label:
    for {
        select {
        case <-myTimer.C:
            i++
            fmt.Println("count: ", i)
            myTimer.Reset(time.Second * 2) // 每次使用完后需要人为重置下, 否则timer只会执行一次
            if i == 3 {
                // 不再使用了,结束它
                myTimer.Stop()
                break label
            }
        }
    }
}

func TestTicker() {
    ticker := time.NewTicker(time.Second * 2)
    i := 0
    for t := range ticker.C {
        i++
        fmt.Println(i, t)
        if i == 5 {
            ticker.Stop()
            return
        }
        if i == 3 {
            ticker.Reset(time.Second * 10)
        }
    }
}
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  条评论