Go学习总结

文章目录

Go基础学习

go学习笔记

Go 包的概念

包的特点

  1. 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中
  2. 包中首字母大写,可被包外访问,首字母小写只能被包成员访问。该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。
  3. 包名使用简短小写字母

包引用

  1. 绝对路径,包的绝对路径就是 $GOROOT/src 或 $GOPATH/src 后面包的源码的全路径(而不是系统根目录/),我们引用标准包都是使用的绝对路径 “xxx”
  2. 相对路径,当前文件所在目录的相对路径引入包 “./xxx”
  3. 使用mod管理包,则mod.go所在目录root为根目录, “root/xxx”
  4. 几种引用格式
    import "fmt"    // 标准格式
    import F "fmt"  // 别名
    import . "fmt"  // 合并fmt的命名空间,可以不用前缀fmt.
    import _ "fmt"  // 只执行包的初始化函数,不使用包
    import "github.com/net/http" // 导入网络包,go get会去url上下载这个包
    

包初始化

初始化顺序

  1. 根据依赖关系,先初始化未被依赖的包
  2. 初始化包作用域的变量,不是从上到下的顺序,而是根据依赖关系进行初始化
  3. 执行init函数
  4. 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;

init函数特点

  1. init函数没有输入参数,没有返回值

Go语言字符串

go语言字符串
go语言直接使用utf8编码,所以不像其他某些语言,需要将文件转成unicode码存储在内存中。而是直接使用utf8码进行操作。

1. 字符串string类型

  1. string是一个数据类型,是一个常量,只读不能修改,只能转换成[]byte或者[]rune,修改完再转回来。内部实现使用utf8编码,每一个元素类型是uint8。所以对于中文,需要将其转换成rune类型,才能读出一个完整的字符。不能使用&str[0]的方式访问地址。第 i 个字节并不一定是字符串的第 i 个字符,因为对于非 ASCII 字符的 UTF8 编码会要两个或多个字节
  2. 字符串转换成切片会复制一份内存,所以尽量少转换。
  3. 字符串是一个2元数据结构,一个指针指向字节数组的起点,另一个表示字节长度。基于字符串创建的切片,依然是字符串,且指向同一个底层字符数组。
  4. 对字符串进行大量写入时,使用bytes.Buffer类型。 buf.WriteString(“hello”),再使用buf.String()转成字符串。

2. 字节byte和字符rune

  1. go语言使用byte和rune类型来存储字符。string的存储结构就是[]byte,所以len()是按照byte类型去计算数组长度。使用索引访问byte数组读中文会出现乱码,所以如果想要使用索引的方式读中文,需要转换成rune数组。
  2. 使用range遍历string时,不再是逐个字节访问,而是会进行utf8解码,保证读一个完整的utf8字符码。也可以理解为是按照字符遍历,字符可能是一个字节,也可能是两个或三个字节。而不像是byte和rune,是固定长度字节遍历。所以字符串可以理解为一个字符数组,它的元素就是字符

3. 获取字符串长度

最后两种需要转换类型,要进行复制,不建议使用。

strings.Count(str, "") - 1
utf8.RuneCountInString(str)
len([]rune(str))
bytes.Count([]byte(str), nil) - 1

4.字符串和数字转换

  1. 将数字转换成字符串
    fmt.Sprintf("%d", 123)
    
    strconv.Itoa(123)
    strconv.FormatInt(int64(123), 2) // 转换成2进制字符串
    
  2. 字符串转换整数
    strconv.Atoi("123")
    strconv.ParseInt("123", 10, 64) //第三个参数是用于指定整型数的大小;16表示 int16,0则表示int。在任何情况下,返回的结果y总是int64类型.
    

Go语言interface

go语言的接口是实现是解耦合的。当一个结构实现了接口的所有方法,我们就认为这个结构实现了这个接口。原来的c++和java需要先定义接口,再去实现。go可以先实现方法,再去想接口如何定义。

Go语言变量、常量

全局变量

全局变量声明必须以 var 关键字开头

iota特殊常量

  1. iota 的含义是初始化一个计数器,这个计数器的影响范围只能是 const 后括号作用域范围内的常量。
  2. iota 不会自动初始化括号作用域内 iota 前边的常量
  3. iota 默认初始值为 0,我们可以定义的常量初始值从 10 开始

