目录

环境:go1.21.1

安装

go 1.11之前代码必须在GOPATH中写
# 不设置无法安装包
go env -w GOPROXY=http://goproxy.cn,direct
# 或 export GOPROXY=http://goproxy.cn,direct
go version

go mod init xxxx
# linux
vim /etc/profile
# 在最后一行添加
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
# 保存退出后source一下(vim 的使用方法可以自己搜索一下)
source /etc/profile

# 
go env -w GO111MODULE=on
go env -w GOPROXY="http://goproxy.cn"

# windows
将安装目录添加到path中,手动修改

常用命令

build 	# 编译包和依赖
clean	# 移除对象文件
env		# 打印环境信息
bug		# 启动错误报告
fix		# 运行go tool fix
fmt		# 运行gofmt进行格式化
generate# 从processing source生成go文件
get		# 下载并安装包和依赖
install	# 编译并安装包和依赖
list	# 列出包
run		# 编译并运行go程序
test	# 运行测试
tool	# 运行go提供得工具
version	# 显示go版本
vet		# 运行go tool vet

一、数据类型

布尔、数字型(整型int和浮点型float32、float64,go支持整型和浮点型数字,并且支持复数,其中位的运算采用补码)、字符串(go的字符串是由单个字节连接起来的,go语言的字符串的字节使用utf-8编码标识unicode文本)、派生型(指针、数组、结构化类型struct、Channel类型、函数类型、切片类型、接口类型、切片类型、接口interface、Map类型)

go也有基于架构的类型:int(32位系统就是int32,64就是int64),uint(同int,uint32,uint64),uintptr(无符号整数,用于存放一个指针)

布尔类型:bool

整型:uint8,uint16,uint32,int8,byte, int16,int32,int64,uint,int,uintptr

浮点型:float32,float64,

复数类型:complex64,complex128

字符串:string

字符类型:rune

错误类型:error

复合类型:

指针(pointer)
数组(array)
切片(slice)
字典(map)
通道(chan)
结构体(struct)
接口(interface)

// 格式化输出,2,8,10,16进制的数据
fmt.Printf("%b   %o   %d   %x")
// 查看单个字符
fmt.Printf("%c")
// 查看浮点数最大值
import "math"
fmt.Println(math.MaxFloat32)
fmt.Println(math.MaxFloat64)

// 查看变量类型
fmt.Println("%T\n", strings.Split(s1, "5"))
// 完整打印变量信息
fmt.Printf("p=%#v\n", p)

1.布尔值

默认是false,go语言中不允许将整型强制转换为布尔型,无法参与数值运算,也无法和其他类型转换。

var b bool
b = 1 // 编译错误
b = bool(1) // 编译错误
以下的用法才是正确的:
var b bool
b = (1!=0) // 编译正确

2.字符串

多行字符串:两个反引号

s := `

`

常用方法:

s1 := "123"
s2 := "456"

// 长度
len(s1)
// 拼接
s1 + s2
// 分割
strings.Split(s2, "5")
// 是否包含
strings.Contains
// 前缀后缀判断
strings.HasPrefix, strings.HasSuffix
// 字串出现的位置
strings.Index(), strings,LastIndex()
// join操作
strings.Join(a[]string, sep string)
str := "Hello world" // 字符串也支持声明时进行初始化的做法
str[0] = 'X'	// 编译错误
ch := str[0]	// 取字符串的第一个字符

Go编译器支持UTF-8的源代码文件格式。这意味着源代码中的字符串可以包含非ANSI的字
符,比如“Hello world. 你好,世界!
”可以出现在Go代码中。但需要注意的是,如果你的Go代
码需要包含非ANSI字符,保存源文件时请注意编码格式必须选择UTF-8。特别是在Windows下一
般编辑器都默认存为本地编码,比如中国地区可能是GBK编码而不是UTF-8,如果没注意这点在
编译和运行时就会出现一些意料之外的情况。
字符串的编码转换是处理文本文档(比如TXT、XML、HTML等)非常常见的需求,不过可
惜的是Go语言仅支持UTF-8和Unicode编码。对于其他编码,Go语言标准库并没有内置的编码转
换支持。不过,所幸的是我们可以很容易基于iconv库用Cgo包装一个。这里有一个开源项目:
https://github.com/xushiwei/go-iconv。

// 字符串操作
x + y	// 字符串连接
len(s)	// 长度
s[i]	// 取字符

字符串遍历

// 以字节数组的方式遍历
str := "Hello,世界"
n := len(str)
for i := 0; i < n; i++ {
ch := str[i] // 依据下标取字符串中的字符,类型为byte
fmt.Println(i, ch)
}
// 这个字符串长度为13。尽管从直观上来说,这个字符串应该只有9个字符。这是
//因为每个中文字符在UTF-8中占3个字节,而不是1个字节

//Unicode字符遍历:
str := "Hello,世界"
for i, ch := range str {
fmt.Println(i, ch)//ch的类型为rune
}

// 以Unicode字符方式遍历时,每个字符的类型是rune (早期的Go语言用int类型表示Unicode字符) ,而不是byte。

3.字符

byte(实际上是uint8的别名), 代表UTF-8字符串的单个字节的值,另一个是rune,代表单个Unicode字符。

关于 rune 相关的操作,可查阅Go标准库的 unicode 包。另外 unicode/utf8 包也提供了UTF8和Unicode之间的转换。出于简化语言的考虑,Go语言的多数API都假设字符串为UTF-8编码。尽管Unicode字符在标准库中有支持,但实际上较少使用。

func main() {
    var v1 byte = 'c'
    var c2 rune = 'c'
    fmt.Println(c1, c2)
    fmt.Println("c1:%T  c2:%T\n", c1, c2)
}
s := "hello北京"
for i := 0; i < len(s); i++ {
    fmt.Printf("%c\n", s[i])
}

for _, r := range s {
    fmt.Printf("%c\n", r)
}

4.整型

类型 长度(字节) 值范围
int8 1 -128~127
uint8(即byte) 1 0~255
int16 2 -32768~32767
uint16 2 0~65535
int32 4 -2 147 483 648 ~ 2 147 483 647
uint32 4 0~4294967295
int64 8 -9223372036854775808~9223372036854775807
uint64 8 0~18446744073709551615
int 平台相关 平台相关
uint 平台相关 平台相关
uintptr 同指针 在32位平台下为4字节,64位为8字节

两个不同类型的整型数不能直接比较,比如int8类型的数和int 类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量(literal)进行比较

位运算

运算 含义
x << y 左移 124 << 2 //结果为796
x >> y
x ^ y 异或
x & y
x | y
^x 取反

5.浮点

两个类型 float32和float64 ,其中 float32等价于C语言的 float类型,float64等价于C语言的double类型。

var fvalue2
fvalue2 := 12.0	// 不加小数点,会被推导为整型而不是浮点型,这个将被自动设为float64

// 浮点数比较
import "math"
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
	return math.Fdim(f1, f2) < p
}

6.复数

var value1 complex64
 // 由2个float32构成的复数类型
value1 = 3.2 + 12i
value2 := 3.2 + 12i
value3 := complex(3.2, 12)
// value2是complex128类型
// value3结果同 value2

对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实部,也就是x,通过imag(z)获得该复数的虚部,也就是y

