Golang入门

之前在学习golang,记录了一些常用的语法知识点,仅作备份

Hello World

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

变量

go的变量用var声明,同javascript。用var声明后如果不赋值则默认为0

var v_name type = value
//eg
var a int = 10

也可以同python使用:=来快速声明,左侧变量应是未声明过的,否则会报错

v_name := value

声明多个变量

var x, y int
var ( //这种写法一般用于声明全局变量
        a int
        b bool
)


var c, d int = 1, 2
var e, f = 123, "text"

常量

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

格式:

const identifier [type] = value

其中type可以省略,交由编译器自行推断

//eg:
const str string = "abc"
const s = "abc"

常量也可以用作枚举

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

iota

iota用于定义一个枚举时,可以简化常量中用于增长数字的定义,如下:

const (
    Id = iota    // 0
    Name        // 1
    Area        // 2
)

将第一个常量赋值为iota后,后续的常量都不需要继续自行赋值,编译器会自动为下面的常量按顺序赋值

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)
)

执行顺序

golang中有两个保留的函数,为init函数和main函数,这两个函数在定义时不能有任何的参数和返回值

程序运行时会自动调用init()和main()函数,无需自行调用

每个package中的init函数都是可选的,但package main就必须包含一个main函数。

包之间的导入关系如下

img

go指针

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

例:输出变量地址

package main

import "fmt"

func main() {
    var a int = 10
    fmt.Printf("变量的地址: %x", &a)
}

out:

变量的地址: c00001c0a8

go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如*int*int8

如果要取出内存地址中的值,则需要使用*来取出

func main() {
    a := 10
    b := &a        // 0xc00009e058(内存地址)
    fmt.Print(*b)    // 10
}

defer

在 Go 语言中,defer 是一个用于延迟函数调用执行的关键字。通过使用 defer,可以在函数返回之前(无论这个函数是正常结束还是发生了异常),推迟某个函数的执行。这在编写代码时可以很有用,特别是在需要确保资源释放或清理操作的情况下。

defer 语句的语法是:

defer functionCall(arguments)

当在一个函数内部使用 defer 时,被推迟执行的函数调用会被添加到一个栈中,而不会立即执行。在函数执行完毕并即将返回之前,栈中的函数调用会按照逆序执行。因此,如果有多个 defer 语句,它们会按照后进先出(LIFO)的顺序执行,即最后一个推迟的函数调用会最先执行,而最早的推迟的函数调用会最后执行。

以下示例演示了有三个 defer 的情况:

package main

import "fmt"

func main() {
    defer fmt.Println("第一个defer")
    defer fmt.Println("第二个defer")
    defer fmt.Println("第三个defer")
    fmt.Println("正常的函数调用")
}

out:

正常的函数调用
第三个defer
第二个defer
第一个defer

在这个示例中,尽管 fmt.Println("正常的函数调用") 在代码中位于三个 defer 语句之前,但它会最先执行。然后,defer 语句会按照后进先出的顺序执行,所以先执行第三个 defer,然后是第二个 defer,最后是第一个 defer。这就是 defer 的执行顺序

defer 的设计是为了在函数返回前执行一些清理工作,或者确保一些资源被正确释放。通过使用 defer,我们可以更方便地管理代码,并确保清理操作不会被遗漏。

defer注册要延迟执行的函数时该函数所有的参数都需要确定其值

func calc(index string, x, y int) int {
    sum := x + y
    fmt.Println(index, x, y, sum)
    return sum
}
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
}

按照defer的注册顺序,每个函数的需要先按顺序注册一遍,以确定其值

// 注册顺序
defer calc("AA", x, calc("A", x, y))
defer calc("BB", x, calc("B", x, y))

此时,因为函数的所有参数值都需要确定,函数参数中的calc也会根据注册顺序被调用

接下来,defer会按照LIFO顺序依次将函数压入栈底

defer calc("BB", x, calc("B", x, y))
defer calc("AA", x, calc("A", x, y))

所以,最后输出结果为:

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

数组

go的数组需要在定义的时候给出一个长度

固定长度数组

var 数组名 [数组长度]类型

belike:

var myArray [10]int // 创建一个长度为10的int类型数组

常规遍历数组的方式:

for i := 0; i < len(myArray); i++ {
    fmt.Println(myArray[i])
}

go语言中可以使用for range来对数组进行遍历,以同时获得下标与数组值

for index, value := range myArray{
    fmt.Println("下标为:", index, ",值为:", value)
}

数组可以通过...来让编译器由初始值自动推断数组长度

arr := [...]int{1, 2, 3} // 3

数组可以在初始化时给定下标

arr := [...]int{0:1, 2:2, 5:3} // [1 0 2 0 0 3]

切片(slice)

切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度len和切片的容量cap

切片的初始化不需要像数组一样提供一个长度

var mySlice []int //创建一个int类型的切片

切片类型是一种引用类型(在被引用时会将地址赋值给新变量)

将数组赋值给另一个变量并输出的结果:

arr1 := [...]int{1, 2, 3}
arr2 := arr1
arr1[0] = 11
fmt.Printf("arr1:%v - arr2%v\n", arr1, arr2)    // arr1:[11 2 3] - arr2[1 2 3]

可以看到arr2并没有随着arr1一同改变

如果将切片赋值给其他变量,因为是将地址赋值给了新的变量,所以当slice1改变时,slice2也会一同改变

slice1 := []int{1, 2, 3}
slice2 := slice1
slice1[0] = 11
fmt.Printf("slice1:%v - slice2%v\n", slice1, slice2)    // slice1:[11 2 3] - slice2[11 2 3]

如果要复制一份不被引用的切片,可以使用copy()函数

slice1 := []int{1, 2, 3, 4}
slice2 := make([]int, 4, 4)
copy(slice2, slice1)

切片的底层是由数组实现的,因此可以通过数组来创建切片

arr1 := [5]int{1, 2, 3, 4, 5}
b := arr1[:] // :代表获取数组内所有值
fmt.Print(b) //[1 2 3 4 5]

切片的长度和容量

长度len():切片的长度就是包含的元素个数

容量cap():切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数

s := []int{2, 3, 5, 7, 9, 11, 13, 15}
fmt.Printf("长度%d 容量%d\n", len(s), cap(s))