常量组不设置初始值

  1. 在定义常量组时,如果不提供初始值,则表示将使用上行的表达式值,所以第一个常量值必须要有值

GO语言条件语句

switch

  1. switch后面的表达式是可选的,如果没有表达式,则case是一个布尔表达式,而不是一个值,相当于多重 if else
  2. switch 可以是任意支持相等比较运算的类型变量,而不一定是整数

GO语言循环语句

goto

  1. goto语句只能用于函数内部跳转,只能跳到同级或上级作用域。不能跳过内部变量声明语句

continue

  1. 和标签一起使用,用户跳出标签所标识的for语句的本次迭代,标签和continue必须在同一个函数内

break

  1. 和标签一起使用,用于跳出标签所标识的 for 、 switch 、 select 语句的执行,可用于跳出多重循环,但标签和 break 必须在同一个函数内。

GO语言数组

  1. 申明数组必须指定元素类型以及元素个数,数组的长度是固定的。

数组初始化

  1. 默认情况下,数组的每个元素会被初始化为元素类型对应的零值
  2. 不指定长度,根据元素个数设置大小
  3. 指定长度,通过索引进行初始化。
  4. 不指定长度,通过索引初始化,数组长度由最后一个索引值确定。

数组长度

  1. 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两个不同的数组类型。如果两个数组类型相同,可以使用(== 和 !=)来判断是否相等。

Go语言切片

空切片和nil切片

var s1 []int
var s2 []int{}

s1是声明,还没初始化,是nil值,len和cap是0,array是nil,底层没有分配内存空间
s2是初始化了,不是nil值,是空切片,底层分配了内存空间。更推荐第一种写法

append

var a = []int{1,2,3}
a = append([]int{0}, a...)
a = append([]int{1,2}, a...)

copy

copy(destSlice, srcSlice) int

切片删除元素

Go 语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

a = []int{1,2,3,4}
a = append(a[:i], a[i+n:]...)

GO语言map

map初始化

dict := map[string]int{}
dict := make(map[string]int)
dict := map[string]int{"age":18, "grade": 1}
  1. map,切片,

访问map

val,ok := map[key]
val := map[key]

第一种采用断言的方式,当 ok 为 true 时,表示 key 存在于字典中,当 ok 为 false 时,表示 key 不存在于字典中;
第二种情况直接查询 key 值的方式,如果 key 不在 map 种时,返回值是字典定义中 value 的类型默认初始值。如 value 的类型为 int,则默认值为 0,如果 value 类型是指针,则默认是 nil;

字典键类型约束

  1. Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。key 的类型必须支持 == 和 != 两种比较操作符。
  2. 数组、字符串和结构体都支持 == 和 !=,所以也可以作为字典的key

清空map

Go 语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map ,不用担心垃圾回收的效率, Go 语言中的并行垃圾回收效率比写一个清空函数要高效的多。

map的有序遍历

  1. 先对key排序,然后再根据key进行遍历

GO语言指针

  1. go不支持指针的运算,不支持显示指针类型转换。如果非要操作,使用unsafe.Pointer
  2. 指针被定义后没有分配到任何变量时,它的值为 nil

Go语言结构体

go语言的引用类型为slice, map, chan,函数,接口,指针。引用类型不初始化都是nil

普通实例化

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。这种用法是为了更明确地表示一个变量被设置为零值。

空结构体

type Empty struct{}

var c = make(chan Empty)
c <- Empty{}

空结构体类型内存占用为0, 通常使用空结构体作为一个事件信息进行Goroutine之间的通信

结构体拥有引用类型

type T struct {
    t T  	// error
}

type T struct {
    t  *T           // ok
    st []T          // ok
    m  map[string]T // ok
}     

一个类型,它所占用的大小是固定的,因此一个结构体定义好的时候,其大小是固定的。但是,如果结构体里面套结构体,那么在计算该结构体占用大小的时候,就会成死循环。
但如果是指针、切片、map 等类型,其本质都是一个 int 大小(指针,4字节或者8字节,与操作系统有关),即因为指针、map、切片的变量元数据的内存占用大小是固定的,因此该结构体的大小是固定的,类型就能决定内存占用的大小

