【Go初阶】两万字快速入门Go语言

时间:2024-10-15 07:11:53

初见golang语法

  package main


  import "fmt"


  func main() {
          /* 简单的程序 万能的hello world */
          fmt.Println("Hello Go")
  }
  • 第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
  • 下一行import "fmt"告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数

  • 下一行func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)

注意:这里面go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行

变量声明

单独变量声明

第一种,指定变量类型,声明后若不赋值,使用默认值0

var a int
fmt.Printf(" = %d\n", a)

第二种,根据值自行进行判断变量类型

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a = "abc"
	fmt.Println("a的类型是:", reflect.TypeOf(a))
}

第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误

a := 10
b := "hello"

var c int
c := 20 // 这里会报错,因为 `c` 已经声明过了

总结

package main


import "fmt"


func main() {
        //第一种 使用默认值
        var a int
        fmt.Printf("a = %d\n", a)


        //第二种
        var b int = 10
        fmt.Printf("b = %d\n", b)


        //第三种 省略后面的数据类型,自动匹配类型
        var c = 20
        fmt.Printf("c = %d\n", c)


        //第四种 省略var关键字
        d := 3.14
        fmt.Printf("d = %f\n", d)
}

多变量声明

package main


import "fmt"


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


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


//这种不带声明格式的只能在函数体内声明
//g, h := 123, "需要在func函数体内实现"


func main() {
        g, h := 123, "需要在func函数体内实现"
        fmt.Println(x, y, a, b, c, d, e, f, g, h)


        //不能对g变量再次做初始化声明
        //g := 400


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

常量 

基本使用

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

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

  • 常量的定义格式: const identifier [type] = value
    • 显示定义:const b string = "abc"
    • 隐式定义:const b = "abc"

例如:

package main


import "fmt"


func main() {
   const LENGTH int = 10
   const WIDTH int = 5   
   var area int
   const a, b, c = 1, false, "str" //多重赋值


   area = LENGTH * WIDTH
   fmt.Printf("面积为 : %d\n", area)
   println(a, b, c)   
}

//运行结果
面积为 : 50
1 false str

常量声明枚举

在 Go 语言中没有直接的枚举类型,但是我们可以可以定义一组常量来表示枚举值

const (
    Unknown = 0
    Female = 1
    Male = 2
)
// 数字 0、1 和 2 分别代表未知性别、女性和男性

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

package main

import "unsafe"
const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(a)
)

func main(){
    println(a, b, c)
}

iota标识符

在我们定义一些常量的时候,可能需要给它们进行赋值。在Go语言当中,就可以简化这个赋值操作通过iota标示符,如:

package main

import "fmt"