a := s[2:]
fmt.Printf("长度%d 容量%d\n", len(a), cap(a))

b := s[1:3]
fmt.Printf("长度%d 容量%d\n", len(b), cap(b))

故,当切片取arr[x:y]时,切片容量为切片arr中索引x的位置开始到arr最大元素的个数

动态创建切片

上述创建切片的方法都是基于数组来创建切片,如果要动态地创建一个切片,则需要使用make函数

make([]T, size, cap)

eg:

var sliceA = make([]int, 4, 8)
fmt.Println(sliceA)    // [0, 0, 0, 0]

要往切片中新增元素,需要使用append方法

sliceA = append(sliceA, 1)
sliceA = append(sliceA, 1, 2, 3, 5) //也可同时追加多个元素

还可以使用append方法将一个切片追加到另一个切片中

sliceA := []string{"a", "b"}
sliceB := []string{"c", "d"}
sliceC := append(sliceA, sliceB...)    // ...为固定语法,在数组中...代表不定长度,这里代表将切片打散进行传递
fmt.Printf("%v", sliceC)    // [a, b, c, d]

切片排序

go提供了sort包来实现快速排序

intList := []int{2, 4, 5, 6, 7, 9 ,1, 0, 8}
float8List := []float64{4.2, 5.1, 12.1, 5.2, 15.1}
stringList := []string{"a", "b", "d", "c", "z", "y"}

sort.Ints(intList)
sort.Float64s(float8List)
sort.Strings(stringList)

降序排序

intList := []int{2, 4, 3, 6, 7, 9, 1, 0 ,8}
sort.Sort(sort.Reverse(sort.IntSlice(intList)))

流程控制

for

go的循环统一使用for

一个标准的for循环

for initialization; condition; post {
    // zero or more statements
}

eg:

for i := 0; i <= 10; i++ {
    // 循环体
}

go中的while写法就是for循环省去initialization和post部分:

for conditioin{
  
}

在go中进行无限循环,只需要将for的三个部分全部省去

for {
  
}

go中可以通过一种名为标签的特性来在循环中进行跳转

label1:
    for i := 0; i <= 10; i++ {
        // 循环体
        if i == 3 {
            goto label1
        }
    }

这样,在循环中i=3时,将会跳转到label1继续执行循环。在多重嵌套中也可以使用label跳出循环

switch

go语言的switch分支语句在case的时候默认自带break,既匹配到一项后自动结束switch循环

switch calc {
    case "+":
        return "add"
    case "-":
        return "sub"
    case "*":
        return "times"
        break    // break可写可不写
    default:
        return nil
}

_

在上文遍历数组的例子提到,每次循环迭代,range产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。

Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用range_echo程序,因为隐式地而非显式地索引os.Args,容易写对。

import中的空标识符

import(
    _ "net"
)

此时_的作用是:当导入一个包的时候,不需要把所有的包都导进来,只需要导入使用该包下的文件里所有的init()的函数。

package main

import _ "hello/imp"

func main() {
    //imp.Print() //编译报错,说:undefined: imp
}

代码中的空标识符

作用是:下划线在代码中是忽略这个变量

也可以理解为占位符,那个位置上本应该赋某个值,但是我们不需要这个值,所以就把该值给下划线,意思是丢掉不要,这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。

如果方法返回两个值,只想要其中的一个结果,那另一个就用_占位

package main

import "fmt"

v1, v2, _ := function(...)

map

map是一种无序的基于k-v的数据结构,go中map为引用类型,定义后默认值为nil,必须初始化后才可以使用。

go中map语法定义如下:

map[KeyType]ValueType

用make来创建一个map

var user = make(map[string]string)
user["id"] = "1"
user["password"] = "114514"
fmt.Println(user)    // map[id:1 password:114514]

也可以在声明的同时填入元素

userinfo := map[string]string{
    "username": "admin",
    "password": "114514"
}

可以通过key来直接获取value

userinfo := map[string]string{
    "username": "admin",
    "password": "114514"
}
fmt.Println(userinfo[key])    // "admin"

map的遍历

for k, v := range userinfo{
    fmt.Printf("key:%v - value: %v", k, v)
}

获取map中的值

v, ok := userinfo["password"] // 当指定key的value存在时,返回v的值且返回ok为true;不存在则返回nil和false

要删除map中的一组键值对,可以使用内建函数delete()

delete(userinfo, "password")    // 删除key为password的键值对

函数

go函数的定义方式如下:

package main

import "fmt"

func 函数名(参数名 参数类型) 返回类型 {
    return 参数名
}

例:

package main


import "fmt"

//         ↓↓↓ 参数类型相同可以简写
func swap(x, y string) (string, string) {
   return y, x
}


func main() {
   a, b := swap("Mahesh", "Kumar")
   fmt.Println(a, b)
}

out:

Kumar Mahesh

通过在参数名后加...可以声明一个可变参数

func arg_test(x ...int) {
    fmt.Printf("%v - %t", x, x)
}

func main() {
    arg_test(1, 2, 3, 4)
}
// 输出 [1 2 3 4] - []int

可以看到,可变参数将会以一个切片的形式传入到函数中

可变参数可以和普通参数一同使用,但可变参数必须放在后面

func arg_test(x int, y ...int) {
    ...
}

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

func calc(x, y int) (sum int, sub int) {
    sum = x + y
    sub = x - y
    return
}

func main() {
    x, y := calc(3, 2)
    fmt.Printf("%d, %d", x, y)    // 5, 1
}

type

通过type关键字,我们可以定义一个函数类型

type calculation func(int, int) int

e.g:

type calculation func(int, int) int // 定义一个名为calculation的类型

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

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

func main() {
    var calc calculation
    calc = add
    fmt.Printf("type of c is: %T", calc)    // type of c is: main.calculation
}

假如定义了一个函数不符合calculation的类型,则会报错

func test() {
    fmt.Println("test")
}
func main(){
    var t calculation
    t = test    // 报错 cannot use test (value of type func(i int) int) as calculation value in assignment
}

形参方法

方法作为参数传入

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

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

type calcType func(x, y int) int    // 自定义函数类型,统一规范传入函数的类型

func calc(x, y int, operate calcType) int {
    return operate(x, y)
}

func main() {
    sumRes := calc(3, 2, add)
    subRes := calc(3, 2, sub)
    fmt.Printf("%d -- %d", sumRes, subRes)
}