go语言函数

  1. go语言没有默认参数值,也不能通过参数指定形参,
  2. 不支持函数重载
  3. 不支持命名函数嵌套,但是支持匿名函数嵌套
    func add(a, b int) (sum int) {
        asum := func(x, y int) int {
            return x + y
        }
        return asum(a,b)
    }
    

defer

  1. defer会在return之前执行
package main
func add(x, y int) (z int) {
	defer func() {
		println(z) // 输出: 203
	}()
	z = x + y
	return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}

func main() {
	println(add(1, 2)) // 输出: 203
}
  1. defer函数会被压入runtime实现的defer列表中,会占用内存资源,并且如果for的loop次数很多,这个消耗将很客观。

多个返回值

  1. 多返回值一般将错误类型作为最后一个返回值,多个返回值只能使用多个变量或"_"忽略###

不定参数

  1. go支持不定数目的形式参数,格式 param …type
  2. 类型 …type本质上是一个数组切片
  3. 不定参数必须是函数的最后一个参数
  4. 切片作为参数传递给不定参数,切片名后面加上"…"
  5. 形参为不定参数和切片的函数是不同类型。
  6. 如果是 …interface{} 则可以是不同类型的可变参数

匿名函数

  1. 在go语言中,函数是一等公民,函数类型也是一等的数据类型,简单来说,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。
    而更深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。
  2. 拥有相同的形参和返回值列表,次序,个数和类型都相同的两个函数可以看做是类型相同

GO语言闭包

当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。

闭包记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
```
package main

import (
“fmt”
)

/*
累加器生成函数,这个函数输出一个初始值,
调用时返回一个为初始值创建的闭包函数
*/
func Accumulate(value int) func() int {
// 返回一个闭包,每次返回会创建一个新的函数实例
return func() int {
// 累加,对引用的 Accumulate 参数变量进行累加,注意 value 不是上一行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
value++
// 返回一个累加值
return value
}
}

func main() {
// 创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
// 打印累加器的函数地址
fmt.Printf(“%p\n”, &accumulator) //输出 0xc00000e030

// 创建一个累加器, 初始值为10
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator2)   //输出 0xc00000e040

}
```
可以看出 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。

闭包实现生成器

package main

import (
	"fmt"
)

// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
	// 血量一直为150
	hp := 150
	// 返回创建的闭包
	return func() (string, int) {
		// 将 hp 和 name 变量引用到匿名函数中形成闭包。
		return name, hp
	}
}

func main() {
	// 创建一个玩家生成器
	generator := playerGen("wohu")
	// 返回玩家的名字和血量
	name, hp := generator()
	// 打印值
	fmt.Println(name, hp)   // wohu 150
}

go语言异常处理

一个可能造成错误的函数,需要返回一个错误接口error,如果调用是成功的,错误接口将返回nil,否则返回错误。

go语言中没有try/catch捕获异常,提供了两种错误处理方式

  1. 函数返回error类型对象判断错误
  2. panic异常
    优先使用error,捕获异常。

panic 和 recover

在go中,panic主要有两类来源,一是go运行时,二是开发者通过panic函数主从触发。在panic之前的defer函数依然会被执行。

recover只有在defer中才能生效。当在延迟调用中引发的错误,可被后续的延迟调用捕获,但是仅有最有一个错误可被捕获

package main
import "fmt"
func test() {
	defer func() {
		fmt.Println(recover())
	}()
	defer func() {
		panic("defer panic")
	}()
	panic("test panic")
}
func main() {
	test()
}

Go语言 json

  1. 默认使用结构体成员名字作为Json的对象,只有导出的结构体成员(首字母大写)才会被编码。
  2. Color 成员的 Tag 还带了一个额外的 omitempty 选项,表示当 Go 语言结构体成员为空或零值时不生成该 JSON 对象
  3. json解码时,根据结构的变量名或者tag来对应json的值的。
  4. json解码时,除了使用结构体接收之外,还可以使用map[string]interface{}来接收。使用map接收,更加灵活

Go语言 文件读写

标准库有os和ioutil

os.Create

func Create(name string) (file *File, err Error)

创建一个文件,如果文件存在则清空全部内容。

