【后端开发】Golang语法基础

Last updated on February 22, 2024 am


Golang基础

参考学习资料:

Golang简介

go语言的优势

  1. 部署简单
    • 可以直接编译成机器码可执行
    • 不依赖其他库
    • 直接运行即可部署
  2. 静态类型语言
    • 编译的时候就能查出来大多数的问题
  3. 语言层面的并发
    • 能够成分利用多核
  4. 强大的标准库
    • runtime系统调度机制
    • 丰富的标准库
  5. 简单易学
    • 25个关键字,内嵌C语法支持
    • 面向对象的特征,能够跨平台
    • go语言没有异常,全部都用ERROR来表示

go应用方向

  1. 云计算基础设施建设
    • Docker, kubernetes
    • Consul, cloudflare CDN
  2. 基础后端软件:tide, influxdb, cockroachdb
  3. 微服务:go-kit, micro
  4. 互联网基础设施: 以太坊,hyperledger

Go的环境安装

下载官网

go的官网下载网站,选择合适的系统版本进行安装https://go.dev/dl/

安装步骤

  1. 下载安装包并按照安装包的指引下载相关的内容

  2. 对于Mac系统会直接配置好环境变量,根据官网的安装手册进行安装 https://go.dev/doc/install

  3. 测试GO的版本

    1
    go version

    go version

  4. 测试GO的环境变量

    1
    go env

    go env

GO环境变量

GOROOT路径

GOROOT 表示的是安装包所在的位置,一般不需要修改

GOPATH路径

GOPATH表示的是运行文件所在的位置,表示的是workspace的文件位置,GOPATH是我们的工作空间,保存go项目代码和第三方依赖包GOPATH可以设置多个,其中,第一个将会是默认的包目录,使用 go get 下载的包都会在第一个path中的src目录下,使用 go install时,在哪个GOPATH中找到了这个包,就会在哪个GOPATH下的bin目录生成可执行文件

修改GOPATH的路径

1
export GOPATH="/Users/lihaibin/workspace/golang"

将文件查找的路径设置为GOROOT和GOPATH的并集合

1
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

将两个部分并在一起之后,就能从两个地方开始寻找定义的包

查找文件的路径顺序

首先会从GOROOT进行搜索,接着从GOPATH进行搜索。

GOPATH是开发时的工作目录。用于:

  1. 保存编译后的二进制文件。
  2. go getgo install命令会下载go代码到GOPATH。
  3. import包时的搜索路径

使用GOPATH时,GO会在以下目录中搜索包:

  1. GOROOT/src:该目录保存了Go标准库代码。
  2. GOPATH/src:该目录保存了应用自身的代码和第三方依赖的代码。

GOPATH的弊端

在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端.

  • 无版本控制概念. 在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。

  • 无法同步一致第三方版本号. 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。

  • 无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar

GOPROXY设置

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

GOPROXY 的默认值是:https://proxy.golang.org,directproxy.golang.org国内访问不了,需要设置国内的代理

  • 阿里云 https://mirrors.aliyun.com/goproxy/
  • 七牛云 https://goproxy.cn,direct

并通过以下的命令进行设置

1
go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。

1
go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct

GO111MODULE

GO111MODULE 有三个值:off, on和auto(默认值)。

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形: 当前目录在GOPATH/src之外且该目录包含go.mod文件 当前文件在包含go.mod文件的目录下面。

执行以下命令开启go mod管理

1
go env -w GO111MODULE=on

Go mod操作

1
go mod init github.com/hub/project

Go基本语法

如何编译并运行一个Go文件

对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行

1
go run hello.go

或者将编译和运行两个过程分开,先编译后运行:

1
2
go build hello.go
./ hello

写一个hello.go

首先给出基本框架

1
2
3
4
5
6
7
8
9
10
11
package main

import(
"fmt",
"time"
)

func main(){
fmt.Println("hello world!")
time.Sleep(1*time.Second)
}

程序的第一行声明了名为main的package。一个package会包含一个或多个.go源代码文件。每一个源文件都是以package开头。比如我们的例子里是package main。这行声明语句表示该文件是属于哪一个package。

  • 第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  • 下一行import "fmt"告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数
  • .和没有.导入包的区别,如果一开始引入的时候有.那么就不需要指定哪个包的来调用函数,否则需要再调用函数的时候指定对应的包package
  • 下一行func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

一个程序的main入口函数必须不带任何输入参数和返回结果。而且go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行

变量的声明

声明变量的一般形式是使用 var 关键字