7.map

fmt.Println("--------------------map-------------------------")
// 定义一个map, 键类型为string,值为PersonInfo
var personDB map[string]PersonInfo
// 指定大小为100
personDB = make(map[string]PersonInfo, 100)
// 往这个map中插入几条数据
personDB["1234"] = PersonInfo{"1234", "Tom", "Room 203..."}
personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}
// 查找key为"1234"的信息
person, ok := personDB["1234"]
// ok 返回的是bool,返回true表示找到了对应的数据
if ok {
    fmt.Println("Found person", person.Name, "with ID 1234.")
} else {
    fmt.Println("Did not find person with ID 1234.")
}

// myMap是声明的map变量名, string是键的类型, PersonInfo则是其中所存放的值类型。
var myMap map[string] PersonInfo
// 创建并初始化map的代码如下:
myMap = map[string] PersonInfo{
	"1234": PersonInfo{"1", "Jack", "Room 101,..."},
}

// 删除
delete(myMap, "1234")	// 如不存在1234的key,则什么也不发生,如果传入的map变量的值是nil,该调用将导致程序抛出异常(panic)。

二、常量、变量

1.变量

常量一般全部使用大写字母组成,并使用下划线分词

一个go语言包中的标识符首字母是大写的,表示对外可见的

下划线_表示匿名变量,匿名变量不占用命名空间,不会 分配内存,所以不存在重复声明

:=是声明语句

var v1 int
var v2 string
var v3 [10]int
var v4 []int	// 数组切片
var v5 struct {
    f int
}
var v6 *int
var v7 map[string]int
var v8 func(a int) int

intVal := 1等于
var intVal int
intVal = 1

批量命名

var (
	a string
    b int
    c float32
)

2.常量

常量定义可以限定常量类型,但不是必须的,如果定义时没有制定类型,那么和字面常量一样是无类型常量

const Pi float64 = 3.1415926
const zero = 0.0

常量赋值是一个编译器行为,所以右值不能出现任何运行期才能得出结果的表达式,但是可以是编译期运算的常量表达式

const mask = 1 << 3	// 正确
consr Home = os.GetEnv("HOME")  // 错误,运行期得出结果

3.预定义常量

true, false, iota都是预定义常量

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

iota 可以被用作枚举值:

const (
    a = iota
    b = iota
    c = iota
)

4.枚举

const后跟一对原括号的方式定义一组常量,常用于定义枚举值。

const (
	Sunday = iota
    numberOfDays // 这个常量没有导出
)

以大写字母开头的常量在包外可见。Sunday可被其他包访问,numberOfDays为包私有。

三、流程控制

1.条件语句

关键字if、 else和else if

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200
 
   /* 判断条件 */
   if a == 100 {
       /* if 条件语句为 true 执行 */
       if b == 200 {
          /* if 条件语句为 true 执行 */
          fmt.Printf("a 的值为 100 , b 的值为 200\n" );
       }
   }
   fmt.Printf("a 值为 : %d\n", a );
   fmt.Printf("b 值为 : %d\n", b );
   // 特殊写法
   if score := 65; score >= 90 {
   }
}

// if with a short statement”语法,if语句可以包含一个初始化语句,该语句在条件判断之前执行,并且该变量的作用域仅限于if语句块内部。如果初始化语句成功执行且条件表达式为true,则执行if语句块内的代码。
if configEnv := os.Getenv("env"); configEnv != "" {
      config = configEnv
   }

2.选择语句

switch i {
    case 0:
    	fmt.Printf("0")
    case 1:
    	fmt.Printf("1")
    case 2:
    	fallthrough
    case 3:
    	fmt.Printf("3")
    case 4, 5, 6:
    	fmt.Printf("4, 5, 6")
    default:
    	fmt.Printf("Default")
}
  • 左花括号{ 必须与switch处于同一行

  • 条件表达式不限制为常量或者整数

  • 单个case中,可以出现多个结果选项

  • Go语言不需要用break来明确退出一个case

  • 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个 case

  • 可 以 不 设 定 switch 之 后 的 条 件 表 达 式 , 在 此 种 情 况 下 , 整 个 switch 结 构 与 多 个
    if…else…的逻辑作用等同

3.循环语句

关于for range的坑
for _, v := range xx {}

range的流程是:1.开辟一块内存给v;2.range开始遍历,将遍历给v,3.此时v地址是固定的,变动的是地址指向的值,4.所以因为地址指向一样,所以会导致结果一样。

go的for循环有三种,可以执行指定次数的循环。

和c的for循环一样

for init; condition; post { }

和c的while一样

for condition { }

和c的for(;;)一样, 无限循环

for { }
  • init: 一般为赋值表达式,给控制变量赋初值;
  • condition: 关系表达式或逻辑表达式,循环控制条件;
  • post: 一般为赋值表达式,给控制变量增量或减量。

for语句执行过程如下:

  • 1、先对表达式 1 赋初值;
  • 2、判别赋值表达式 init 是否满足给定条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

for key, value := range oldMap {
    newMap[key] = value
}

以上代码中的 key 和 value 是可以省略。

如果只想读取 key,格式如下:

for key := range oldMap

或者这样:

for key, _ := range oldMap

如果只想读取 value,格式如下:

for _, value := range oldMap
for j := 0; j < 5; j++ {
    for i := 0; i < 10; i++ {
        if i > 5 {
        	break JLoop
        }
        fmt.Println(i)
    }
}
JLoop:
// 本例中,break语句终止的是JLoop标签处的外层循环

select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

select 语句的语法:

  • 每个 case 都必须是一个通信

  • 所有 channel 表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通信可以进行,它就执行,其他被忽略。

  • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
 package main

import (
   "fmt"   
   "time"
)

func Chann(ch chan int, stopCh chan bool) {
   for j := 0; j < 10; j++ {
      ch <- j
      time.Sleep(time.Second)
   }
   stopCh <- true}

func main() {

   ch := make(chan int)
   c := 0   
   stopCh := make(chan bool)

   go Chann(ch, stopCh)

   for {
      select {
      case c = <-ch:
         fmt.Println("Receive C", c)
      case s := <-ch:
         fmt.Println("Receive S", s)
      case _ = <-stopCh:
         goto end
      }
   }
end:
}

4.跳转语句

Go 语言的 goto 语句可以无条件地转移到过程中指定的行。

goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 10

   /* 循环 */
   LOOP: for a < 20 {
      if a == 15 {
         /* 跳过迭代 */
         a = a + 1
         goto LOOP
      }
      fmt.Printf("a的值为 : %d\n", a)
      a++    
   }  
}


func myfunc() {
    i := 0
    HERE:
    fmt.Println(i)
    i++
    if i < 10 {
    	goto HERE
    }
}

四、函数

小写字母开头的函数只在本包内可见,大写字母开头的函数才
能被其他包使用。
这个规则也适用于类型和变量的可见性。

Go 语言中变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数
func function_name( [parameter list] ) [return_types] {
   函数体
}
// 如果参数列表中若干个相邻的参数类型的相同,比如上面例子中的a和b,则可以在参数列表中省略前面变量的类型声明
func Add(a, b int)(ret int, err error) {
// ...
}
// 如果返回值列表中多个返回值的类型相同,也可以用同样的方式合并。如果函数只有一个返回值,也可以这么写:
func Add(a, b int) int {
// ...
}