匿名函数

go中声明匿名函数的方法很简单,只需要在普通函数的基础上去掉函数名

func(x,y int) int {} ()    // 简单的匿名函数调用

如果函数的参数需要传入另一个函数,那么这时候也可以使用匿名函数来进行传入

type calcType func(x, y int) int    // 自定义函数类型,统一规范传入函数的类型

func calc(x, y int, operate calcType) int {
    return operate(x, y)
}

func main() {
    // 自定义一个匿名函数运算乘法
    times := calc(5, 5, func(x, y int) int {
        return x * y
    })
    fmt.Printf("res:%d", times)    // res:25
}

函数递归

递归就是函数调用函数自身,例:

func fn1(n int){
    if n > 0 {
        n--
        fn1()
    }
}

func main(){
    fn1(10)    // 打印出10到0的所有整数
}

例:递归实现1-100的和

func sum(n int) int {
    if n > 1{
        return n + sum(n - 1)
    } else {
        return 1
    }
}
sum(100)

闭包

闭包可以理解为“定义在一个函数内部的函数”。本质上,闭包是将函数内部和函数外部连接的桥梁。

要解释闭包的用处,首先要了解全局变量与局部变量的特点

全局变量局部变量
1常驻内存不常驻内存
2污染全局不污染全局

而闭包的特点就是让一个变量常驻内存且不污染全局

闭包是指有权访问另一个函数作用域中的变量的函数,创建必报的常见方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数

e.g:

func adder() func() int {
    var i = 10
    return func() int {
        return i + 1
    }
}

func main() {
    var a = adder()
    fmt.Printf("%d", a())    // 11
}

在main函数中声明了一个变量a,并调用了adder方法赋值给a,此时因为adder已经被调用,会返回一个函数赋值给a,此时再对a进行调用,则会调用adder函数内的函数,并返回其值。

用另一个例子来演示可以更好地看出闭包的特点

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

func main() {
    var a = adder()
    fmt.Printf("%d\n", a(10))
    fmt.Printf("%d\n", a(10))
    fmt.Printf("%d", a(10))
}

输出:

20
30
40

由于adder函数被调用并赋于a,对于adder中的匿名函数来说,adder中定义的i就相当于是全局变量,不会随着函数结束生命周期被销毁

panic & recover

go中使用panic和recover模式来处理错误

panic用于抛出一个异常来终止程序运行

func fn1() {
    fmt.Println("fn1")
}

func fn2() {
    panic("抛出异常")
}

func main() {
    fn1()
    fn2()    // 执行到函数fn2时,会抛出panic异常,并结束程序执行
    fmt.Println("结束")
}
fn1
err: 抛出异常

recover需要配合defer使用,用于捕获panic异常并且使panic异常不会终止程序运行

func fn1() {
    fmt.Println("fn1")
}

func fn2() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("err:", err)
        }
    }()
    panic("抛出异常")
}

func main() {
    fn1()
    fn2()
    fmt.Println("结束")
}
fn1
err: 抛出异常
结束 

make和new

引用数据类型必须使用make函数初始化后才可以使用,如map、slice

var userinfo map[string]string
userinfo["username"] = "user"    // 报错

userinfo := make(map[string]string) // 正确写法

同理,指针也是引用数据类型

var a *int
*a = 100    // 报错

除了前面使用过的make,我们还可以使用new来为引用数据类型分配内存空间

new在实际开发中不常用,使用new函数得到的是一个类型的指针,且该指针对应的值为该类型的零值

例如:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("值: %v 类型:%T 指针变量对应值: %v\n", a, a, *a)
    fmt.Printf("值: %v 类型:%T 指针变量对应值: %v", b, b, *b)
}
值: 0xc00001a0a8 类型:*int 指针变量对应值: 0
值: 0xc00001a0c0 类型:*bool 指针变量对应值: false

new和make的区别

  1. 二者都是用来做内存分配的
  2. make只用于slice、map和channel的初始化,返回的还是这三个引用类型本身
  3. new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

struct

golang中的结构体struct与其他语言的相似,且比起类具有更高的拓展性和灵活性。

golang中通过type来自定义一个结构体

type 类型名 struct{
    字段名 字段类型
}

初始化结构体

结构体在定义后,需要实例化才可以使用

注:结构体名和字段的首字母可以大写也可以小写,大写代表该结构体为共有,可在其他包里使用

type Person struct {
    name string
    age int
    sex string
}
func main() {
    var p1 Person // 实例化
    p1.name = "lihua"
    p1.age = 10
    p1.sex = "male"
    fmt.Printf("Person:%#v type:%T", p1, p1)    // %#v用于输出结构体信息 输出:Person:main.Person{name:"lihua", age:10, sex:"male"} type:main.Person
}

还可以使用new来实例化一个结构体

var p2 = new(Person)
p2.name = "lihua"
p2.age = 10
p2.sex = "male"
fmt.Printf("Person:%#v type:%T", p2, p2)    // Person:&main.Person{name:"lihua", age:10, sex:"male"} type:*main.Person

可以看到通过new关键字实例化的结构体是一个结构体指针。在golang中支持对结构体指针的直接使用.来访问结构体的成员。如p2.name = "lihua"的底层就是(*p2).name = "lihua"

此外,还可以使用取地址符&来实例化一个结构体

var p3 = &Person{}
p3.name = "lihua"
p3.age = 22
p3.sex = "female"
fmt.Printf("Person:%#v type:%T\n", p3, p3)    // Person:&main.Person{name:"lihua", age:22, sex:"female"} type:*main.Person

可以看出这种方法实例化结构体跟使用new是一样的

要在结构体实例化时直接给其成员赋值,可以使用以下方法

p4 := Person{
    name: "zhangsan",
    age: "22",
    sex: "none"}
fmt.Printf("Person:%#v type:%T\n", p4, p4)    // Person:main.Person{name:"zhangsan", age:22, sex:"none"} type:main.Person 

这种通过键值对实例化结构体的方式,也可以使它打印指针类型

p5 := &Person{
    name: "zhangsan",
    age:  22,
    sex:  "none"}
fmt.Printf("Person:%#v type:%T\n", p5, p5)    // Person:&main.Person{name:"zhangsan", age:22, sex:"none"} type:*main.Person