第一种声明:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var v_name v_type
v_name = value
package main


import "fmt"


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


$go run test.go
a = 0

第二种声明:

根据值自行判定变量类型。

1
var v_name = value

第三种声明:

省略var, 注意 :=左侧的变量不应该是已经声明过的,就是:=只能用于没有被声明的变量赋值上,否则会编译错误

1
2
3
4
5
6
7
v_name := value


// 例如
var a int = 10
var b = 10
c : = 10

几种声明类型的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
}

全局变量声明

和一般的定义变量的方式一样

1
2
3
var x, y int
var c, d int = 1, 2
var e, f = 123, "lihaibin"

特殊的定义全局变量的方式,而且:=的定义方式不能够用于定义全局变量

1
2
3
4
5
6
7
8
9
10

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

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

func main() {}

多变量声明

:=不能用于已经被初始化之后的变量的赋值,如果对于_的情况是不具备可读性,相当于忽略

1
2
3
4
5
6
7
8
9
10
11
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
}

常量

常量的声明方式

常量是一个简单值的标识符,在程序运行时,不会被修改的量。常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:

1
2
const identifier [type] = value
const b string = "abc"

隐式定义类型方法:

1
const b = "abc"

多重赋值

1
const a, b, c = 1, false, "str" //多重赋值

枚举类型

1
2
3
4
5
const (
Unknown = 0
Female = 1
Male = 2
)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main


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


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

输出结果为:abc, 3, 16

unsafe.Sizeof(a) = 16

字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。

常量中的iota标识符

在 golang 中,一个方便的习惯就是使用iota标示符,简化了常量用于增长数字的定义。

下面的代码中,当第一行赋值了iota之后,那么相当于初始化位置是0,后面的依次增加是1,2

1
2
3
4
5
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)

如果对iota进行运算,其实相当于是选择当前的行作为iota的取值进行运算,如果中间不对运算加以改变,那么会一直持续按照当前的运算规则执行下去

1
2
3
4
5
6
7
8
9
const (
//关键字 iota
/* BEIJING=0
SHANGHAI=1
SHENGZHENG=2 */
BEIJING = 10 * iota // 默认为0 输出0
SHANGHAI //输出10
HANGZHOU //输出20
)

同样的在同一个const中去定义不同的 iota的计算方式也可以,iota的取值就是选择当前的行,从哪个地方开始改变,那么就改成不同的计算方式

1
2
3
4
5
6
7
8
9
const (
//这个iota的值表示的是和行数有关的数值,因此计算的时候用行来表示
a, b = iota + 1, iota + 2 //每次新开一个关于iota的计算 那么后续全部会跟着这个计算方式下去
c, d
e, f
g, h = iota * 2, iota * 3
//iota只能在const之中使用
i, k
)

以下是输出的内容:

1
a= 1 b= 2 c= 2 d= 3 e= 3 f= 4 g= 6 h= 9 i= 8 k= 12

函数

基本函数的定义

多个返回值初始化设置了函数的形参之后,初始值是0

go每次设置一个变量值之后都有初始值,如果是数据就是0,如果是字符串那么就是空,防止出现一些野指针的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}

输出的结果是

1
Kumar Mahesh

import和init

所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,执行main包中的init函数,最后执行main函数。下图详细地解释了整个执行过程:

init的调用顺序

分别建不同的文件夹对应的就是package的名字,相应的在.go文件内部声明package的名字

main 函数只能在package main中

注意: 在包中设置接口的时候,函数名称必须第一个字母是大写,如果是小写的话将无法识别

  • 如果函数名第一个是大写就是对外开放的函数,认为是public
  • 如果函数名第一个是小写的话就认为是私有的函数,认为是private

接口函数大小写的区别

init函数的调用过程,首先会对包中的init进行初始化再进行调用接口

init()调用顺序和过程

如果你导入了包比如lib1,但是没有使用这个包里面的接口函数,仍然会报错

导入包但是没有使用接口函数

以下是一个import包的例子,首先定义两个不同包以及对应的接口函数和初始化函数的实现

1
2
3
4
5
6
7
package InitLib1

import "fmt"

func init() {
fmt.Println("lib1")
}
1
2
3
4
5
6
7
package InitLib2

import "fmt"