os.Open

func Open(name	string) (file *File, err Error)

只读模式打开一个文件

os.OpenFile

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)

func OpenFile(name string, flag int, perm uint32)(file *File, err Error)

flag表示打开模式, perm表示权限模式

写入文件

func (file	*File).Write(b []byte)  (n int, err Error)

//从 slice 的某个位置开始写入, 返回写的个数, 错误信息,通过 File 的内存地址调用
//要使用该函数,则不能是O_APPEND模式打开
func (file	*File).WriteAt(b []byte, off int64) (n int, err Error) 

//写入一个字符串, 返回写的个数, 错误信息, 通过 File 的内存地址调用
func (file	*File).WriteString(s string) (ret int, err Error)

读取文件

func (file	*File).Read(b []byte) (n int, err Error)

func (file	*File).ReadAt(b	[]byte, off	int64) (n int, err Error)

删除文件

//传入文件的路径来删除文件,返回错误个数
func Remove(name string)	Error

Go语言 goroutine 并发

Go 语言通过编译器运行时( runtime ),从语言上支持了并发的特性。runtime类似java的虚拟机,主要负责内存管理,垃圾回收,栈处理,协成,信道(channel),切片,字典,和反射等。请添加图片描述
如上图所示,Go 运行时和用户编译后的代码被 Go 链接器(Linker )静态链接起来,形成一个可执行文件。从运行的角度来说,这个 Go 可执行文件由两部分组成:一部分是用户的代码,另一部分就是 Go 运行时。
goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。

MPG模型

Go语言 竞争、锁资源、同步

1. 原子函数

atomic的原子函数可以原子操作变量,LoadInt64() 和StoreInt64()原子的读写一个整型值。使用其创建一个同步标志。

// 这个示例程序展示如何使用atomic包里的
// Store和Load类函数来提供对数值类型
// 的安全访问
package main
import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var (
	shutdown int64
	wg sync.WaitGroup
)

func main() {
	wg.Add(2)

	go doWork("A")
	go doWork("B")
	time.Sleep(1 * time.Second)

	atomic.StoreInt64(&shutdown, 1)
	wg.Wait()
}

// doWork用来模拟执行工作的goroutine,
// 检测之前的shutdown标志来决定是否提前终止
func doWork(name string) {
	defer wg.Done()
	for {
		fmt.Printf("Doing %s Work\n", name)
		time.Sleep(250 * time.Millisecond)
		if atomic.LoadInt64(&shutdown) == 1 {
			fmt.Printf("Shutting %s Down\n", name)
			break
		}
	}
}

2. 互斥锁

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

package main
import (
	"fmt"
	"runtime"
	"sync"
)

var (
	counter int
	wg sync.WaitGroup
	mutex sync.Mutex
)

// main是所有Go程序的入口
func main() {
	wg.Add(2)  // 通常都是在一个地方进行add操作然后在不同的协成中进行done
	go incCounter(1)
	go incCounter(2)
	wg.Wait()
	fmt.Printf("Final Counter: %d\n", counter)
}

// incCounter使用互斥锁来同步并保证安全访问,
// 增加包里counter变量的值
func incCounter(id int) {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		// 同一时刻只允许一个goroutine进入这个临界区
		mutex.Lock()
		{	 // 使用大括号只是为了让临界区看起来更清晰,并不是必需的。
			value := counter
			// 当前goroutine从线程退出,并放回到队列
			runtime.Gosched()
			value++
			counter = value
		}
		mutex.Unlock()
	}
}

3. 读写锁RWMutex

读和读之间不影响,写和写,读和写互斥。

var (
	count int
	countGuard sync.RWMutex
)

func GetCount() int {
	countGuard.RLock()
	defer countGuard.RUnlock()
	return count
}
func SetCount(c int) {
	countGuard.Lock()
	{
		count += c
	}
	countGuard.Unlock()
}

4. channel

channel也是进行goroutine之间通信的方式。

5. goroutine同步 waitgroup

无缓冲channel和原子操作可以实现goroutine之间的同步,waitgroup也能实现同步,
不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行。换句话说,要杜绝对同一个WaitGroup 值的两种操作的并发执行。我们最好用 先统一 Add ,再并发 Done ,最后 Wait 这种标准方式,来使用 WaitGroup 值