package main
import "fmt"


func max(num1, num2 int) int {
	var result int
	if (num1 > num2) {
		result = num1
	} else {
		result = num2
	}
	return result
}

func swap(x, y string) (string, string) {
	return y, x
}

func swap2(x *int, y *int) {
	var temp int
	temp = *x
	*x = *y
	*y = temp
}

func Add(a int, b int) (ret int, err error) {
	if a < 0 || b < 0 { // 假设这个函数只支持两个非负数字的加法
		err = errors.New("Should be non-negative numbers!")
		return
	}
	return a + b, nil
	// 支持多重返回值
}

func main() {
	var a int = 100
	var b int = 200
	var e int = 300
	var f int = 400
	var ret int
	// 调用并返回最大值
	ret = max(a, b)
	fmt.Printf("最大值是: %d\n", ret)

	c, d := swap("goole", "baidu")
	fmt.Println(c, d)
	fmt.Println("交换前e,f的值: %d, %d", e, f)
	
	/* 调用 swap() 函数
     * &a 指向 a 指针,a 变量的地址
     * &b 指向 b 指针,b 变量的地址
    */
	swap2(&e, &f)
	fmt.Println("交换后e,f的值: %d, %d", e, f)
}

不定参数

// 不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受
// 不定参数类型: 接受不定量参数,类型全部是int
func myfunc(args ...int) {
    for _, arg := range args {
    	fmt.Println(arg)
    }
}

形如…type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一个语法糖(syntactic sugar),从内部实现机理上来说,类型…type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数args可以用for循环来获得每个传入的参数。

// 不定参数传递
// 假设有另一个变参函数叫做myfunc3(args ...int),下面的例子演示了如何向其传递变参:
func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)
// 传递片段,实际上任意的int slice都可以传进去
myfunc3(args[1:]...)
}
// 任意类型的不定参数,用interface{}传递任意类型数据是Go语言的惯例用法。使用interface{}仍然是类型安全的
func Printf(format string, args ...interface{}) {
// ...
}

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
        case int:
        	fmt.Println(arg, "is an int value.")
        case string:
        	fmt.Println(arg, "is a string value.")
        case int64:
        	fmt.Println(arg, "is an int64 value.")
        default:
        	fmt.Println(arg, "is an unknown type.")
        }
    }
}

匿名函数与闭包

1.匿名函数

在Go里面,函数可以像普通变量一样被传递或使用,这与C语言的回调函数比较类似。不同的是,Go语言支持随时在代码里定义匿名函数

// 匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
func(a, b int, z float64) bool {
	return a*b <int(z)
}

func(ch chan int) {
	ch <- ACK
} (reply_chan) // 花括号后直接跟参数列表表示函数调用

2.闭包

go的匿名函数是一个闭包

make()函数

make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

make(Type, len, cap)
// Type: 数据类型,必要参数,值只能是slice,map,channel
// len: 数据类型实际占用长度的内存空间长度,map,channel是可选参数,slice是必要参数
// cap: 为数据类型提前预留的内存空间长度,可选参数。所谓的天谴预留是当前为数据类型申请内存空间的时候,提前申请好额外的内存空间,这样可以避免二次分配内存带来的开销,大大提高程序的性能

// 三种用法
// 1.只传类型,不指定实际占用的内存空间和提前预留的内存空间,适用于map和channel
make(map[string]string)
// 2.指定实际占用的内存空间为2,不指定提前预留的内存空间
make([]int, 2)
// 3.实际占用为2,预留为4
make([]int, 2, 4)

new函数

p=new(int) 
//该方法的参数要求传入一个类型,而不是一个值,它会申请一个该类型大小的内存空间,并会初始化为对应的零值,返回指向该内存空间的一个指针。
// 可以不用创建额外的变量来创建一个指针
func new(Type) *Type

// 定义空struct时返回地址一样
type Point struct {}
p2 := new(Point)
q2 := new(Point)
fmt.Println(p2, q2, p2 == q2) // &{} &{} true

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

闭包

函数和引用环境的整体叫闭包

func AddUpper() func (int) int {
    var n int = 10
    return func(x int) int {
        n = n + x
        return n
    }
}

defer

标识一条语句在函数执行后再执行,defer语句会把语句和相应数值的拷贝进行压栈,先入后出。

// defer + 闭包
func makeSuffix(suffix string) func(string) string {
    var n = 1
    defer fmt.Println("suffix = ", suffix, ", n = ", n)
    defer fmt.Println("...")
    n = n + 1
 
    fmt.Println("makeSuffix..")
    return func(file_name string) string {
        if (! strings.HasSuffix(file_name, suffix)) {
            return file_name + suffix
        }
 
 
        return file_name
    }
}
 
func main() {
    f := makeSuffix(".txt")
    fmt.Println(f("szc.txt"))
    fmt.Println(f("szc"))
}

五、数组

1. 一维数组

// 定义格式
var variable_name [SIZE] variable_type
// 常见数组的声明方法
[32] byte	// 长度为32的数组,每个元素为一个字节
[2*N] struct {x, y int32}	// 复杂类型数组
[1000]*float64	// 指针数组
[3][5]int	// 二维数组
[2][2][2]float64	//等同于[2][2]([2]float64)

// 数组初始化,默认长度省略长度
var a3 = [...]int{1,2}

var variable_name [SIZE] variable_type

var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

//  将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
// 编译器推导数组长度
var boolArray = [...]bool{true, false, true}
// 使用索引值的方式初始化
var langArray = [...]string{1: "GOlang", 7: "Python"}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

balance[4] = 50.0
var salary float32 = balance[9]
func main() {
	var i,j,k int
	// 声明数组并快速初始化
	balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

	// 输出数组元素
	for i = 0; i < 5; i++ {
		fmt.Printf("balance[%d] = %f\n", i, balance[i])
	}
	// 编译器推导数组长度
	balance2 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
	for j = 0; j < 5; j++ {
		fmt.Printf("balance2[%d] = %f\n", j, balance2[j])
	}

	// 使用索引值的方式初始化,将索引1和3的元素初始化
	balance3 := [5]float32{1:2.0, 3:7.0}
	for k = 0; k < 5; k++ {
		fmt.Printf("balance3[%d]= %f\n", k, balance3[k])
	}
    
    // for range遍历
    for index, value := range cityAssay{
        fmt.Println(value)
    }
}

2.多维数组

var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
var threedim [5][10][4]int
a := [3][4]int{  
 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
 {8, 9, 10, 11},   /* 第三行索引为 2 */
}

3.向函数中传递数组

package main

import "fmt"


func getAverage(arr [5]int, size int) float32 {
   var i, sum int
   var avg float32

   for i = 0; i < size; i++ {
      sum += arr[i]
   }

   avg = float32(sum) / float32(size)

   return avg;
}

func main() {
	var balance = [5]int {1000, 2, 3, 17, 50}
	var avg float32

	avg = getAverage(balance, 5)
	fmt.Printf("平均值为: %f\n", avg)

   // 设置精度
   a := 1690           // 表示1.69
   b := 1700           // 表示1.70
   c := a * b          // 结果应该是2873000表示 2.873
   fmt.Println(c)      // 内部编码
   fmt.Println(float64(c) / 1000000) // 显示
}