如果在实例化时不给结构体赋值,那么会设置为默认值,如字符串会被设为空,int会被设为0

p6 := &Person{
    name: "zhangsan"}
fmt.Printf("Person:%#v type:%T\n", p6, p6)    // Person:&main.Person{name:"zhangsan", age:0, sex:""} type:*main.Person 

键值对实例化结构体的方法也可以只写值,不过得与结构体内定义的顺序对应

p7 := Person{
    "zhangsan",
    22,
    "none"}
fmt.Printf("Person:%#v type:%T\n", p7, p7)    // Person:main.Person{name:"zhangsan", age:22, sex:"none"} type:main.Person

结构体是值类型,在副本被修改后不会改变变量本身的值

func main() {
    p := Person{name: "zhangsan"}
    p2 := p
    p2.name = "lisi"
    fmt.Printf("%#v\n", p)    // main.Person{name:"zhangsan", age:0, sex:""}
    fmt.Printf("%#v", p2)    // main.Person{name:"lisi", age:0, sex:""}
}

结构体方法

同上所述,go中没有类的概念,但可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。接收者的概念类似于其它语言的this和self

结构体方法的定义如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    ...
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self,this类的命名。如Person的接收者变量应该命名为p,Car的变量该命名为c
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
  • 方法名、参数列表、返回参数:同函数的定义

e.g:实现一个java类的toString方法:

type Person struct {
    name string
    age  int
    sex  string
}

func (p Person) toString() {
    fmt.Printf("name:%v age:%v sex:%v", p.name, p.age, p.sex)
}

func main() {
    p1 := Person{
        name: "lihua",
        age:  0,
        sex:  "male",
    }
    p1.toString()
}

这里的p可以看作java中的this,来操作Person对象(结构体)

接收者函数类型,可以是结构体类型结构体指针类型

e.g:接上例,实现一个java类的setter方法:

func (p *Person) setPerson(name string, age int, sex string) {
    p.name = name
    p.age = age
    p.sex = sex
}
func main() {
    p1 := Person{
        name: "lihua",
        age:  0,
        sex:  "male",
    }
    p1.toString()    // name:lihua age:0 sex:male
    p1.setPerson("zhangsan", 10, "female")
    p1.toString()    // name:zhangsan age:10 sex:female
}

注:如果涉及到修改结构体类型的属性的话,传入的接受者必须是一个结构体指针! 如*Person;如果只用于输出属性,则两者都可使用

这么做是因为结构体是一个值类型,不接受结构体指针进行修改的话,则修改的是结构体对象里面的属性,而无法修改实例的属性。

func (p Person) setPerson(name string, age int, sex string) {
    p.name = name
    p.age = age
    p.sex = sex
}
func main() {
    p1 := Person{
        name: "lihua",
        age:  0,
        sex:  "male",
    }
    p1.toString()    // name:lihua age:0 sex:male
    p1.setPerson("zhangsan", 10, "female")    // 接收者传入的非结构体指针,无法修改实例的值
    p1.toString()    // name:lihua age:0 sex:male
}

为自定义类型添加方法

接受者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

如:我们基于内置的int类型使用type关键字定义新的自定义类型,然后可以为自定义类型添加方法

type myInt int

func (m myInt) get() {
    fmt.Println("这是自定义类型中的自定义方法")
}

func main() {
    var a myInt = 20
    a.get()    // 这是自定义类型中的自定义方法
}

嵌套结构体 & 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种声明方式称为匿名字段