go语言 channel 通道

  1. 通道类型的值本身就是并发安全的,这也是go语言自带唯一可以满足并发安全的类型
  2. 通道的发送和接收都是原子操作
    1. 发送操作 包括了 复制元素值放置副本到通道内部 这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。
    2. 接收操作 通常包含了 复制通道内的元素值、放置副本到接收方、 删掉原值 三个步骤,也就是说通常,值进入通道时会被复制一次,然后出通道的时候依照通道内的那个值再被复制一次并给到接收方。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。
  3. 无缓冲通道是同步,有缓冲通道是异步操作。

1. 接收channel数据

有两种方式接收channel数据

  1. data, ok := <-channel 通过ok判断channel是否关闭
  2. for data := range channel 通道关闭时,会退出循环
  3. 从已经关闭的通道接收数据,将会接收到通道类型的零值和false,不会阻塞

被关闭的channel不会被设置为nil,尝试对已经关闭的通道进行发送,会触发panic

2. Select 非阻塞通信

Select 中的case条件只能对chan类型变量进行读写操作。

  1. 在循环中使用select default case 需要避免某个通道被关闭。通道被关闭,就不会再被阻塞,select case就一直处于可用状态。
case value, ok := <-c:
			if !ok {
				c = make(chan int)
				fmt.Println("ch is closed")
			} else {7
				fmt.Printf("value is %#v\n", value)
			}
  1. 在 select 语句与 for 语句联用时,怎样直接退出外层的 for 语句?
  • 可以使用 goto 加 lable 跳转到 for 外面;
  • 可以设置一个额外的标记位,当 chan 关闭时,设置 flag=true ,在 for 的最后判断 flag 决定是否 break ;

3. 使用chan实现信号量

go func(id int) {
			defer wg.Done()
			sem <- 1 // 向 sem 发送数据,阻塞或者成功。
			for x := 0; x < 3; x++ {
				fmt.Println(id, x)
			}
			<-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。
		}(i)

Go语言 type类型

type的作用,1)定义结构体,2)定义接口,3)定义类型,4)类型别名,5)类型查询

1. 类型别名和新类型

type TypeAlias = Type
type newType Type
  1. 类型别名和对应类型属于同一个类型,能够相互赋值和运算。但是类型定义是一种新的类型,本身依然具备Type类型的特性,新类型和底层类型不能相互直接赋值和运算,需要显示转换。
  2. 不能在一个非本地(同一个包)的类型上定义新方法。
  3. 给类型别名新增方法,会添加到原类型方法集中

2. type类型查询

类型查询只能在switch中对interface{}生效,

switch t := a.(type) {
	case string:
		fmt.Println("字符串")
	case int:
		fmt.Println("字符串")
	default :
		fmt.Println("其他类型", v)
}

3. nil值

  1. nil值不能相互比较,不同类型的nil值不能比较,相同类型中 function,slice,map类型的nil值也不能比较
  2. nil 是function,slice,map,chan,pointer,interface的零值

go语言 range

参与循环的是range表达式的副本

func main() {
	a := [3]int{0, 1, 2}
	a := []int{0,1,2}	
	for i, v := range a { // index、value 都是从复制品中取出。
		if i == 0 { // 在修改前,我们先修改原数组。
			a[1], a[2] = 999, 999
			fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
		}
		a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。
	}
	fmt.Println(a) // 输出 [100, 101, 102]。
}

rang a使用的是副本,因为数组类型副本是值拷贝,所以循环内的修改是修改的数组副本。但是slice是引用,副本就是引用本身。所以循环内的修改会影响到外面的切片本身

go语言 make和new的区别

  • make 只能用来分配及初始化类型为 slice 、 map 、 chan 的数据。 new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type 。 make 返回引用,即 Type ;
  • new 分配的空间被清零。 make 分配空间后,会进行初始化;

Go语言结构体

内嵌结构体

  1. 内嵌的结构体可以直接访问期成员变量
  2. 内嵌结构体的字段名是他的类型名字

Go语言数据类型

数据类型

底层类型