4.数组切片

数组切片(slice)就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是
个指针。数组切片的数据结构可以抽象为以下3个变量:
一个指向原生数组的指针;
数组切片中的元素个数;
数组切片已分配的存储空间。

在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。

// 创建
// 两种方式:基于数组和直接创建

// 基于数组
// 数组切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片
var mySlice []int = myArray[:5]

// 直接创建
// Go语言提供的内置函数make()可以用于灵活地创建数组切片
// 创建初始元素个数为5的数组切片,元素初始值为0
mySlice1 := make([]int, 5)
// 创建初始元素个数为5的数组切片,初始值为0,并预留10个元素的存储空间
mySlice := make([]int, 5, 10)
// 创建并初始化包含五个元素的数组切片
mySlice3 := []int{1,2,3,4,5}

数组切片支持Go语言内置的cap()函数和 len()函数,,

cap()函数 返回的是数组切片分配的空间大小

len()函数 返回的是数组切片中当前所存储的元素个数

append()	从尾端给mySlice加上3个元素,从而生成一个新的数组切片,mySlice = append(mySlice, 1, 2, 3)

mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片,省略号表示打散传入
mySlice = append(mySlice, mySlice2...)

基于数组切片创建数组切片

oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片

比如newSlice可以基于oldSlice的前6个元素创建,虽然oldSlice只包含5个元素。只要这个选择的范围不超过oldSlice存储能力(即cap()返回的值),那么这个创建程序就是合法的。newSlice 中超出oldSlice元素的部分都会填上0。

内容复制

数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

5.删除元素

var a1 = []int{1, 2, 3}
// 删除第i个元素
i := 2
a1 = append(a1[:i], a[a+1])

六、指针

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针

// 声明格式
var var_name *var-type


var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

1.空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

func main() {
	var a int = 10
	var ip *int
	var ptr *int
	
	ip = &a
	fmt.Printf("变量a的地址:%x\n", &a)
	fmt.Printf("ip变量存储的指针地址: %x\n", ip)
	fmt.Printf("*ip 变量的值: %d\n", *ip)
	fmt.Printf("ptr的值为: %x\n", ptr)

	/* ptr 不是空指针 */
	if(ptr != nil) {
		fmt.Println("ptr不是空指针")
	}
	/* ptr 是空指针 */
	if(ptr == nil) {
	fmt.Println("ptr是空指针")
	}
}

2.指针数组

有一种情况,我们可能需要保存数组,这样我们就需要使用到指针。

const MAX int = 3

func main() {
	a := []int{10, 100, 200}
	var i int
	var ptr [MAX]*int

	
	for i = 0; i < MAX; i++ {
		fmt.Printf("a[%d] = %d\n", i, a[i])
	}
	
	fmt.Println("指针数组的使用")
	for i = 0; i < MAX; i++ {
		// 整数地址赋值给指针数组
		ptr[i] = &a[i]
	}

	for i = 0; i < MAX; i++ {
		fmt.Printf("a[%d] = %d\n", i, *ptr[i])
	}
}

3.指针的指针

func main() {
	var a int
	var ptr *int
	var pptr **int
	var ptr1 *int = &a
	var ptr2 **int = &ptr1
	var ptr3 **(*int) = &ptr2 // 也可以写作:var ptr3 ***int = &ptr2
	a = 3000

	ptr = &a 
	pptr = &ptr

	fmt.Printf("变量 a = %d\n", a)
	fmt.Printf("指针变量 *ptr = %d\n", *ptr)
	fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
	
}

4.向函数传递指针参数

func swap(x *int, y *int) {
	var temp int
	temp = *x
	*x = *y
	*y = temp
}

七、结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

访问结构体成员:结构体.成员名

结构体指针

var struct_pointer *Books
// 查看结构体变量地址,可以将 & 符号放置于结构体变量前
struct_pointer = &Book1
// 使用结构体指针访问结构体成员,使用 "." 操作符
struct_pointer.title
package main

import "fmt"

type Books struct {
	title string
	author string
	subject string
	book_id int
}

// 结构体做函数参数
func printBook(book Books) {
	fmt.Printf("book title: %s\n", book.title)
	fmt.Printf("book.author: %s\n", book.author)
}

// 结构体指针
func printBook2(book *Books) {
	fmt.Printf("book title: %s\n", book.title)
}

func main() {
	var book1 Books
	var book2 Books

	book1.title = "java"
	book1.author = "作者2"

	book2.title = "c++"
	book2.author = "c++作者"
	book2.subject = "c+++教程"

	fmt.Println(Books{"go语言", "作者1", "教程", 8541265})

	// 也可以使用key => value 格式
	fmt.Println(Books{title: "c语言", author: "作者2", subject: "c语言教程", book_id: 5452456})

	// 忽略的字段为0或空
	fmt.Println(Books{title: "python"})

	fmt.Printf("book1.title= %s\n", book1.title)
	fmt.Printf("book1.author= %s\n", book1.author)

	fmt.Printf("结构体做函数参数\n")
	printBook(book1)

	fmt.Println("结构体指针")
	printBook2(&book2)

}

八、切片(Slice)

Go 语言切片是对数组的抽象。

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

// 切片定义格式
var identifier []type

1.定义切片

var identifier []type

// 切片不需要说明长度。或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len)
也可以简写为
slice1 := make([]type, len)

// 也可以指定容量,其中 capacity 为可选参数。这里 len 是数组的长度并且也是切片的初始长度。
make([]T, length, capacity)

2.切片初始化

s :=[] int {1,2,3 } 

直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

s := arr[:] 

初始化切片 s,是数组 arr 的引用。

s := arr[startIndex:endIndex] 

将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。

s := arr[startIndex:] 

默认 endIndex 时将表示一直到arr的最后一个元素。

s := arr[:endIndex] 

默认 startIndex 时将表示从 arr 的第一个元素开始。

s1 := s[startIndex:endIndex] 

通过切片 s 初始化切片 s1。

s :=make([]int,len,cap) 

通过内置函数 make() 初始化切片s[]int 标识为其元素类型为 int 的切片。

3.len()函数和cap()函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

4.空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0

5.切片截取

切片截取的cap会减掉前面截取的数。

func slice2() {
	numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	
	// 打印子切片从索引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)

	 printSlice(numbers[3:6])

}

6.append()和copy()函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

package main

import "fmt"

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

func slice2() {
	numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	
	// 打印子切片从索引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)

	 printSlice(numbers[3:6])

}

// append()和copy函数
func slice3() {
	var numbers []int
	printSlice(numbers)

	// 允许追加空切片
	numbers = append(numbers, 0)
	printSlice(numbers)

	// 向切片添加一个元素
	numbers = append(numbers, 1)
	printSlice(numbers)

	// 同时添加多个元素
	numbers = append(numbers, 2, 3, 4)
	printSlice(numbers)

	// 创建切片numbers1是之前切片的两倍容量
	numbers1 := make([]int, len(numbers), cap(numbers)*2)

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

}

func main() {
	var numbers = make([]int, 3, 5)
	var numbers2 []int
	printSlice(numbers)

	if(numbers2 == nil) {
		fmt.Println("切片是空的")
	}

	fmt.Println("切片截取")
	slice2()

	fmt.Println("append() 和 copy() 函数")
	slice3()

}