func init() {
fmt.Println("lib2")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

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

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

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

"GolangTraining/InitLib1""GolangTraining/InitLib2"是两个包的地址,go会默认从GOROOT和GOPATH两个默认的位置进行寻找,首先要保证地址的正确性

代码的输出:

1
2
3
4
lib1
lib2
libmain init
libmian main

匿名导包方式

如果我不想调用lib1的函数接口,但是想使用lib1的init()函数怎么办呢,如果这个时候直接导入了包但是不调用接口,就会出现上述的错误

导入包但是没有使用接口函数

在导入的包前面加上下划线来认为这个包是匿名的,这样就能知进行init操作

1
2
3
4
import(
"fmt"
_"lib2"
)

那么这个时候就只会调用init()函数同时不会出错

只调用init()函数

除了能够匿名导包之外,还能给新导入的包起个别的名字,比如叫mylib作为新的别名

给导入的包换个名字

或者直接使用·来进行调用

换别名进行导包

最好别使用这种,如果两个包的函数名称一样那么可能会导致出现歧义的情况

函数值传递

函数如果使用参数,该变量可称为函数的形参。

形参就像定义在函数体内的局部变量。调用函数,可以通过两种方式来传递参数:值传递和指针传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

1
2
3
4
5
6
7
8
9
10
11
12
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

接下来,让我们使用值传递来调用 swap() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main


import "fmt"


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


fmt.Printf("交换前 a 的值为 : %d\n", a )
fmt.Printf("交换前 b 的值为 : %d\n", b )


/* 通过调用函数来交换值 */
swap(a, b)


fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}


/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

运行的结果为:

1
2
3
4
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200

GO指针

和C++以及C中的是一样的,对go中的指针定义的时候 *int 传递变量的地址&

指针和引用的传递例子

在对一个指针赋值的时候,传递的是某一个变量的地址,就是传递这个变量的引用,引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

defer

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数,主要作用:

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

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

1
2
3
4
5
6
7
8
9
func Demo(){
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
defer fmt.Println("4")
}
func main() {
Demo()
}

输出的内容

1
2
3
4
4
3
2
1

数组与切片

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

静态数组

通过这种方式进行初始化数组以及进行切片操作,通过range关键字进行遍历数组,并给出index和value进行给出不同的下标和数值

初始化数组并进行切片操作

固定数组传递的是一个值拷贝

动态数组 slice

切片不需要说明长度

1
2
3
4
5
6
7
8
9
10
11
12
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 := []int{1, 2, 3}
s := arr[:]
//但是这个时候就只会重新赋予空间并复制0
slice1 = make([]int, 4)

/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int

也可以指定容量,其中capacity为可选参数。

1
make([]T, length, capacity)

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

1
s := arr[startIndex:endIndex]

缺省endIndex时将表示一直到arr的最后一个元素,缺省startIndex时将表示从arr的第一个元素开始

1
2
s := arr[startIndex:]
s := arr[:endIndex]

通过切片s初始化切片s1

1
s1 := s[startIndex:endIndex]

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

同时动态数组传递的过程中的参数形式是一致的,能够适配所有的slice参数类型,但是对于

动态数组传递引用同时不会因为长度不一样而改变形参

动态数组的初始化和打印

这里面的下划线表示的是不需要考虑的index的数值,可以忽略,这里是关于切片slice的声明和打印

1
2
3
4
5
	//声明切片之后的长度是3,同时初始化的值是1,2,3
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
}

打印的结果是

输出的结果

声明slice但是不一定声明了空间,因此需要注意的是声明的同时并给出空间大小,同时没办法中途增加空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func  main(){
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
//直接对没有赋予空间的位置修改会出现位置越界
//slice1[3]=999
slice1 = make([]int,4)
//但是这个时候就只会重新赋予空间并复制0
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//slice2[0]=2 //直接赋予数值会出现错误,越界
slice2 = make([]int,3)
//开辟空间,但是默认值都是0
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
slice2[0]=1000
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//此时就修改成功
/*
声明slice类型,同时分配空间
*/
var slice3 =make([]int,5)
fmt.Printf("len = %d, slice =%v\n", len(slice3),slice3)


判断一个切片是不是空的

1
2
3
4
5
6
7
8
9
	/* 
判断一个slice切片是不是为0
*/
if slice1 == nil{
fmt.Println("slice1 is null")
}else{
fmt.Println("slice1 is not null")
}
}