const (
    Sunday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

func main() {
    today := Monday
    fmt.Println("Today is:", today)
}

iota不仅仅用于自增,还可以根据定义一些百搭是,来进行赋值操作,如:

const (
    d = iota * 2
    e
    f
)

func main() {
    fmt.Println(d) // 0
    fmt.Println(e) // 2
    fmt.Println(f) // 4
}

函数

函数返回多个值

Go的函数可以返回多个值,例如:

package main

import "fmt"

func swap(x, y string) (string, string) {
	return y, x
}
func main() {
	a, b := swap("a", "b")
	fmt.Println(a, " ", b)
}

//执行结果:b   a

init函数和import

init 函数可在package main中,可在其他package中,可在同一个package中出现多次,而main 函数只能在package main中

执行顺序

golang里面保留了两个函数:init函数(能应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义的时候不能有任何参数和返回值

虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:

代码示例:

package InitLib1

import "fmt"

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

package InitLib2

import "fmt"

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

package main

import (
    "fmt"
    _ "GolangTraining/InitLib1"
    _ "GolangTraining/InitLib2"
)

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

func main() {
    fmt.Println("libmian main")
}

//执行结果:
lib1
lib2
libmain init
libmian main

输出的顺序与我们上面图给出的顺序是一致的。那我们现在就改动一个地方,Lib1包导入Lib2,main包不管:

package InitLib1

import (
    "fmt"
    _ "GolangTraining/InitLib2"
)

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

//输出结果:
lib2
lib1
libmain init
libmian main

main包以及Lib1包都导入了Lib2,但是只出现一次,并且最先输出,说明如果一个包会被多个包同时导入,那么它只会被导入一次,而先输出lib2是因为main包中导入Lib1时,Lib1又导入了Lib2,会首先初始化Lib2包的东西

注意:如果需要将当前的包函数提供给外部,需要将函数名首字母大写!!!否则会引用不了!!!

函数参数

函数的参数在传递的时候主要是分为两种类型:值传递指针传递

值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数

/* 定义相互交换值的函数 */
func swap(x, y int) int {
   var temp int
   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/
   return temp;
}

指针传递:指针传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数

/* 定义交换值函数*/
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保持 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

defer

defer类似于我们Java中的finally,它会在我们程序执行的最后执行一些任务defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。

defer作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行:

func Demo(){
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	defer fmt.Println("4")
}
func main() {
	Demo()
}

//运行结果:4 3 2 1

panic和recover

panic(宕机) 和 recover(恢复)是Go语言的两个内置函数,这两个内置函数用来处理Go的运行时错误(runtime errors)。panic 用来主动抛出异常,recover用来捕获panic抛出的异常

引发panic有两种情况:一种是程序主动调用panic()函数,另一种是程序产生运行时错误,由运行时检测并抛出。例如:

示例1:程序主动调用panic,触发宕机,让程序崩溃

func funcA() {
    fmt.Println("func A")
}
 
func funcB() {
    panic("panic in B")
}
 
func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}
运行结果:

func A
panic: panic in B
 
goroutine 1 [running]:
main.funcB(...)
    /home/wangxm/go_work/src/chapter05/demo.go:12
main.main()
    /home/wangxm/go_work/src/chapter05/demo.go:20 +0x96
exit status 2

当在funcB中主动调用了panic 函数后,程序发生宕机直接退出了,同时输出了堆栈和goroutine相关信息,这让我们可以看到错误发生的位置。当panic()触发的宕机发生时,panic后面的代码将不会被执行,但是在panic()函数前面的已经执行过的defer语句依然会在宕机发生时执行defer中的延迟函数。

示例2:在宕机时触发defer语句延迟函数的执行:

func funcA() {
    fmt.Println("func A")
}
 
func funcB() {
    panic("panic in B")
}
 
func funcC() {
    fmt.Println("func C")
}
 
func funcD() {
    fmt.Println("func D")
}
 
func main() {
    defer funcA()
    defer funcC()
    
    fmt.Println("this is main")
    
    funcB()
    
    defer funcD()
}
运行结果:

this is main
func C
func A
panic: panic in B
 
goroutine 1 [running]:
main.funcB(...)
    /home/wangxm/go_work/src/chapter05/demo.go:12
main.main()
    /home/wangxm/go_work/src/chapter05/demo.go:29 +0xca
exit status 2

从运行结果可以看出,当程序流程正常执行到funcB()函数的panic语句时,在panic()函数被执行前,defer语句会优先被执行,defer语句的执行顺序是先进后出,所以funcC()延迟函数先执行,funcA()后执行,当所有已注册的defer语句都执行完毕,才会执行panic()函数,触发宕机,程序崩溃退出,因此程序流程执行不到funcD()函数。

发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈信息,直到被recover捕获或运行到最外层函数而退出。如本例中,程序从funcB()函数返回到上层的main()函数中,然后执行已注册的defer语句的延迟函数,最后从main函数中退出,并且打印了退出状态码的值为2

recover()函数用来捕获或者说是拦截panic的,阻止panic继续向上层传递。无论是主动调用panic()函数触发的宕机还是程序在运行过程中由Runtime层抛出的异常,都可以配合defer 和 recover 实现异常捕获和恢复,让代码在发生panic后能够继续执行

Go语言没有异常系统,其使用panic触发宕机类似于其他语言的抛出异常,而recover的宕机恢复机制就对应try...catch机制

示例:使用recover捕获panic异常,恢复程序的运行:

func funcA() {
    fmt.Println("func A")
}
 
func funcB() {
    defer func(){
        //捕获panic,并恢复程序使其继续运行
        if err := recover(); err != nil {
            fmt.Println("recover in funcB")
        }
    }()
 
    panic("panic in B")  //主动抛出异常
}
 
func funcC() {
    fmt.Println("func C")
}
 
func funcD() {
    fmt.Println("func D")
}
 
func main() {
    defer funcA()
    defer funcC()
    
    fmt.Println("this is main")
    
    funcB()
    
    defer funcD()
}

运行结果:

this is main
recover in funcB
func D
func C
func A

当recover捕获到panic时,不会造成整个进程的崩溃,它会从触发panic的位置退出当前函数,然后继续执行后续代码

IF判断

在Go语言中,if语句用于条件判断,它有以下几种常见的用法和特点:

基本用法

  • 语法结构
    if condition {
        // 当条件为真时执行的代码块
    }
    
    其中condition是一个布尔表达式,如果condition的值为true,则执行花括号内的代码块。
  • 示例
    num := 10
    if num > 5 {
        fmt.Println("num大于5")
    }
    

带有else子句

  • 语法结构
    if condition {
        // 当条件为真时执行的代码块
    } else {
        // 当条件为假时执行的代码块
    }
    
  • 示例
    num := 3
    if num > 5 {
        fmt.Println("num大于5")
    } else {
        fmt.Println("num小于等于5")
    }
    

带有else if子句

  • 语法结构
    if condition1 {
        // 当条件1为真时执行的代码块
    } else if condition2 {
        // 当条件2为真时执行的代码块
    } else {
        // 当条件1和条件2都为假时执行的代码块
    }
    
  • 示例
    num := 7
    if num > 10 {
        fmt.Println("num大于10")
    } else if num > 5 {
        fmt.Println("num大于5且小于等于10")
    } else {
        fmt.Println("num小于等于5")
    }
    

if语句中声明和初始化变量

  • 语法结构
    可以在if语句的条件判断部分同时声明和初始化一个变量,这个变量的作用域仅限于if语句及其相关的elseelse if子句
    if var_declaration := expression; condition {
        // 当条件为真时执行的代码块,且可以使用var_declaration变量
    }
    
  • 示例
    if num := 8; num > 5 {
        fmt.Println("num大于5,num的值为:", num)
    }
    
    在这个例子中,numif语句中被声明和初始化,并且只能在if相关的代码块中使用。如果numif语句外已经存在,那么在if语句中使用这种方式声明num会导致编译错误,因为if语句中这种方式的变量声明会被视为一个新的局部变量声明

循环

在Go语言中,有两种主要的循环结构:for循环和range循环(range可以看作是一种特殊的基于for循环的便利形式,用于迭代容器类型的数据)。

for循环

  1. 基本for循环

    • 语法结构
      for initialization; condition; post {
          // 循环体
      }
      
      • initialization:循环开始前的初始化操作,通常用于声明和初始化循环变量。
      • condition:循环的条件判断,只要该条件为true,循环就会继续执行。
      • post:每次循环结束后执行的操作,通常用于更新循环变量。
    • 示例
      for i := 0; i < 5; i++ {
          fmt.Println(i)
      }
      
      这个循环会从i = 0开始,只要i < 5就会执行循环体,每次循环结束后i的值会增加1,最终会打印出04这几个数字。

range循环

  1. 用于迭代数组和切片

    • 语法结构
      for index, value := range arrayOrSlice {
          // 对索引和值进行处理
      }
      
      其中index是数组或切片元素的索引,value是元素的值。对于每个元素,循环都会执行一次。
    • 示例
      arr := []int{1, 2, 3, 4, 5}
      for index, value := range arr {
          fmt.Printf("索引为 %d 的元素值为 %d\n", index, value)
      }
      
      这个循环会遍历切片arr,并打印出每个元素的索引和值。
  2. 用于迭代字符串

    • 字符串在Go语言中可以看作是一个字节数组,所以也可以用range循环来迭代。
    • 语法结构
      for index, value := range stringValue {
          // 对索引和值进行处理
          // 需要注意的是,这里的价值可能是一个字节(对于ASCII字符)或者是一个Unicode码点的第一个字节(对于非ASCII字符)
          // 如果要获取完整的Unicode码点,需要进一步处理
      }
      
    • 示例
      str := "hello"
      for index, value := range str {
          fmt.Printf("索引为 %d 的字符值为 %c\n", index, rune(value))
      }
      
      这里使用rune函数将字节值转换为Unicode码点对应的字符,以便正确打印出字符串中的字符。
  3. 用于迭代映射(map)

    • 语法结构
      for key, value := range mapValue {
          // 对键和值进行处理
      }
      
      其中key是映射的键,value是对应键的值。对于每个键值对,循环都会执行一次。
    • 示例
      m := map[string]int{"a": 1, "b": 2, "c": 3}
      for key, value := range m {
          fmt.Printf("键为 %s 的值为 %d\n", key, value)
      }
      
      这个循环会遍历映射m,并打印出每个键值对的键和值。

集合

集合分为slice和map两种,其中slice是数组的抽象。

slice

slice是数组的抽象,Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大

方式一:声明一个未指定大小的数组来定义切片

var identifier []type

方式二:使用make()函数来创建切片

var slice1 []type = make([]type, len)
也可以简写为
slice1 := make([]type, len)

//也可以指定容量,其中capacity为可选参数
make([]T, length, capacity)

 make函数参数说明

  • length
    • 表示切片初始的长度,即切片中元素的个数。这个值决定了切片创建后可以直接访问的元素范围。例如,如果length3,那么可以通过切片索引012来访问元素
  • capacity
    • 表示切片的容量,它是切片底层数组的大小。切片的容量必须大于等于长度。容量决定了切片在不重新分配内存的情况下能够扩展的最大程度。例如,如果capacity5,长度为3,那么在不重新分配内存的情况下,切片可以通过append操作最多再容纳2个元素

操作方法:

append函数

newSlice := append(slice, elements...)

其中slice是原始切片,elements是要添加的一个或多个元素。append函数会返回一个新的切片,这个新切片包含了原始切片的所有元素以及新添加的元素

copy函数

/* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)

len函数

//获取切片长度
len(slice)

cap函数

//获取切片容量
cap(slice)

切片截取:可以通过设置下限及上限来设置截取切片[lower-bound:upper-bound]

package main

import "fmt"

func main() {
	/* 创建切片 */
	numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	printSlice(numbers)

	/* 打印原始切片 */
	fmt.Println("numbers ==", numbers)

	/* 打印子切片从索引1(包含) 到索引4(不包含)*/
	fmt.Println("numbers[1:4] ==", numbers[1:4])

	/* 默认下限为 0*/
	fmt.Println("numbers[:3] ==", numbers[:3])

	/* 默认上限为 len(s)*/
	fmt.Println("numbers[4:] ==", numbers[4:])

	numbers1 := make([]int, 0, 5)
	printSlice(numbers1)

	/* 打印子切片从索引  0(包含) 到索引 2(不包含) */
	number2 := numbers[:2]
	printSlice(number2)

	/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
	number3 := numbers[2:5]
	printSlice(number3)

}