7.切片动态扩容

data := make([]int, 0)

for i, n := 0, 10000; i < n; i++ {
		data = append(data, 1)
		if len(data) == cap(data) {
				fmt.Printf("len=%d cap=%d\n", len(data), cap(data))
		}
}
// 动态扩容
// 这个例子中前11次是每次扩展一倍的长度,第十二次扩容并没有扩展一倍,这是动态数组算法,避免的内存浪费

九、Range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。

for key, value := range oldMap {
    newMap[key] = value
}

如果只想读取 key,格式如下:

for key := range oldMap

for key, _ := range oldMap

如果只想读取 value,格式如下:

for _, value := range oldMap
package main

import "fmt"

var pow = []int{1, 2, 4 ,8, 16, 32, 64, 128}


func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}


// 读取 key 和 value
for key, value := range map1 {
    fmt.Printf("key is: %d - value is: %f\n", key, value)
}

// 读取 key
for key := range map1 {
    fmt.Printf("key is: %d\n", key)
}

// 读取 value
for _, value := range map1 {
    fmt.Printf("value is: %f\n", value)
}

十、Map

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

按键取值: map1["name"]

删除某值: delete(map1, "age")

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

/* 查看key是否存在 */
// 查看元素再元素中是否存在
capital, ok := map2["kk"]	// capital是值, ok是布尔类型,存在时true,不存在时false
// 仅获取值,存在返回正确值,不存在返回,value类型的默认值, 值是int类型就返回0
val :=  map2["kk1"]

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对

package main

import (
	"fmt"
	"math/rand"
	"sort"
	"strings"
)

// map
func mapDemo() {
	var a map[string]int
	fmt.Println(a == nil)
	// map初始化
	a = make(map[string]int, 8)
	fmt.Println(a == nil)
	// 添加键值对
	a["a"] = 1
	a["b"] = 2
	fmt.Println(a)
	fmt.Printf("type:%T\n", a)
	// 声明的同时完成初始化
	b := map[int]bool{
		1: true,
		2: false,
		3: true,
	}
	fmt.Printf("b:%#v\n", b)
	fmt.Printf("type:%T\n", b)

	//
	fmt.Println("按照某个固定的顺序遍历map")
	var scoreMap = make(map[string]int, 100)
	// 添加50键值对
	for i := 0; i < 50; i++ {
		key := fmt.Sprintf("stu%02d", i)
		value := rand.Intn(100)
		scoreMap[key] = value
	}

	// for k, v := range scoreMap {
	// 	fmt.Println(k, v)
	// }
	// 按照key从小到大顺序遍历map
	// 1.先取出key存放到切片中
	keys := make([]string, 0, 100)
	for k := range scoreMap {
		keys = append(keys, k)
	}
	// 2.对key排序
	sort.Strings(keys)
	// 3.按照排序后的key对score排序
	for _, key := range keys {
		fmt.Println(key, scoreMap[key])
	}

}

// 值为map的切片
func mapSlice() {
	// 元素为map的切片
	var mapSlice = make([]map[string]int, 8, 8)
	fmt.Println(mapSlice)
	// 给map初始化
	mapSlice[0] = make(map[string]int, 8)

}

// 值为切片的map
func sliceMap() {
	var sliceMap = make(map[string][]int, 8) // []int是切片的定义
	v, ok := sliceMap["中国"]
	if ok {
		fmt.Println(v)
	} else {
		//
		sliceMap["中国"] = make([]int, 8) // 对切片的初始化
		sliceMap["中国"][0] = 100
		sliceMap["中国"][2] = 200
		sliceMap["中国"][3] = 300
		sliceMap["中国"][4] = 400

	}
	// 遍历sliceMap
	for k, v := range sliceMap {
		fmt.Println(k, v)
	}
}

// 统计字符串中出现的单词次数
func wordCount() {
	// "how do you do"
	// 0.定义一个map[string]int
	var s = "how do you do"
	var wordCount = make(map[string]int, 10)
	// 1. 字符串中都有哪些单词
	words := strings.Split(s, " ")
	// 2.遍历单词做统计
	for _, word := range words {
		v, ok := wordCount[word]
		if ok {
			wordCount[word] = v + 1
		} else {
			wordCount[word] = 1
		}
	}
	fmt.Println(wordCount)

}

func main() {
	// mapDemo()
	// mapSlice()
	// sliceMap()
	wordCount()
}

十一、结构体补充

1.自定义类型

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

2.类型别名

type byte = uint8

3.结构体

结构体占用一块连续的内存

go结构体没有构造函数,

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

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

// 函数内的叫匿名结构体
// 自定义类型
type MyInt int

// 定义结构体
type person struct {
	name, city string
	age        int8
}

// 自定义的结构体构造函数
func newPerson(name, city string, age int8) *person {
	return &person{
		name: name,
		city: city,
		age:  age,
	}
}

func main() {
	var p person
	p.name = "an"
	p.city = "bn"
	p.age = 18
	fmt.Printf("p=%#v\n", p)

	var p1 = new(person)
	fmt.Printf("%T\n", p1) // *main.person   main包下的person结构体
	(*p1).name = "cn"
	(*p1).age = 11
	p1.city = "上海" // go语法糖,可以直接指结构体对象名.属性
	fmt.Printf("p1=%#v\n", p1)

	// 取结构体指针的地址进行实例化
	fmt.Println("取结构体指针的地址进行实例化")
	p3 := &person{}
	fmt.Printf("%T\n", p3)
	fmt.Printf("%#v\n", p3)

	// 通过键值对初始化
	fmt.Println("通过键值对初始化")
	p4 := person{
		name: "dn",
		age:  11,
		city: "不知道",
	}
	fmt.Printf("%#v\n", p4)
	// p5 := &person{

	// }
	fmt.Println("通过值的列表对初始化")
	p6 := person{
		"tn",
		"纽约",
		23,
	}
	fmt.Printf("%#v\n", p6)
	fmt.Println("自定义结构体构造函数")

}

4.结构体嵌套

结构体的匿名字段:只有字段类型,没有字段名称,相同类型的匿名字段只能有一个

// 定义结构体
type Person struct {
	string
	int
}

func main() {
	p1 := Person{
		string: "an",
		int:    18,
	}
	fmt.Println(p1)
	fmt.Println(p1.string, p1.int)
}

结构体嵌套:

type Address struct {
	province string
	City     string
}

type Person struct {
	Name    string
	Gender  string
	Age     int
	Address Address
}

func main() {
	p1 := Person{
		Name:    "an",
		Gender:  "cccc",
		Age:     18,
		Address: Address{province: "山东", City: "威海"},
	}
	fmt.Println(p1)
	fmt.Printf("%#v\n", p1)
}

嵌套结构体的字段冲突:

type Address struct {
	province   string
	City       string
	UpdateTime string
}

type Email struct {
	Addr       string
	UpdateTime string
}

type Person struct {
	Name   string
	Gender string
	Age    int
	Address
	Email
}