注意if-else的格式有要求,{必须是出现在else和if紧接着的位置,不能换行写

判断当前的动态数组是不是空的

append()和copy()

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

追加元素的操作显示

注意,如果append超过了当前的空间,那么slice就会继续增加空间,增加的大小是cap的大小增加

拷贝copy()操作

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


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

关于切片的截取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
切片的截取
*/
var slice6 = make([]int,3)
slice6[0]=1
slice6[2]=33
fmt.Printf("len = %d, cap = %d, slice =%v\n", len(slice6), cap(slice6), slice6)
//截取的过程中是左闭右开
s1:=slice6[0:2]
// s1:=slice6[:]//表示截取全部
fmt.Println(s1)
//注意此时的s1的地址和slice6的位置一样了

//copy
var s2 = make([]int,3)
copy(s2,slice6)
fmt.Println(s2)// 只会从头开始截取s2长度的,如果长了那么就是0,
//否则就是选择slice6中相同长度的元素,从左到右边

map

map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main
import (
"fmt"
)

func main() {
//第一种声明
var test1 map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
test1 = make(map[string]string, 10)
test1["one"] = "php"
test1["two"] = "golang"
test1["three"] = "java"
fmt.Println(test1) //map[two:golang three:java one:php]


//第二种声明
test2 := make(map[string]string)
test2["one"] = "php"
test2["two"] = "golang"
test2["three"] = "java"
fmt.Println(test2) //map[one:php two:golang three:java]

//第三种声明
test3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java",
}
fmt.Println(test3) //map[one:php two:golang three:java]



language := make(map[string]map[string]string)
language["php"] = make(map[string]string, 2)
language["php"]["id"] = "1"
language["php"]["desc"] = "php是世界上最美的语言"
language["golang"] = make(map[string]string, 2)
language["golang"]["id"] = "2"
language["golang"]["desc"] = "golang抗并发非常good"

fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]]


//增删改查
// val, key := language["php"] //查找是否有php这个子元素
// if key {
// fmt.Printf("%v", val)
// } else {
// fmt.Printf("no");
// }

//language["php"]["id"] = "3" //修改了php子元素的id值
//language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值
//delete(language, "php") //删除了php子元素
fmt.Println(language)
}

面向对象结构体

定义一个结构体

1
2
3
4
//定义一个结构体
type T struct {
name string
}

分别定义不同的拷贝和引用的函数

1
2
3
4
5
6
7
func (t T) method1() {
t.name = "new name1"
}

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

结果是使用值拷贝的输出的name没有改变,只有使用引用的才发生了改变

关于结构体定义的细节,内部的成员变量和结构体本身的大小写就是蕴含了是不是私有和公有的关系,大写标识公有,小写表示私有

1
2
3
4
5
6
// 如果这个类如果是大写,那么其他的Package也能够访问
type Human struct {
Name string //其中变量名称表示大写说明是能够对外界可见的public
Age int
//如果是小写那么就是private的属性
}

对于结构体内部的成员函数,必须是传递了引用的地址才能够修改,否则就是默认的值传递

1
2
3
4
5
6
7
8
9
10
11
func (this *Human) GetName() {
fmt.Println(this.Name)
}
func (this *Human) SetName(newname string) {
//只有是引用地址的传递的时候才是能够修改的
this.Name = newname
}
func (this Human) SetName1(newname string) {
//默认是值传递
this.Name = newname
}

类的继承性

如果新定义的类继承了某个类,那么只需要在内部写上所继承的类的名称,同时这里没有C++中的公有保护等其他类型的继承,公有私有的设定保持一致

1
2
3
4
5
type Superman struct {
Human //表示Superman继承了Human,同时这里没有C++中的公有保护等其他类型的继承
//在子类中重新增加变量
Level int
}

对继承类中的方法重写,同样传递的还是引用和指针

1
2
3
4
5
6
// 对于父类方法进行重写
func (this *Superman) GetName() {
fmt.Println(this.Name)
fmt.Println(this.Level)
}

重新定义新的方法

1
2
3
4
5
6
// 子类中的新方法
func (this *Superman) LevelUp() {
fmt.Println("level up")
this.Level = this.Level + 1
fmt.Println(this)
}

关于主函数中的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
human := Human{Name: "zhang", Age: 99}
human.SetName1("li")
fmt.Println(human) //{zhang 99}
human.SetName("li")
fmt.Println(human) //{li 99}
human.GetName()
fmt.Println("-------------")
superman := Superman{Human{"li4", 18}, 99}
//父类方法的重写
superman.GetName()
//子类新方法
superman.LevelUp()
/* level up
&{{li4 18} 100} */
//父类方法原封不动
superman.SetName("wang5")
fmt.Println(superman) //{{wang5 18} 100}

fmt.Println("+++++++++++++")
//第二种继承类对象的声明
var super Superman
super.Name = "zhangmazi"
super.Level = 100
super.Age = 19
fmt.Println(super)
}