func printSlice(x []int) {
	fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

运行结果:

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

map

映射(map)是Go语言中一种用于存储键值对的数据结构。以下是一些常见的map操作方法:

1. 创建map

  • 可以使用make函数创建map,语法为make(map[KeyType]ValueType),其中KeyType是键的类型,ValueType是值的类型。例如m := make(map[string]int)创建了一个键为字符串类型,值为整数类型的map。

2. 插入键值对

  • 可以直接给map赋值来插入键值对。例如m := make(map[string]int); m["key1"] = 1,这里将键为"key1",值为1的键值对插入到map中。

3. 访问键值对

  • 使用键来访问map中的值。例如m := make(map[string]int); m["key1"] = 1; fmt.Println(m["key1"])会打印出键为"key1"的值1
  • 如果访问的键不存在于map中,会返回值类型的零值。例如m := make(map[string]int); fmt.Println(m["nonexistent_key"])会打印出整数类型的零值0

4. 修改键值对

  • 可以使用键来修改map中的值。例如m := make(map[string]int); m["key1"] = 1; m["key1"] = 2; fmt.Println(m["key1"])会将键为"key1"的值从1修改为2

5. 删除键值对

  • 使用delete函数来删除键值对,语法为delete(map, key),其中map是要操作的map,key是要删除的键。例如m := make(map[string]int); m["key1"] = 1; delete(m, "key1"); fmt.Println(m["key1"])会删除键为"key1"的值,并返回整数类型的零值0,因为该键值对已经被删除。

6. 检查键是否存在

  • 可以使用一种特殊的语法来检查键是否存在于map中,同时获取对应的值。例如m := make(map[string]int); m["key1"] = 1; value, exists := m["key1"]; fmt.Println(value, exists)会打印出1 true,因为键"key1"存在于map中,并且对应的值为1。如果键不存在,exists会返回falsevalue会返回值类型的零值。

7. 遍历map

  • 使用range来遍历map,会依次获取map中的键值对。例如m := make(map[string]int); m["key1"] = 1; m["ive had no joy with maps in GoLang!": "Here is an example of a text that could be inserted into a map, though it might not be a very practical one"]; for key, value := range m { fmt.Println(key, value) }会遍历map中的键值对并打印出来。

8. map的长度

  • 使用len函数可以获取map的长度,即map中键值对的个数。例如m := make(map[string]int); m["key1"] = 1; m["key2"] = 2; fmt.Println(len(m))会打印出2,因为这个map中有两个键值对

结构体

在 Go 语言中,结构体(struct)是一种复合数据类型,它允许用户将不同类型的数据组合在一起,形成一个新的类型。以下是关于结构体的详细介绍:

结构体的定义

结构体的定义一个是使用struct关键字,基础语法如下:

type StructName struct {
    Field1 Type1
    Field2 Type2
    //...
}

其中,type是关键字用于定义新的类型,StructName是结构体的名称,struct是结构体的关键字,后面跟着大括号,大括号内是结构体的各个字段(field),每个字段由字段名和字段类型组成。例如:

type Person struct {
    Name string
    Age  int
}

定义了一个名为Person的结构体,它包含两个字段:Name(字符串类型)和Age(整数类型)

结构体变量的创建和初始化

  • 方法一:逐个字段初始化
    可以通过结构体类型创建结构体变量,并逐个初始化结构体的字段。例如:
var p Person
p.Name = "Alice"
p.Age = 25
  • 方法二:使用结构体字面量初始化
    使用结构体字面量可以在创建结构体变量的同时初始化结构体的字段。有两种形式:
  • 按字段顺序初始化
p := Person{"Bob", 30}

这种方式要求按照结构体定义时字段的顺序提供值。

  • 指定字段名初始化
p := Person{Name: "Charlie", Age: 35}

这种方式可以不按照结构体定义时字段的顺序提供值,只要指定正确的字段名和值即可。

结构体的嵌套

  • 结构体可以嵌套其他结构体。例如:
type Address struct {
    City    string
    Street  string
    ZipCode int
}

type Employee struct {
    Person
    JobTitle  string
    Address   Address
    Salary    int
}

这里定义了Address结构体和Employee结构体,Employee结构体嵌套了Person结构体和Address结构体。当访问嵌套结构体的字段时,可以使用.操作符进行多级访问。例如:e := Employee{Person{"David", 40}, "Engineer", Address{"New York", "5th Avenue", 10001}, 80000}; fmt.Println(e.Person.Name)会打印出David

结构体方法

  • 可以为结构体定义方法,结构体方法类似于面向对象语言中的类方法。结构体方法的定义语法如下:
func (s StructName) MethodName() ReturnType {
    // 方法体
}

其中,(s StructName)部分称为接收者(receiver),它表示该方法是属于StructName结构体的,s是接收者变量,可以在方法体中使用接收者变量来访问结构体的相关字段。例如:

func (p Person) GetName() string {
    return p.Name
}

定义了一个Person结构体的方法GetName,它返回Person结构体的Name字段的值。

结构体的内存布局

  • 结构体在内存中的布局是按照字段定义的顺序依次排列的,每个字段占用一定的内存空间。不同类型的字段在内存中所占的空间大小不同,例如,整数类型通常占用4个字节(在32位系统中)或8个字节(在62位系统中),字符串类型的内存占用则比较复杂,它包含一个指针和一些其他信息。
  • 当结构体嵌套其他结构体时,嵌套的结构体的内存空间也是按照其自身的字段定义顺序依次排列在主结构体的内存空间内。

结构体的比较

  • 如果结构体的所有字段都是可比较的,那么结构体本身也是可比较的。比较两个结构体时,会逐个比较结构体的各个字段。例如:
p1 := Person{"Alice", 25}
p2 := Person{"Alice", 25}
if p1 == p2 {
    fmt.Println("p1和p2相等")
}

如果结构体中有不可比较的字段(如切片、映射等),那么结构体本身不可比较

接口

接口定义

  • 首先我们定义了一个接口Shape
type Shape interface {
    Area() float64
}

这个接口规定了任何实现它的类型都必须有一个Area方法,该方法没有参数并且返回一个float64类型的值。这就像是一个合同,规定了实现这个接口的类型需要具备计算面积的能力。

接口实现

  • 然后我们有一个Rectangle结构体:
type Rectangle struct {
    length float64
    width  float64
}

并且为这个结构体定义了一个Area方法:

func (r Rectangle) Area() float64 {
    return r.length * r.width
}

通过定义这个Area方法,Rectangle结构体满足了Shape接口的要求,也就是说Rectangle结构体实现了Shape接口。这就好比Rectangle结构体签署了Shape接口这个合同,它具备了按照合同要求计算面积的能力。

接口使用

  • 作为函数参数:我们定义了一个函数CalculateArea
func CalculateArea(s Shape) float64 {
    return s.Area()
}

在这个函数中,参数sShape接口类型。这意味着我们可以传入任何实现了Shape接口的类型的值作为参数。当我们调用这个函数时,如果传入的是Rectangle结构体的值(因为Rectangle实现了Shape接口),那么在函数内部就会调用Rectangle结构体的Area方法来计算面积。

  • 作为变量类型:我们还可以定义一个Shape接口类型的变量s
var s Shape
r := Rectangle{length: 5, width: 3}
s = r
fmt.Println(s.Area())

这里首先定义了一个Shape接口类型的变量s,然后创建了一个Rectangle结构体的值r,并将r赋值给s。因为Rectangle实现了Shape接口,所以这种赋值是合法的。最后我们可以通过s调用Area方法来计算面积,实际上是调用了Rectangle结构体的Area方法。

这样通过接口,我们可以实现不同类型之间的通用性和多态性,使得代码更加灵活和可维护。例如,如果我们还有一个Square结构体也实现了Shape接口,那么我们可以同样使用CalculateArea函数来计算它的面积,而不需要为Square重新定义一个计算面积的函数。

面向对象的特性

Go 语言虽然不是纯粹的面向对象编程语言,但它支持一些面向对象的特性,主要包括封装、继承和多态,以下是具体介绍:

封装性

定义:封装是指将数据和操作数据的方法组合在一起,并对外部隐藏数据的实现细节,只提供必要的接口来访问和操作数据

实现方式:通过结构体和方法实现,结构体用于定义数据结构,将相关的数据组合在一起。例如,定义一个Rectangle结构体来表示矩形的长和宽:

type Rectangle struct {
       length float64
       width float64
}

方法用于操作结构体中的数据。例如,为Rectangle结构体定义一个计算面积的方法:

func (r Rectangle) Area() float64 {
   return r.length * r.width
}

通过这种方式,Rectangle结构体的内部数据(长和宽)被封装起来,外部只能通过Area方法来获取矩形的面积,而无法直接访问长和宽的数据

看下面的例子会更加清晰:

package main


import "fmt"


//定义一个结构体
type T struct {
    name string
}


func (t T) method1() {
    t.name = "new name1"
}


func (t *T) method2() {
    t.name = "new name2"
}


func main() {


    t := T{"old name"}


    fmt.Println("method1 调用前 ", t.name)
    t.method1()
    fmt.Println("method1 调用后 ", t.name)


    fmt.Println("method2 调用前 ", t.name)
    t.method2()
    fmt.Println("method2 调用后 ", t.name)
}

运行结果:

method1 调用前  old name
method1 调用后  old name
method2 调用前  old name
method2 调用后  new name2

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

当调用t.method2()=>method2(t),这是将 T 类型传给了 *T 类型,go可能会取 t 的地址传进去:method2(&t)。所以 method1() 的修改可以影响 t。

所以说下面的代码保证了封装性:

func (t T) method1() {
    t.name = "new name1"
}

继承

在Go语言中,虽然没有像传统面向对象语言中那样的类继承机制,但可以通过结构体嵌套实现类似继承的效果。以下是详细说明:

基本概念

  • 继承是一种机制,它允许一个类型(子类或派生类)继承另一个类型(父类或基类)的属性和方法,从而实现代码的复用和扩展。子类可以在继承父类的基础上添加自己的特性,并且可以重写父类的某些方法以满足自身的需求。

Go语言中的实现方式 - 结构体嵌套

  • 假设我们有一个Base结构体代表基类,它具有一些属性和方法。例如:
type Base struct {
    Property1 string
    Property2 int
}

func (b Base) Method1() {
    // 基类方法的实现
}
  • 现在我们想要创建一个Derived结构体来继承Base结构体的特性。我们可以通过结构体嵌套来实现:
type Derived struct {
    Base
    AdditionalProperty string
}
  • 通过这种嵌套方式,Derived结构体继承了Base结构体的所有属性和方法。例如,Derived结构体可以直接访问Base结构体的Property1Property2属性,并且可以调用Base结构体的Method1方法。

方法重写(覆盖)

  • 在Go语言中,虽然没有严格的方法重写语法,但通过结构体嵌套和方法定义,可以实现类似的效果。
  • 假设我们想要在Derived结构体中重写Base结构体的Method1方法。我们可以在Derived结构体中重新定义一个Method1方法:
func (d Derived) Method1() {
    // 新的方法实现,可能会调用原始Base结构体的方法或者完全替代它
    // 例如,可以先调用原始Base结构体的方法,然后再添加一些额外的操作
    d.Base.Method1()
    // 其他额外操作
}
  • 这样,当我们调用Derived结构体的Method1方法时,执行的是我们在Derived结构体中重新定义的方法,而不是Base结构体的原始方法。

注意事项

  • 虽然结构体嵌套实现了类似继承的功能,但它与传统的继承机制还是有一些区别。例如,在Go语言中,没有像其他语言中那样的类层次结构和多态性的严格语法定义。
  • 这种结构体嵌套的方式在实际应用中需要谨慎使用,要确保代码的可读性和可维护性。如果嵌套过于复杂,可能会导致代码难以理解和维护。

多态

定义:多态是指不同类型的对象对同一方法调用可以表现出不同的行为。

实现方式:通过接口实现多态,首先定义一个接口,接口中规定了一些方法,但没有具体的实现。例如,定义一个Shape接口,要求实现Area方法:

type Shape interface {
    Area() float64
}

然后让不同的结构体实现这个接口。例如,RectangleSquare结构体都可以实现Shape接口:

func (r Rectangle) Area() float64 {
	return r.length * r.width
}
func (s Square) Area() float64 {
	return s.sideLength * s.sideLength
}

当使用接口类型的变量时,根据实际赋值的结构体不同,调用Area方法会表现出不同的行为。例如:

var s Shape
r := Rectangle{length: 5, width: 3}
s = r
fmt.Println(s.Area()) // 输出15
s1 := Square{sideLength: 4}
s = s1
fmt.Println(s.Area()) // 输出16

通过接口实现了多态,使得程序可以根据不同的对象类型灵活地执行相应的操作。