func main() {
	p1 := Person{
		Name:   "an",
		Gender: "cccc",
		Age:    18,
		Address: Address{
			province:   "山东",
			City:       "威海",
			UpdateTime: "2022-01-01",
		},
		Email: Email{
			Addr:       "xxx@qq.com",
			UpdateTime: "2022-01-01",
		},
	}
	fmt.Println(p1)
	fmt.Printf("%#v\n", p1)
	fmt.Println(p1.province)	// 没冲突的可以直接访问
	fmt.Println(p1.Address.UpdateTime) // 冲突的需要加上具体的结构体名称
}

5.结构体函数

// 结构体
// 对象
type rect struct {
	width, height int
}

// 作用于特定类型的函数,特定类型变量称为接收者(receiver).类比js中的this
/*
	func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {}
	建议:
	1. 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母
	2. 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
	3. 方法名、参数列表、返回参数:具体格式与函数定义相同
	区别函数:
	函数不属于任何类型,方法属于特定类型
*/
// 结构体函数
// 对象方法
// 1. 指针类型的接收者: 由一个结构体的指针组成
// 由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的
// 2. 值类型的接收者: 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
// 在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身
/*
	什么时候使用指针接受者
	1.需要修改接收者中的值
  2.接收者是拷贝代价比较大的大对象
  3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
*/
func (r *rect) area() int {
	return r.width * r.height
}
func (r rect) perim() int {
	return 2*r.width + 2*r.height
}

// 可以给任何类型定义方法,扩展方法。
// 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法
func main() {
	r := rect{width: 10, height: 5}
	fmt.Println("area:", r.area())
	fmt.Println("area:", r.perim())
	rs := &r
	fmt.Println("area:", rs.area())
	fmt.Println("area:", rs.perim())

}

6.结构体的继承

结构体的匿名字段用来实现继承效果,

type Animal struct {
	name string
}

// 定义结构体函数
func (a *Animal) move() {
	fmt.Printf("%s会动~\n", a.name)
}

type Dog struct {
	Feet    int8
	*Animal // 匿名嵌套。嵌套的是结构体指针
}

// 定义结构体函数
func (d *Dog) wang() {
	fmt.Printf("%s会叫\n", d.name)
}

func main() {
	d1 := &Dog{
		Feet: 4,
		Animal: &Animal{
			name: "乐乐",
		},
	}
	d1.wang()
	d1.move()
}

7.结构字段的可见性和json

结构字段小写可能会导致序列化失败,

结构体标签可以实现json字段的自定义

  • 可导出的成员属性的首字母是大写的,才可以转成 json 。
  • 如果属性有 json 标签,那么转化成的 json key 就用该*标签里的值*,否则取*属性名*来作为 key
  • bool类型也是可以直接转换为json的value值。循环的数据结构(导致marshal陷入死循环)、Channel, complex 以及函数不能被编码json字符串。
  • 切片在编码时自动转换为它所指向的值
import (
	"encoding/json"
	"fmt"
)

type student struct {
	ID   int
	Name string
}

//  `json:"title"`  是结构体标签
type class struct {
	Title    string    `json:"title"`
	Students []student `json:"student_list" db:"student" xml:"ss"`
}

// student 构造函数
func newStudent(id int, name string) student {
	return student{
		ID:   id,
		Name: name,
	}
}

func main() {
	// 创建班级c1
	c1 := class{
		Title:    "火箭101",
		Students: make([]student, 0, 20),
	}
	// 添加学生
	for i := 0; i < 10; i++ {
		tempStu := newStudent(i, fmt.Sprintf("stu%02d", i))
		c1.Students = append(c1.Students, tempStu)
	}
	fmt.Printf("%#v\n", c1)

	// JSOn序列化
	data, err := json.Marshal(c1)
	if err != nil {
		fmt.Println("json marshal failed, err:", err)
	}
	fmt.Printf("%T\n", data)
	fmt.Printf("%s\n", data)
	// json反序列化
	jsonStr := `{"Title":"火箭101","Students":[{"ID":0,"Name":"stu00"},{"ID":1,"Name":"stu01"},{"ID":2,"Name":"stu02"},{"ID":3,"Name":"stu03"},{"ID":4,"Name":"stu04"},{"ID":5,"Name":"stu05"},{"ID":6,"Name":"stu06"},{"ID":7,"Name":"stu07"},{"ID":8,"Name":"stu08"},{"ID":9,"Name":"stu09"}]}`
	var c2 class
	err = json.Unmarshal([]byte(jsonStr), &c2)
	if err != nil {
		fmt.Println("反序列化失败, err:", err)
	}
	fmt.Printf("c2=%#v", c2)
}

7.类型转换

go不支持隐式转换类型

var a int64 = 3
var b int32
b = a
fmt.Printf("b 为 : %d", b)
// 此时会报错,改成b = int32(a)就不会报错了
package main

import "fmt"

func main() {
   var sum int = 17
   var count int = 5
   var mean float32
   
   mean = float32(sum)/float32(count)
   fmt.Printf("mean 的值为: %f\n",mean)
}

十二、接口(interface)

go中是一种类型,是一种抽象的类型

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

接口可以嵌套。

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}


import (
	"fmt"
)

type dog struct {
}

func (d dog) say() {
	fmt.Printf("汪汪\n")
}

type cat struct {
}

func (c cat) say() {
	fmt.Printf("喵喵\n")

}

// 接口不管是什么类型,只管你要实现什么方法
type sayer interface {
	say()
}

func da(arg sayer) {
	arg.say()
}

func main() {
	c1 := cat{}
	da(c1)
	d1 := dog{}
	da(d1)

}
package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

1.接口接收

// 使用值接收和使用指针接收接口的区别
type mover interface {
	move()
}

type person struct {
	name string
	age  int8
}

// 使用值接收
// func (p person) move() {
// 	fmt.Printf("%s在跑\n", p.name)
// }

//
func (p *person) move() {
	fmt.Printf("%s在跑\n", p.name)
}

func main() {
	var m mover
	p1 := &person{ //person类型的值
		name: "an",
		age:  19,
	}
	// p2 := &person{ // person类型的指针
	// 	name: "bn",
	// 	age:  33,
	// }
	m = p1
	// m = p2
	m.move()
	fmt.Println(m)
}

2.空接口

func main() {
    // 定义一个空接口
    var x interface{}
    s := ""
    x = s
    i := 100
    x = i
}

空接口的应用:

  1. 作为函数的参数,可以实现接口任意类型的函数参数。
func show(a inertface{}){
    
}
  1. 空接口作为map的值
var m = make(map([string]interface{}, 16))
m["name"] = "an"
m["age"] = 18

类型断言

x := false
ret, ok := x.(string)	// 类型断言
fmt.Println(ret)

// 使用switch进行类型断言
switch v := x.(type) {
case string:
    fmt.Println("")
case bool:
    fmt.Println("")
default:
    fmt.Println()
}

十三、面向对象

1. 类型系统

可以给任意类型(包括内置类型,不包括指针类型)添加相应的方法

type Integer int
func (a Integer) Less(b Integer) bool {
	return a < b
}
// 定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法Less()

2.初始化

rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
// 未进行显式初始化的变量都会被初始化为该类型的零值

在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以
NewXXX来命名,表示“构造函数”

func NewRect(x, y, width, height float64) *Rect {
   return &Rect{x, y, width, height}
}

3.匿名组合

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


4. 接口

侵入式接口:

非侵入式接口