type Person struct {
    string
    int
}
func main(){
    p1 := Person{
        "user",
        123,
    }
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段不能重复

匿名字段常用在嵌套结构体,如:

type User struct {
    Username string
    Password string
    Sex      string
    Age      int
    Address  Address // User结构体嵌套Address
}

type Address struct {
    Name  string
    Phone string
    City  string
}

func main() {
    var u User
    u.Username = "user"
    u.Password = "114514"
    u.Sex = "male"
    u.Age = 114
    u.Address.Name = "LiTianSuo"
    u.Address.Phone = "1145141919810"
    u.Address.City = "ShanTou"
    fmt.Printf("%#v", u)    // main.User{Username:"user", Password:"114514", Sex:"male", Age:114, Address:main.Address{Name:"LiTianSuo", Phone:"1145141919810", City:"ShanTou
"}}

}

上述代码是一段嵌套结构体的演示,如果要使用匿名结构体。在嵌套结构体中,嵌套结构体的字段名一般都是与该结构体名相同。所以这种时候可以直接使用匿名结构体:

type User struct {
    Username string
    Password string
    Sex      string
    Age      int
    Address // 匿名
}

如果将嵌套结构体字段定义为匿名,那么嵌套结构体可以直接访问匿名结构体里的字段

u.City = "Shanghai" // 当访问结构体成员时会现在结构体中查找该字段,找不到再去匿名结构体中查找

字段类型

结构体的字段类型可以是基本数据类型,也可以是切片、Map以及结构体

如果结构体的字段类型时:指针,slice和map的零值都是nil,即尚未分配空间。如需使用,则需要通过make方法

type Person struct{
    sl []string
    map1 map[string]string
}
func main(){
    var p Person
    p.sl = make([]string, 6, 6)
    p.sl[0] = "some string"
    p.map1 = make(map[string]string)
    p.map1["addr"] = "127.0.0.1"
}

继承

go中的继承只需要将将要被继承的父结构体以嵌套结构体的形式写入子结构体字段中即可实现

// Animal 父结构体
type Animal struct {
    name string
}

func (a Animal) run() {
    fmt.Printf("%v 在跑\n", a.name)
}

type Dog struct {
    age    int
    Animal // 结构体嵌套父结构体,表继承
}

func (d Dog) wof() {
    fmt.Printf("%v 在叫\n", d.name)
}

func main() {
    var dog = Dog{
        age:    10,
        Animal: Animal{name: "lzy"},
    }
    dog.run() // 继承父结构体的run方法
    dog.wof() // Dog没有name属性,name是从父结构体Animal中找的
}

sturct <-> Json

要将struct转换为json,可以使用encoding/json包里的Marshal方法

type Student struct {
    Name   string
    Age    int
    Gender string
    pwd    string    // 私有字段 不能被json包读取
}

func main() {
    var s1 = Student{
        Name:   "stu",
        Age:    12,
        Gender: "male",
        pwd:    "114514",
    }
    fmt.Printf("%#v\n", s1)    // main.Student{Name:"stu", Age:12, Gender:"male", pwd:"114514"}

    jsonByte, _ := json.Marshal(s1) // Marshal方法用于将一个变量转换为json bytes
    jsonStr := string(jsonByte)    // 用string方法转换为字符串

    fmt.Printf("%v", jsonStr)    // {"Name":"stu","Age":12,"Gender":"male"}

}

要将json转化为struct实例,可以参考以下代码

func main() {
    var str = `{"Name":"stu","Age":12,"Gender":"male"}`
    var s1 Student
    err := json.Unmarshal([]byte(str), &s1) // <-因为要修改到s1,所以需要使用引用类型
    if err != nil {
        fmt.Printf("%v", err)
    }
    fmt.Printf("%#v", s1) // main.Student{Name:"stu", Age:12, Gender:"male", pwd:""}
}

结构体标签Tag

在使用json包转换struct为json后,转换出的json key首字母都为大写。实际开发中,对应的字段不一定为大写,但是因为Go中只有将结构体字段名写为大写才可供读取,所以读取出的字段全为大写,那么如果要将其转换为小写,就需要用到结构体标签Tag

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

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

e.g:

type Student struct {
    ID     int    `json:"id"`
    Gender string `json:"gender"`
    Name   string
    Sno    string
}

func main() {

    var s1 = Student{
        ID:     0,
        Gender: "male",
        Name:   "stu",
        Sno:    "s001",
    }

    jsonByte, _ := json.Marshal(s1)
    fmt.Printf("%v", string(jsonByte)) // {"id":0,"gender":"male","Name":"stu","Sno":"s001"}

}

附:公有和私有

golang中,定义一个变量、方法或结构体等,都不需要显式地声明该对象是公有或私有。在go中,声明共有和私有都是通过命名首字母大小写来区别。

  • 私有:只有当前包能访问
  • 公有:所有包都能访问

如果要使一个实例变成公有,只需要将其名字首字母大写

var aa = "priavte" // 私有变量,无法被包外访问
var Aa = "public"    // 公有变量

func Add(x, y int) int{    // 公有方法
    return x + y
}

func sub(x, y int) int { // 私有方法,无法被包外调用
    return x - y
}

type Stu struct{    // 公有结构体
    Id int    // 公有结构体字段
    name string    // 私有结构体字段
}

接口

golang中的接口是一种抽象数据类型,golang中接口定义了对象的行为规范,只定义规范不实现,而是由具体的对象来实现。

接口(interface)是一种抽象的类型。它是一组函数method的集合。

接口不能包含任何变量,接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想。

golang中定义接口的格式如下:

type 接口名 interface {
    方法名(参数) 返回值
}

如果接口里面有方法,那么必须通过结构体或者自定义类型来实现这个接口

并且要实现接口的话,必须实现接口中的所有方法

type Usber interface {
    start(string)
    stop()
}

// 如果接口里面有方法,那么必须通过结构体或者自定义类型来实现这个接口
type Phone struct {
    Name string
}

// 要实现接口的话,必须实现接口中的所有方法
func (p Phone) start(a string) {
    fmt.Println(p.Name, a, "启动")
}

func (p Phone) stop() {
    fmt.Println(p.Name, "暂停")
}

func main() {
    p := Phone{Name: "美国"}
    var usb Usber = p    // 让Phone的实例实现了Usber接口
    usb.start("原神")
}

还可以将结构体变量作为参数传入结构体方法来实现接口

type controller interface {
    start(game string)
    close()
}

type Player struct {
    game string
}

type Pc struct {
    name string
}

type Ps struct {
    name string
}

func (p Player) play(c controller) {
    c.start(p.game)
    c.close()
}

func (p Pc) start(game string) {
    fmt.Printf("%v%v,启动!\n", p.name, game)
}

func (p Pc) close() {
    fmt.Printf("%v,关闭\n", p.name)
}

func (p Ps) start(game string) {
    fmt.Printf("%v%v,启动!\n", p.name, game)
}

func (p Ps) close() {
    fmt.Printf("%v,关闭\n", p.name)
}

func main() {
    pc := Pc{name: "电脑"}
    p1 := Player{game: "genshin"}
    p1.play(pc)
    ps := Ps{name: "PS5"}
    p2 := Player{game: "genshit"}
    p2.play(ps)
}

输出:

电脑genshin,启动!
电脑,关闭 
PS5genshit,启动!
PS5,关闭  

空接口

定义的接口中没有任何方法的接口被称为空接口,空接口没有任何约束,任意类型都可以实现空接口

type A interface{} // 空接口

空接口可以被任意类型实现

type A interface{}

func main() {
    var str = "hello"
    var a A = str
    fmt.Printf("%v - %T", a, a)    // hello - string
}

golang中空接口也可以直接当作类型来使用,可以表示任意类型

func main(){
    var a interface{}
  
    // 以下都是允许的
    a = 20
    a = "hello"
    a = true
}

所以,如果函数参数要实现接受任意类型,可以将参数类型定义为接口

func show(a interface{}){
    fmt.Printf("%v", a)
}

包括map,可以让value接受一个空接口,这样map的value就可以是任意类型

map[string]interface{}

注意:空接口类型不支持索引

var user = make(map[string]interface{})
user["user"] = []string{"张三", "李四"}
fmt.Println(user["user"][0]) // 报错:cannot index user["user"] (map index expression of type interface{})

空接口定义的类型方法也不允许调用

type Address struct {
    Name  string
    Phone string
}

func main() {
    var user = make(map[string]interface{})
    user["user"] = []string{"张三", "李四"}
    fmt.Println(user["user"])

    addr := Address{
        Name:  "张三",
        Phone: "114514",
    }
    user["address"] = addr
    fmt.Println(user["address"]) // {张三 114514}

    fmt.Println(user["address"].Name) // user["address"].Name undefined (type interface{} has no field or method Name)

}

要调用则需要使用下文介绍的类型断言

fmt.Println(user["address"].(Address).Name)

切片也可以用空接口定义来接受任意类型

var slice = []interface{1, 2, "hello", true}

类型断言

一个接口的值是有一个具体类型和其值组成的。这两部分分别称为接口的动态类型和动态值

如果我们想要判断空接口中的值的类型,那么这个时候就可以使用类型断言:

x.(T)    // returns: 值,是否对应类型

使用例:

var a interface{}
a = "hello"
v, ok := a.(string)
if ok {
    fmt.Println("a是string,值为:", v)
} else {
    fmt.Printf("非string类型")
}

还可以通过写一个函数来判断类型,称为类型断言(类型断言只能在switch语句中使用)

func typeof(a interface{}){
    switch a.(type) {
    case int:
        fmt.Println("是int类型")
    case string:
        fmt.Println("是string类型")
    case bool:
        fmt.Print("是bool类型")
    }
}

类型断言也可用于结构体

func (p Player) play(c controller) {
    if _, ok := c.(Pc); ok {
        c.start(p.game)
    } else {
        c.close()
    }
}

结构体值接收者&指针接收者

  • 值接收者

    如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量

    type Animal interface{SetName(s string)}
    ...
    func (d Dog) GetName() {
        return d.Name
    }
    ...
    /**
        1、可直接使用结构体值类型赋值
     */
    func main(){
        d := Dog {
            Name:"旺财"
        }
        var animal Animal = d
    }
    /**
        2、也可使用结构体指针类型赋值
     */
    func main(){
        d := &Dog {
            Name:"旺财"
        }
        var animal Animal = d
    }
  • 指针接收者

    如果结构体中的方法是指针接收者,那么实例化后结构体指针类型都可以赋值给接口变量

    type Animal interface{SetName(s string)}
    ...
    func (d *Dog) SetName(s string) {
        d.Name = s
    }
    ...
    /**
        1、不可直接使用结构体值类型赋值
     */
    func main(){
        d := Dog {    // 报错
            Name:"旺财"
        }
        var animal Animal = d // 报错: Dog does not implement Animal(start method has pointer receiver)
    }
    /**
        2、只能使用结构体指针类型赋值
     */
    func main(){
        d := &Dog {
            Name:"旺财"
        }
        var animal Animal = d
    }

实现多个接口

package main

type Animal1 interface {
    SetName(s string)
}

type Animal2 interface {
    GetName() string
}

type Dog struct {
    Name string
}

func (d *Dog) SetName(s string) {
    d.Name = s
}

func (d Dog) GetName() string {
    return d.Name
}

func main() {
    d := Dog{
        Name: "旺财",
    }
    // 定义两个变量来分别实现两个接口
    var d1 Animal1 = &d
    var d2 Animal2 = d

    d1.SetName("旺财")
    println(d2.GetName())

}

接口嵌套

package main

type A interface {
    SetName(s string)
}

type B interface {
    GetName() string
}

// 在Animal接口中嵌套A、B接口
type Animal interface { // 嵌套接口
    A
    B
}

type Dog struct {
    Name string
}

func (d *Dog) SetName(s string) {
    d.Name = s
}

func (d Dog) GetName() string {
    return d.Name
}

func main() {
    d := Dog{ // 报错
        Name: "旺财",
    }
    var dog Animal = &d

    dog.SetName("旺财")
    println(dog.GetName())

}

Goroutines

在Go语言中,每一个并发的执行单元叫作一个goroutine。

假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。

golang中的主线程:在一个golang程序的主线程上可以起多个协程。Golang中多协程,可以实现并行或并发。

协程:可以理解为用户级线程,是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生支持协程。在函数或者方法前加go关键字就可以创建一个协程。可以说golang中的协程就是goroutines

func test(){
    ...
}
func main(){
    go test()    // 开启一个协程
    ...
}

开启协程后,main和test方法将会并发执行

但main的优先级是高于test的,如果main函数比test函数提前结束了,那么程序将会终止。

要避免这种情况发生,可以使用sync.WaitGroup

var wg sync.WaitGroup
func test(){
    ...
    wg.Done() // 协程计数器-1
}
func main(){
    wg.Add(1) // 协程计数器+1
    go test()    // 开启一个协程
    ...
    wg.Wait()    // 等待协程执行结束
}

在main函数中,调用wg.Add()方法来使协程计数器加一,并且调用wg.Wait()方法等待协程执行结束(当协程计数器非0时,main函数不会结束)。当协程方法执行结束后,会调用wg.Done()来使协程计数器减1。若协程计数器为0则终止main函数。

还可以使用go循环来创建多条协程

var wg sync.WaitGroup

func test(num int) {
    for i := 1; i <= 10; i++ {
        fmt.Printf("协程(%v)打印的第%v条数据\n", num, i)
    }
    wg.Done()
}

func main() {
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go test(i)
    }
    wg.Wait()
}