Interface与类型断言

在继承和多态上,一系列家族定义的接口,每个子类能够重写方法,实现同一个方法有多个接口表现形式

类的多态性

本质上利用interface来实现类的多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 本质是一个指针多态
type Animal interface {
//给出接口包含的多态的函数
Sleep()
GetColor() string
GetType() string
}

// 定义一个具体的类
type Cat struct {
/*如果继承一个接口interface 那么就不需要直接写出来继承,
只需要进行实现就可以认为继承了这个接口inerface
*/
color string
}

// 第二个多态的类
type Dog struct {
//同样需要进行继承这个interface
color string
}

那怎么认为这个cat继承了这个animal类呢?只需要对animal中的所有函数重写即可认为是继承了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 必须要完全重写所有的接口才能认为是多态满足 */
func (this *Cat) Sleep() {
fmt.Println("cat sleep...")
}

func (this *Cat) GetColor() string {
fmt.Printf("the cat color is %v\n", this.color)
return this.color
}

func (this *Cat) GetType() string {
fmt.Printf("the type is cat\n")
return "Cat"
}

同理对于dog也是一样

1
2
3
4
5
6
7
8
9
10
11
12
func (this *Dog) Sleep() {
fmt.Println("dog sleep...")
}
func (this *Dog) GetColor() string {
fmt.Printf("the dog color is %v\n", this.color)
return this.color
}

func (this *Dog) GetType() string {
fmt.Printf("the type is dog\n")
return "Dog"
}

主函数中如何实现不同的多态调用呢?注意哦,这个地方传递的是继承类的引用进去来实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func showanimal(animal Animal) {
animal.Sleep()
// fmt.Println("color = ",animal.GetColor())
// fmt.Println("type = ",animal.GetType())
animal.GetType()
animal.GetColor()
}


cat := Cat{"Green"}
dog := Dog{"Yellow"}

//通过传递指针和引用来实现多态性
showanimal(&cat)
showanimal(&dog)

万能类型interface

golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。

1
2
3
func funcName(a interface{}) string {
return string(a)
}

interface{}相当于是一个万能的数据类型,适用于对任何的函数的参数传递中的使用

  • 直接断言使用
1
2
var a interface{}
fmt.Println("Where are you,Jonny?", a.(string))

如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断

1
value, ok := a.(string)

如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。

interface{}的例子

定义一个断言类型的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func myFunc(arg interface{}) {
fmt.Println("myfunc is called...")
fmt.Println(arg)

//interface{}如何确定类型是什么呢?

//类型断言机制 用于判断是什么类型
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not a string")
fmt.Printf("the value is %T\n", arg) //the value is main.Book
} else {
fmt.Println("arg is string type,is = ", value)
}
}

主函数的调用关系如下:

1
2
3
4
5
6
7
8
9
type Book struct {
auth string
}
func main() {
book := Book{"golang"}
myFunc(book) //{golang}
myFunc(100) //100
myFunc("goland")
}

输出的内容是:

1
2
3
4
5
6
7
8
9
10
11
#输出的值
myfunc is called...
{golang}
arg is not a string
the value is main.Book
myfunc is called...
100
arg is not a string
the value is int
myfunc is called...
goland

Reflect反射

在讲反射之前,先来看看Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型

1
(value, type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型concrete type,另外一个指针指向实际的值对应value

reflect的基本功能

reflect的反射类型对象:TypeOf和ValueOf

那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf()reflect.TypeOf()

1
2
3
4
5
6
7
8
9
10
11
12
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}

//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345

fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}

运行结果:
type: float64
value: 1.2345

说明:

  1. reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 "Allen.Wu" 25} 这样的结构体struct的值
  3. 反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Typereflect.Value这两种

reflet例子1:

1
2
3
4
func reflectNum(arg interface{}) {
fmt.Println("type:", reflect.TypeOf(arg))
fmt.Println("value:", reflect.ValueOf(arg))
}

主函数的调用

1
2
3
4
func main() {
var num float64 = 1.2345
reflectNum(num)
}

注意,在使用反射之前需要引入reflect的包

1
2
3
4
import (
"fmt"
"reflect"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var num float64 = 1.2345

pointer := reflect.ValueOf(&num)
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := value.Interface().(float64)

fmt.Println(convertPointer)
fmt.Println(convertValue)
}

运行结果:
0xc42000e238
1.2345