一个类实现了接口要求的所有函数,就说这个类实现了该接口。

接口赋值

两种情况:

  • 将对象实例赋值给另一个接口
  • 将一个接口赋值给另一个接口

在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。

接口查询

类型查询

接口组合

十四、错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}
// 如果觉得一句话干不完清理的工作,也可以使用在defer后加一个匿名函数的做法:
defer func() {
// 做你复杂的清理工作
} ()

一个函数中可以存在多个defer语句,因此需要注意的是,defer语句的调用是遵照
先进后出的原则,即最后一个defer语句将最先被执行。

panic()和recover()

了两个内置函数panic()和recover() 以报告和处理运行时错误和程序中的错误场景:

func panic(interface{})
func recover() interface{}
当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立即终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用panic()函数时传入的参数,这个过程称为错误处理流程。

panic(404)
panic("network broken")
panic(Error("file not exists"))

recover() 函数用于终止错误处理流程。一般情况下,recover()应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(使用recover关键字),会导致该goroutine所属的进程打印异常信息后直接退出.

我们对于foo()函数的执行要么心里没底感觉可能会触发错误处理,或者自己在其中明确加
入了按特定条件触发错误处理的语句,那么可以用如下方式在调用代码中截取recover():
defer func() {
if r := recover(); r != nil {
log.Printf(“Runtime error caught: %v”, r)
}
}()
foo()
无论foo()中是否触发了错误处理流程,该匿名defer函数都将在函数退出时得到执行。假
如foo()中触发了错误处理流程, recover()函数执行将使得该错误处理过程终止。如果错误处
理流程被触发时,程序传给 panic函数的参数不为nil,则该函数还会打印详细的错误信息

十五、并发(goroutine)

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

go 函数名( 参数列表 )

Go 允许使用 go 语句开启一个新的运行线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

package main

import (
	"fmt"
	"time"
)


func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}

// 获取并设置使用cpu数量
num := runtime.NumCPU()	// 获取cpu核数
fmt.Println("CPU count:", num)
runtime.GOMAXPROCS(num - 1)	// 设置最大并发数

MPG模式

M: 操作系统主线程

P:协程执行所需的上下文

G:协程

如果主线程M0上的G0协程阻塞,协程队列中还有别的协程,那么就会开启一个M1主线程,把协程队列上里的其他协程挂在M1上执行

全局互斥锁

当设计多个协程对同一个引用类型的对象进行读写操作时,使用全局锁。

// 声明锁变量
var (
	result []int      = make([]int, 0) // 素数切片
	lock   sync.Mutex                  // 全局锁
)

// 素数切片时进行锁的请求和释放
func is_prime(n int) {
	is_prime := true
	for i := 2; i < int(math.Sqrt(float64(n)))+1; i++ {
		if n%i == 0 {
			is_prime = false
			break
		}
	}
	if is_prime && !exits(result, n) {
		lock.Lock() // 请求锁
		result = append(result, n)
		lock.Unlock() // 释放锁
	}
}

func exits(slice []int, n int) bool {
	for _, value := range slice {
		if value == n {
			return true
		}
	}
	return false
}

func main() {
    /* 全局锁 */
	num := runtime.NumCPU()
	runtime.GOMAXPROCS(num - 1)
	for i := 2; i < 2000; i++ {
		go is_prime(i)
	}
	time.Sleep(10 * time.Second)

	lock.Lock() //遍历的时候依旧利用锁进行同步控制
	for _, value := range result {
		fmt.Println(value)
	}
	lock.Unlock()
}

通道(channel)

通道(channel)是用来传递数据的一个数据结构,本质是一个队列,线程安全,有类型

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

int_chan1 chan<- int = make(chan int, 8) // 只写
int_chan2 <- chan int  = make(chan int, 8) // 只读

package main

import "fmt"

var (
	write_chan chan int  = make(chan int, 50) // 数据管道,整型管道,容量50(不可扩容)
	exit_chan  chan bool = make(chan bool, 1) // 状态管道,布尔型管道,容量1(不可扩容)
)

func write_data() {
	for i := 0; i < 50; i++ {
		write_chan <- i // 写
		fmt.Println("write data: ", i)
	}
	close(write_chan) // 关闭管道,只影响写,不影响读
}

func read_data() {
	for {
		v, ok := <-write_chan // 读数据,管道为空则阻塞
		if !ok {
			break
		}
		fmt.Println("read_data: ", v)

	}
	exit_chan <- true
	close(exit_chan)
}

func main() {
	go write_data()
	go read_data()
	for {
		_, ok := <-exit_chan
		if !ok {
			break
		}
	}
}

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

通道缓冲区

通过make的第二个参数指定缓冲区大小

ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
        // 缓冲区大小为2
        ch := make(chan int, 2)

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
        // 而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        // 获取这两个数据
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

遍历通道和关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

package main

import (
        "fmt"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

协程调度模式(MPG模式)

Go goroutine泄露得场景

泄露原因

  1. Goroutine内进行channe/mutex等读写操作被阻塞
  2. Goroutine内得业务逻辑进入死循环,资源一直无法释放
  3. Goroutine内的业务逻辑进入长时间等待,有不断新增的Goroutine进入等待

如何排查

单个函数:调用runtime.NumGoroutine方法来打印执行代码前后Goroutine的运行数量,进行前后比较就知道有没有泄露了

生产/测试环境: 使用PProf实时监测Goroutine的数量

控制goroutine并发的数量

  1. 有缓冲的channel,利用缓冲满时发送阻塞的特性
  2. 无缓冲的channel,任务发送和执行分离,指定消费者并发协程数

十六、包

包就是存放go文件的文件夹,该文件夹下所有.go文件都要在非注释的第一行添加包声明

import (
	asName "code/"
)

1.自定义包

package packagename

packagename: 包名,可以不与文件夹的名称一致,不能包含-符号

一个文件夹下面直接包含的文件只能属于一个包,同一个包的文件不能在多个文件夹下。

2.标识符可见性

同一个包内部声明的标识符都位于同一个命名空间下,不同的包内部声明的标识符就属于不同的命名空间。在包外部使用内部的标识符需要添加包名前缀,如: fmt.Println("Hello")

属于某个包的源文件都应该放置于单独命名的文件夹里,用包名命名该文件夹

包中所有可被导出的变量函数等首字母需要大写。

// 包被导入时自动触发,没有参数,也没有返回值
func init() {
    
}
// go会从main包开始检查其所有导入的包,每个包又可能导入了其他的包,go编译器由此构建出可一个树状的包引用关系,再更根据引用顺序决定编译顺序,依次编译这些包的代码
// 最后导入的包会最先初始化并调用init函数

3.包导入

包引入

import importname "path/to/package"
  • importname: 引入包名,通常省略,默认为引入包的包名
  • go中禁止循环导入包

当引入多个包中存在相同的包名或者为某个引入的包设置一个新包名时,都需要通过importname指定一个在当前文件中使用的新包名

import f "fmt"

如果引入包时设置_为包名,则这个包的引入方式就称为匿名引入,目的是为了加载这个包,从而使这个包中的资源得以初始化。被匿名引入的包中的init函数将被执行并且仅执行一遍。

import _ "github.com/go-sql-driver/mysql"

匿名引入的包与其他包一样会被编译到可执行文件中。

一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的init函数都将被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的init函数再执行当前包中声明的init函数。确保在程序的main函数开始执行时所有的依赖包都已初始化完成。

项目目录结构

https://blog.csdn.net/jiey0407/article/details/126056802

十七、异常捕获

func test_r_0() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("test_r_0 发生错误:", err)
        }
    }()
    var map0 map[int]string
    map0[0] = "szc"
}