所有的类型都有一个底层类型,

  1. 简单类型和复合类型的底层类型是它们自身
  2. 自定义类型 type newtype oldtype 中 newtype 的底层类型是逐层递归向下查找的,直到查到的 oldtype 是简单类型或复合类型为止。
type T1 string
type T3 []string
type T5 []T1

T5 与 T3 、 的底层类型是不一样的, 一个是 []T1 ,另一个是 []string

相同类型和类型赋值

Go语言方法

1. 方法的特点

  1. Go语言对receiver参数的基类型不能使指针类型和接口类型
  2. 为类型增加方法有一个限制,就是方法的定义必须和类型的定义在同一个包中。所以我们不能给原生类型添加方法(int,map等)
  3. 使用 type 定义的自定义类型是一个新类型,新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承。

2. 方法值

f := x.M 
f(args...)
// 等价于
x.M(args...)

变量 x 的静态类型是 T , M 是类型 T 的一个方法, x.M 被称为方法值。方法值是一个带有闭包的函数变量。

Go语言接口

  1. 接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。
  2. Go 1.14 版本以后,Go 接口类型允许嵌入的不同接口类型有相同的方法,并且方法的函数签名也一样(也就是函数名,参数,返回值等保持一致)
  3. 接口名一般以"er"结尾,函数内部方法名不需要以func引导,接口只有申明没有实现
  4. 惯例上尽量定义小接口。
  5. 只能给自定义类型定义方法。非命名类型由于不能定义自己的方法, 所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口

1. 类型断言

var i interface
i.(T)

func main() {
	a := 1
	v, ok := interface{}(a).(int)	// 将 a 转换为接口类型
	if ok {
		fmt.Printf("v type is %T\n", v)
	}
	fmt.Println(a)
}
  1. i必须是接口变量, T可以是接口类型名,也可以是具体类型名
  2. 如果 TypeNname 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 TypeNname
  3. 如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 TypeName 接口

2. 类型赋值

o := i.(TypeName)
  1. TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName ,则变量 o 的类型就是 TypeName , 变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。
  2. TypeName 是接口类型名, 如果接口 i 绑定的实例类型满足接口类型 TypeName ,则变量 o 的类型就是接口类型TypeName , o 底层绑定的具体类型实例是 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本〉。

3. comma,ok表达式

如果类型断言的结果为否,就意味着该类型断言是失败的,失败的类型断言会引发 panic(运行时异常),解决方法

var i1, ok := interface{}(i).(TypeNname)

其中 ok 值体现了类型断言的成败,如果成功,i1 就会是经过类型转换后的 TypeNname 类型的值,否则它将会是 TypeNname 类型的零值(或称为默认值)

4. 类型查询

func main() {
    var x interface{} = 13
    switch v := x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v)
    case string:
        println("the type of v is string, v =", v)
    case bool:
        println("the type of v is bool, v =", v)
    default:
        println("don't support the type")
    }
}

v 存储的是变量 x 的动态类型对应的值信息(而不是类型信息),这样我们在接下来的 case 执行路径中就可以使用变量 v 中的值信息了。

5. 空接口

type Inter interface {
	Ping()
	Pang()
}
type St struct{}
func (St) Ping() {
	println("ping")
}
func (*St) Pang() {
	println("pang")
}
func main() {
	var st *St = nil
	var it Inter = st

	fmt.Printf("%p\n", st) // 0x0
	fmt.Printf("%p\n", it) // 0x0

	if it != nil {
		it.Pang() // pang
		// 下面的语句会导致panic
		// 方法转换为函数调用,第一个参数是St 类型,由于 *St 是nil ,无法获取指针所指的对象值,所以导致panic
		// it.Ping()
	}
}

这个程序暴露出 Go 语言的一点瑕疵, fmt.Printf(“%p\n”, it) 的结果是 0x0 ,但 it! = nil 的判断结果却是 true,

空接口有两个字段, 一个是实例类型, 另一个是指向绑定实例的指针,只有两个都为 nil 时,空接口才为 nil 。上述中,it的指针为nil,而实例类型不为nil。

Go语言 基于Go方法的面向对象(封装、继承、多态)

Go 语言的结构体(struct)和其他语言的类(class)有同等的地位,但 Go 语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

1. 封装

封装其实就是创建一个struct,并实现一些方法。

2. 继承