案例:素数统计

利用goroutine统计1-1000000中的素数

分析:可以创建多个协程,分别计算不同的部分,如下

  1. 协程A负责1-250000
  2. 协程B负责250001-500000
  3. 协程C负责500001-750000
  4. 协程D负责750001-1000000

不使用协程的情况下,运行需要花费69秒(Ryzen r7 5700x)

func main() {
    start := time.Now().Unix()
    for i := 2; i < 1000000; i++ {
        var flag = true
        for j := 2; j < i; j++ {
            if i%j == 0 {
                flag = false
                break
            }
        }
        if flag {
            fmt.Println(i, "是素数")
        }
    }
    end := time.Now().Unix()
    fmt.Println("执行时间:", end-start, "秒")
}

使用协程实现

var wg sync.WaitGroup

func calc(start int) {
    for i := start - 250000 + 1; i < start; i++ {
        var flag = true
        for j := 2; j < i; j++ {
            if i%j == 0 {
                flag = false
                break
            }
        }
        if flag {
            fmt.Println(i, "是素数")
        }
    }
    wg.Done()
}

func main() {
    start := time.Now().Unix()
    for i := 1; i < 5; i++ {
        wg.Add(1)
        go calc(i * 250000)
    }
    end := time.Now().Unix()
    wg.Wait()
    fmt.Println("执行时间:", end-start, "秒")
}

Channel管道

管道是go语言在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是go程序并发的知兴替,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

go语言的并发模型是CSP(Communicating Sequential Process),提倡通过通信共享内存而不是通过共享内存而实现通信

go语言中的管道是一种特殊的烈性。管道像一个传送带或队列,总是遵循先进先出(FIFO)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

Channel类型

channel是一种引用类型,声明格式如下:

var 变量 chan 元素类型
// e.g:
var ch1 chan int    // 声明一个传递int类型的管道

创建channel的格式如下:

make(chan 元素类型, 容量)
// e,g:
ch := make(chan int, 3)
  1. 发送(将数据放在管道内):

    将一个值发送到管道中

    ch <- 10    // 把整形10发送到ch中
  2. 接收(从管道内取值):

    从一个管道中接受值

    x := <- ch    // 从ch中接受值并赋值给变量x
    <-ch    // 接受值并忽略结果

    e.g:

    func main() {
        ch := make(chan int, 2)
        ch <- 10
        ch <- 20
        a := <-ch
        fmt.Println(a) // 遵循FIFO原则,先打印10
        b := <-ch
        fmt.Println(b) // 再打印20
        ch <- 30       // 此时管道已空,可以继续存值(容量为2,前面输出2次,故此时容量为0)
        c := <-ch
        fmt.Println(c)    // 30
        fmt.Printf("管道值:%v, 容量:%v, 长度:%v \n", ch, cap(ch), len(ch)) // 管道值:0xc0000c8000, 容量:2, 长度:0 
        // 值为一个地址值; 容量为2 ; 长度为管道内元素个数
    }
  3. 关闭管道:

    管道还支持close操作,用于关闭管道,随后对基于该管道的任何发送操作都将导致panic异常。对一个已经被close过的管道进行接收操作依然可以接受到之前已经成功发送的数据;如果管道中已经没有数据的话将产生一个零值的数据。

    案例:遍历管道

    func main() {
        ch := make(chan int, 10)
        // 使用for循环遍历管道可以不关闭
        for i := 1; i <= 10; i++ {
            ch <- i
        }
    
        close(ch) // 管道传输完毕后需要关闭管道防止死锁
    
        // 使用for range循环管道必须关闭
        for v := range ch {    // for range遍历管道只会返回一个value值
            fmt.Println(v)
        }
    
    }

Goroutine结合Channel管道

案例:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步执行

  1. 开启一个fn1的协程给管道inChan中写入100条数据
  2. 开启一个fn2的协程读取inChan中写入的数据
  3. 两个方法操作同一个管道
var wg sync.WaitGroup

func fn1(ch chan int) {
    for i := 1; i <= 10; i++ {
        ch <- i
    }
    close(ch)
    wg.Done()
}

func fn2(ch chan int) {
    for v := range ch {
        fmt.Println(v)
    }
    wg.Done()
}

func main() {
    wg.Add(2)
    var ch = make(chan int, 10)
    go fn1(ch)
    go fn2(ch)
    wg.Wait()
}

单向管道

有时我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道再函数中只能发送或者接受。

var chan1 chan int    // 可读可写
var chan2 chan<- int    // 只写
var chan3 <-chan int    // 只读

单向管道通常用在对函数参数的限制,在参数上进行限制可以使双向管道在函数中变成单向管道

select多路复用

在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中提供的select多路复用

通常情况通道在接受数据时,如果没有数据可以接收将会发生阻塞。

使用例:

func main() {
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan <- i
    }

    stringChan := make(chan string, 10)
    for i := 0; i < 10; i++ {
        stringChan <- "hello" + fmt.Sprintf("%d", i)
    }

    // 使用select获取channel数据不需要关闭channel
    for {
        select { // select是随机执行case的
        case v := <-intChan:
            fmt.Printf("从intChan读取的数据%d\n", v)
        case v := <-stringChan:
            fmt.Printf("从strChan读取的数据%v\n", v)
        default:
            fmt.Printf("读取完毕")
            return // 使用return退出
        }
    }
}

Gorotine Recover解决协程中出现的Panic

协程在遇到错误时,会直接结束所有运行的协程。如果想要在一个协程遇到错误时其他协程接着运行,就需要用到错误处理

可以通过定义一个匿名函数来捕获协程函数中的错误:

func test(){
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("test() 发生错误", err)
        }
    }
}

并发安全与锁

在golang中,如果同时调用同一个协程,就可能出现资源抢夺的问题。如:

var (
    count = 0
    wg    sync.WaitGroup
)

func test() {
    count++
    fmt.Println(count)
    time.Sleep(time.Millisecond)
    wg.Done()
}

这时就需要给代码加上锁

互斥锁

互斥锁时传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型注意有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,而Unlock进行解锁

var (
    count = 0
    wg    sync.WaitGroup
    mutex sync.Mutex
)

func test() {
    mutex.Lock()
    count++
    fmt.Println(count)
    time.Sleep(time.Millisecond)
    mutex.Unlock()
    wg.Done()
}

读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能,程序有原来的并发执行变成了串行执行。

其实,当我们对一个不会变化的数据制作”读“操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取都是可以的。

所以问题并非出在”读“上,而是”写“(修改)。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另一种锁,读写锁

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。即当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

go中读写锁由结构体类型sync.RWMutex表示。

此类型的方法集合中包含两对方法:

  • 一组是对写操作的锁定和解锁,简称“写锁定”和“写解锁”

    func (*RWMutex)Lock()
    func (*RWMutex)Unlock()
  • 另一组是表示对读操作的锁定和解锁,简称为“读锁定”与“读解锁”

    func (*RWMutex)RLock()
    func (*RWMutex)RUnlock()

反射

什么是反射

反射是指在程序运行期间对程序本身进行访问和修改的能力。

正常情况下程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法自身的信息。支持反射的语言可以在程序编译期将变量的反射信息如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go语言中反射可以实现的功能

  1. 反射可以在程序运行期间动态地获取变量的各种信息,如变量的类型、类别
  2. 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法
  3. 通过反射,可以修改变量的值,可以调用关联的方法

变量信息

go语言中的变量信息是分为两部分的:

  • 类型信息:预先定义好的元信息
  • 值信息:程序运行过程中可动态变化的

在go语言的反射机制中,任何接口值都是由一个具体类型具体类型的值两部分组成的。

例:利用反射获取类型

func getType(t interface{}) {
    v := reflect.TypeOf(t)
    fmt.Println(v)
}

func main() {
    getType(1)
    getType("text")
    getType(23.4)
    getType(true)
}

type Name 和 type Kind

在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型。但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)

type myInt int
type user struct {
    username string
    password string
}