https://blog.csdn.net/qq_37475168/article/details/105264300

十八、单元测试

1.单元测试命令

// 1.单个文件进入到测试文件所在目录
go test hello_test.go hello.go
ok      command-line-arguments  0.002s

// 2.如果该目录有go.mod存在,直接go test
go test
PASS
ok      dev     0.002s

PASS表示执行成功。

2.运行指定单元测试用例

// 包含多个测试用例

package code3
import "testing"
 
func TestA(t *testing.T) {
    t.Log("A")
}
 
func TestAB(t *testing.T) {
    t.Log("AB")
}
// 指定测试用例
go test -v -run TestA select_test.go

TestA个TestAB的测试用例都被执行,原因是-run跟随的测试用例的名称支持正则表达式。使用-run TestA$即可只执行TestA用例。

3.标记单元测试结果

// 1.终止当前测试用例
func TestFailNow(t *testing.T) {
    t.FailNow()
}
// 2.只标记错误不终止测试的方法
func TestFail(t *testing.T) {
    fmt.Println("before fail")
    t.Fail()
    fmt.Println("after fail")
}

4.单元测试日志

每个测试用例可能并发执行,使用 testing.T 提供的日志输出可以保证日志跟随这个测试上下文一起打印输出。testing.T 提供了几种日志输出方法

方法 备注
Log 打印日志,同时结束测试
Logf 格式化打印日志,同时结束测试
Error 打印错误日志,同时结束测试
Fatal 打印致命日志,同时结束测试
Fatalf 格式化打印致命日志,同时结束测试

十九、go.mod

Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。Go module 提供了以下命令供我们使用:

命名 介绍
go mod init 初始化项目依赖,生成go.mod文件
go mod download 根据go.mod文件下载依赖
go mod tidy 对比项目文件中引入的依赖与go.mod进行对比
go mod graph 输出依赖关系图
go mod edit 编辑go.mod文件
go mod vendor 将项目的所有依赖导出至vendor目录
go mod verify 检验一个依赖包是否被篡改过
go mod why 解释为什么需要某个依赖

Go语言在 go module 的过渡阶段提供了 GO111MODULE 这个环境变量来作为是否启用 go module 功能的开关,考虑到 Go1.16 之后 go module 已经默认开启,

GOPROXY

这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式,直接通过镜像站点来快速拉取。

GOPROXY 的默认值是:https://proxy.golang.org,direct,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cn和https://goproxy.io

# 设置GOPROXY
go env -w GOPROXY=http://goproxy.cn,direct

GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。

GOPRIVATE

设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。

GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:

go env -w GOPRIVATE="git.xxxx.com"

这样在拉取以git.mycompany.com为路径前缀的依赖包时就能正常拉取了。

此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none,允许通内部代理拉取私有仓库的包。

使用go module引入包

创建项目xx并切换到该目录下。

// 初始化
go mod init xx
// 手动下载依赖包
go get -u github.com/gin-gonic/gin
// 指定版本号
go get -u github.com/gin-gonic/gin@V0.1.0

如果依赖包没有发布任何版本则会拉取最新的提交,最终go.mod中的依赖信息会变成类似下面这种由默认v0.0.0的版本号和最新一次commit的时间和hash组成的版本格式:

require github.com/gin/hello v0.0.0-20210218074646-139b0bcd549d

行尾的indirect表示该依赖包为间接依赖,说明在当前程序中的所有 import 语句中没有发现引入这个包。

使用latest表示使用该库的最新版本,然后再项目目录下执行go mod download下载依赖包。

require github.com/gin/hello latest

go导包写总的相对路径即可,即从根目录到该包xx/yy,xx是根目录,yy是自定义的包

如果想导入一个本地包,但是这个包没有被发布到任务代码仓库,可以在go.mod文件中使用replace语句将依赖临时替换为本地的代码包。

replace liwenzhou.com/overtime  => ../overtime

go.mod文件

require module/path v1.2.3
  • require: 声明依赖的关键字
  • module/path: 依赖包的引入路径
  • v1.2.3: 依赖包的版本号

引入某些没有发布过tag版本标识的依赖包时,go.mod中记录的依赖版本信息就会出现类似v0.0.0-20210218074646-139b0bcd549d的格式,由版本号、commit时间和commit的hash值组成。

go.sum文件

使用go module下载了依赖后,项目目录下还会生成一个go.sum文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum格式:

<module> <version>/go.mod <hash>
// 或者
<module> <version> <hash>
<module> <version>/go.mod <hash>

不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了go.sum机制来对依赖包进行校验。

依赖保存位置

Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。

如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令。

使用go module发布包

将本地的包代码推送到远程仓库即可被其他人使用

git tag -a v0.1.0 -m "release version v0.1.0"
git push origin v0.1.0

go modules 建议使用语义化版本控制,

  • 主版本号: 发布了不兼容的版本迭代时递增
  • 次版本号: 发布功能性更新时递增
  • 修顶号: 发布了bug修复类更新时递增

发布新的主版本
在go.mod引入路径后面添加版本号v2,
打好tag推送到远程仓库

git tag -a v2.0.0 -m "release version v2.0.0"
git push origin v2.0.0

新版本下载

go get github.com/xx/yy/v2@v2.0.0

废弃已发布版本

go.mod 中添加retract声明废弃版本

retract v0.1.0

二十、反射

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

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

Go程序在运行期间使用reflect包访问程序的反射信息。
反射在运行时动态获取一个变量得类型信息和值信息

reflect包

任何接口值都是由一个具体类型具体类型得值两部分,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。

TypeOf

使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type), 程序通过类型对象可以访问任意值的类型信息。

import (
	"fmt"
	"reflect"
)


func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("v: %v\n", v)
}

func main() {
	var a float32 = 3.14
	reflectType(a)
	var b int64 = 100
	reflectType(b)
	
}

type name 和type kind

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

import (
	"fmt"
	"reflect"
)

type myInt int64

func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	fmt.Printf("type: %v kind:%v\n", t.Name(), t.Kind())
}

func main() {
	var a *float32
	var b myInt
	var c rune
	reflectType(a) // type:  kind:ptr
	reflectType(b) //type: myInt kind:int64
	reflectType(c) //type: int32 kind:int32

	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "小王子",
		age:  18,
	}
	var e = book{title: "xxxxx"}
	reflectType(d) //type: person kind:struct
	reflectType(e) // type: book kind:struct
}

反射中像数组、切片、Map、指针等类型得变量,它们得.Name()返回都是空。

ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值信息。reflect.Value与原始值之间可以相互转换。

reflect.Value()类型提供获取原始值的方法如下:

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

通过反射获取值

package main

import (
	"fmt"
	"reflect"
)

func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Float32:
		// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
		fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
	case reflect.Float64:
		// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
		fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
	}

}

func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 将int类型的原始值转换为reflect.Value类型
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。