确切地说,Go 语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go 语言的继承方式采用的是匿名组合的方式。

package main
type Base struct {
	Name string
}
func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }
type Foo struct {
	Base
	...
}
func (foo *Foo) Bar() {
	foo.Base.Bar()
	...
}

上述Foo继承了Base的两个函数,然后从Base类继承并改写了Bar()方法。

2.1 名字冲突
type X struct {
	Name string
}
type Y struct {
	X
	Name string
}

所有的 Y 类型的 Name 成员的访问都只会访问到最外层的那个 Name 变量,X.Name 变量相当于被隐藏起来了。

3. 多态

在 Go 语言中可以使用接口实现这一特征。

Go语言反射

1. 反射的概念

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们

Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。

2. 反射的类型对象 reflect.TypeOf

2.1 反射的类型(Type)和种类(Kind)
  1. Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称
  2. 种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

3. 指针和指针指向的元素

Go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作

4. 使用反射获取结构体的成员类型

结构体通过 reflect.TypeOf() 获得反射对象信息后,可以通过反射值对象的 NumField() 和 Field() 方法获得结构体成员的详细信息。

方法说明
Field(i int) StructField根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机
NumField() int返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机
FieldByName(name string) (StructField, bool)根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机
FieldByIndex(index []int) StructField多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机
FieldByNameFunc(match func(string) bool) (StructField,bool)根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机

1) 结构体字段类型
reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。

type StructField struct {
    Name string          // 字段名
    PkgPath string       // 字段路径
    Type      Type       // 字段反射类型对象
    Tag       StructTag  // 字段的结构体标签
    Offset    uintptr    // 字段在结构体中的相对偏移
    Index     []int      // Type.FieldByIndex中的返回的索引值
    Anonymous bool       // 是否为匿名字段
}

字段说明如下:

  • Name:为字段名称。
  • PkgPath:字段在结构体中的路径。
  • Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
  • Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
  • Index:FieldByIndex 中的索引顺序。
  • Anonymous:表示该字段是否为匿名字段。

5. 获取结构体成员的值

reflect.Value 可以通过函数 reflect.ValueOf 获得,reflect.Value 被定义为一个 struct 结构体:

type Value struct {
	type *rtype
	ptr unsafe.Pointer
	flag

6. 反射值的方法

6.1 通过反射获取值的方法
方法名说明
Interface() interface {}将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool将值以 bool 类型返回
Bytes() []bytes将值以字节数组 []bytes 类型返回
String() string将值以字符串类型返回
6.2 通过反射访问结构体成员值的方法

射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问

方法说明
Field(i int) Value根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机
NumField() int返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机
FieldByName(name string) Value根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机
FieldByIndex(index []int) Value多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机
FieldByNameFunc(match func(string) bool) Value根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机
6.3 通过反射修改变量值

要修改一个变量的值,有几个关键点:传递指针(可寻址),通过 Elem 方法获取指向的值,才可以保证值可以被修改,reflect.Value 为我们提供了 CanSet 方法判断是否可以修改该变量。

func main() {
	s := "hello"
	sv := reflect.ValueOf(&s) // 必须是&s,否则报错
	// panic: reflect: call of reflect.Value.Elem on string Value
	sv.Elem().SetString("world")
	fmt.Println(s) // world
}

那么如何修改 struct 结构体字段的值呢?可总结出以下步骤:

  1. 传递一个 struct 结构体的指针,获取对应的 reflect.Value ;
  2. 通过 Elem 方法获取指针指向的值;
  3. 通过 Field 方法获取要修改的字段;
  4. 通过 Set 系列方法修改成对应的值。
func main() {
	p := person{Name: "wohu", Age: 18}
	ppv := reflect.ValueOf(&p)
	ppv.Elem().Field(0).SetString("张三")
	fmt.Println(p) // {张三 18}
}
type person struct {
	Name string
	Age  int
}

最后再来总结一下通过反射修改一个值的规则。

  • 可被寻址,通俗地讲就是要向 reflect.ValueOf 函数传递一个指针作为参数。
  • 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,而不是私有的,也就是该字段的首字母为大写。
  • 记得使用 Elem 方法获得指针指向的值,这样才能调用 Set 系列方法进行修改。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值