reflet例子2:

  • 首先定义一个类以及关于这个类的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type User struct {
    Id int
    Name string
    Age int
    }

    func (this User) Call() { //为什么这个地方返回类型不是*
    fmt.Println("user is called ..")
    fmt.Printf("%v\n", this)
    }
  • 再定义一个利用反射选择类中值和方法的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func DoFiledAndMethod(input interface{}) {
//获取输入的类型type
inputType := reflect.TypeOf(input)
fmt.Println("input type is: ", inputType.Name())
//output: input type is: User

//获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("input value is: ", inputValue)
//output: input value is: {1 eric 19}

//通过type获取其中的字段
/*
1、获取interface中的reflect的type, 通过type得到numfield,进行遍历
2、得到每个filed,就是数据类型
3、通过filed中有一个interface()方法得到对应的value
*/
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
// value:=inputType.Field{i}.interface()
value := inputValue.Field(i).Interface()
// fmt.Println(field) //{Id int 0 [0] false}、{Name string 8 [1] false}
/*
每个field表示的就是一行的元素内容,其中Name表示了这一行的变量名, Type表示的是这一行的类型
通过value单独存在field里面,根据索引值寻找通过Interface()调用
*/
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)

}

/* 如何遍历选择方法呢
也是根据inputType来进行划分
*/
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

注意点:

  1. reflect.TypeOf(input)得到类型
  2. reflect.ValueOf(input)得到对应的值
  3. reflect.TypeOf(input).NumField()的方法是获得interface()中的所有的字段
  4. 如果选择字段中的类型: inputType.Field(i) = reflect.TypeOf(input).Field(i)
  5. 如果选择字段中的值:inputType.Field(i) = reflect.TypeOf(input).Field(i).Interface()
  6. 如果想便利interface中的方法:reflect.TypeOf(input).NumMethod(),其中具体的方法是:reflect.TypeOf(input).Method(i)

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式

通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

运行结果:
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77
  1. 需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针
  2. 如果传入的参数不是指针,而是变量,那么
    • 通过Elem获取原始值对应的对象则直接panic
    • 通过CanSet方法查询是否可以设置返回false
  1. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
  2. reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的,是指只能修改原是对象的值的大小,不能修改地址
  3. newValue.SetFloat(77)重新设置值的操作,传递引用来修改interface中的值

反射的基本原理

反射基本原理总结

结构体标签

本质上还是利用了反射,通过以下形式给结构体中的变量添加标签作用: 其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用。

作用:能够将结构体转化为json格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"reflect"
)
type resume struct{
/*
通过以下形式给结构体中的变量添加标签
作用: 其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用
*/
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}

func findtag(str interface{}){
t :=reflect.TypeOf(str).Elem()

for i:=0;i<t.NumField();i++{
tagstring:=t.Field(i).Tag.Get("info")
tagdoc:=t.Field(i).Tag.Get("doc")
fmt.Println("info:",tagstring,"doc",tagdoc)
}
}

func main(){
var re resume
findtag(&re) //注意这里传递的是引用
}

go printf的占位符格式

go printf的占位符表示

输出之后在json格式转换中可以看到如下,注意可以看到的是输出的内容是根据给定的tag来进行标题的命名的

转化为json格式的例子

利用反射取出元素查询

利用编码和解码对struct 和json之间的转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"encoding/json"
)

//如何将结构体转化成json
type Movie struct{
Title string `json:"title"` //就是告诉json库对应的变量的标签名字是这个
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"` //slice `json:"actors"`
}
func main(){
movie :=Movie{"喜剧之王",2000,10,[]string{"zhouxingchi","zhangbozhi"}}

//编码的过程就是将 struct-->json

jsonStr,err:=json.Marshal(movie)

//返回两个字段

if err!=nil{
fmt.Println("json marshal error",err)
return
}
fmt.Printf("jsonStr=%s\n",jsonStr )
/* jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"Actors":["zhouxingchi","zhangbozhi"]} */

//解码过程:json-->struct
//jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"actors":["zhouxingchi","zhangbozhi"]}

myMovie:=Movie{}
//注意这个地方传递的参量是引用
err=json.Unmarshal(jsonStr,&myMovie)
if err!=nil{
fmt.Println("json unmashal error",err)
return
}
fmt.Printf("%v\n",myMovie)
//返回结构体
//{喜剧之王 2000 10 [zhouxingchi zhangbozhi]}
}

【后端开发】Golang语法基础
https://lihaibineric.github.io/2023/11/27/develop_go_basic/
Author
Haibin Li
Posted on
November 27, 2023
Updated on
February 22, 2024
Licensed under