func getType(t interface{}) {
    v := reflect.TypeOf(t)
    fmt.Println(v.Name()) // 类型名称
    fmt.Println(v.Kind()) // 类型种类
    fmt.Println(v)
}

func main() {
    getType("text")
    // string
    // string  
    // string  
  
    getType(myInt(1))
    // myInt   
    // int   
    // main.myInt
  
    getType(user{
        username: "admin",
        password: "123456",
    })
    // user 
    // struct
    // main.user
}

注:指针、数组、切片的类型名称为空

reflect.ValueOf()

reflect.ValueOf()用于获取变量的原始值

func getValue(t interface{}) {
    // 反射获取变量原始值
    v := reflect.ValueOf(t)
    // 使用反射方法进行类型转换
    var vInt = v.Int()
    fmt.Println(vInt + 10)
}

func main() {
    getValue(1)
}

通过反射设置变量的值

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中专用的Elem()方法来获取指针对应的值。

如果直接传入会报panic错误

func reflectSetValue(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(10)    // panic: reflect: reflect.Value.SetInt using unaddressable value
    }
}

func main() {
    var a int64 = 100
    reflectSetValue(a)
}

需要通过指针变量的值来修改变量

func reflectSetValue(x interface{}) {
    v := reflect.ValueOf(x)
    fmt.Println(v.Elem().Kind()) // Elem返回一个传入的指针类型所指向的变量的实际值,再链式调用Kind方法可以获取该变量的类型
}

func main() {
    var a int64 = 100
    reflectSetValue(&a)
}
func reflectSetValue(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(120)
    }
}

func main() {
    var a int64 = 100
    reflectSetValue(&a)
    fmt.Println(a)    // 120
}

结构体反射

与结构体相关的方法

任意值通过reflect.TypeOf()获取反射对象信息后,如果它的类型是结构体,可以通过反射值对象reflect.TypeNumField()Field()房啊获得结构体成员的详细信息

type Student struct {
    Name  string `json:"name" form:"username"`
    Age   int    `json:"age"`
    Score int    `json:"score"`
}

func PrintStructField(s interface{}) {
    // 类型变量
    t := reflect.TypeOf(s)
    // 值变量
    v := reflect.ValueOf(s)
    // 判断参数是否为结构体类型
    if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
        fmt.Println("传入的参数不是一个结构体")
        return
    }
    // 通过类型变量里面的Field可以获取结构体的字段
    // Field方法返回一个StructField结构体,用于存储结构体的信息
    field0 := t.Field(0)
    fmt.Println(field0)                           // {Name  string json:"name" 0 [0] false}
    fmt.Println("字段名称:", field0.Name)             // 字段名称: Name
    fmt.Println("字段类型:", field0.Type)             // 字段类型: string
    fmt.Println("字段Tag:", field0.Tag.Get("json")) // 字段Tag: name
    fmt.Println("字段Tag:", field0.Tag.Get("form")) // 字段Tag: username

    // 通过类型变量里面的FieldByName来获取
    field1, ok := t.FieldByName("Age")
    if ok {
        fmt.Println("字段名称:", field1.Name)             // 字段名称: Age
        fmt.Println("字段类型:", field1.Type)             // 字段类型: int
        fmt.Println("字段Tag:", field1.Tag.Get("json")) // 字段Tag: age
    }

    // 通过类型变量里面的NumField获取到该结构体有几个字段
    var fieldCount = t.NumField()
    fmt.Println("结构体有", fieldCount, "个属性") // 结构体有 3 个属性

    // 通过值变量获取结构体属性对应的值
    fmt.Println(v.FieldByName("Name"))  // zs
    fmt.Println(v.FieldByName("Age"))   // 18
    fmt.Println(v.FieldByName("Score")) // 100

    // 遍历结构体属性
    for i := 0; i < fieldCount; i++ {
        fmt.Printf("属性名:%v 属性值:%v 属性类型:%v 属性Tag:%v\n", t.Field(i).Name, t.Field(i), t.Field(i).Type, t.Field(i).Tag.Get("json"))
    }
}

func main() {
    stu := Student{
        Name:  "zs",
        Age:   18,
        Score: 100,
    }
    PrintStructField(stu)
}

通过类型变量里的Method可以获取结构体的方法

type Student struct {
    Name  string `json:"name" form:"username"`
    Age   int    `json:"age"`
    Score int    `json:"score"`
}

func (s *Student) SetInfo(name string, age int, score int) {
    s.Name = name
    s.Age = age
    s.Score = score
}

func (s Student) Get() {
    fmt.Println("Print something...")
}

func PrintStructField(s interface{}) {
    // 类型变量
    t := reflect.TypeOf(s)
    // 值变量
    v := reflect.ValueOf(s)
    // 判断参数是否为结构体类型
    if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
        fmt.Println("传入的参数不是一个结构体")
        return
    }

    // 通过类型变量里面的Method方法可以获取结构体中的方法
    method0 := t.Method(0)             // 0为定义的方法中方法名按ascii码排序后的首个方法
    fmt.Println(method0)               // {Get  func(main.Student) <func(main.Student) Value> 0}
    fmt.Println("方法名称:", method0.Name) // 方法名称: Get
    fmt.Println("方法类型:", method0.Type) // 方法类型: func(main.Student)

    // 通过类型变量获取这个结构体有多少个方法
    method1, ok := t.MethodByName("Get")
    if ok {
        fmt.Println(method1.Name) // Get
        fmt.Println(method1.Type) // func(main.Student)
    }

    // 通过值变量调用方法 使用Call方法来调用函数 nil表示无参数
    v.Method(0).Call(nil) // Print something...

    // 需要传入参数时
    var params []reflect.Value
    params = append(params, reflect.ValueOf("张三"))
    params = append(params, reflect.ValueOf(22))
    params = append(params, reflect.ValueOf(100))
    v.MethodByName("SetInfo").Call(params)
  
    // 查看方法的数量
    fmt.Println("方法数量:", t.NumMethod()) //方法数量: 2
}

func main() {
    stu := Student{
        Name:  "zs",
        Age:   18,
        Score: 100,
    }
    PrintStructField(&stu)
    fmt.Printf("%#v", stu) // 已经通过反射调用方法修改
}
最后修改:2023 年 12 月 10 日
如果觉得我的文章对你有用,能不能v我50参加疯狂星期四