Golang基础
第一个go程序
新建一个文件helloworld.go
package main // 声明包名main
import "fmt" // 导入fmt标准库包
func main() { // 定义main函数
fmt.Println("hello world") // 调用fmt包的Println方法打印字符串到控制台
}
使用go run helloworld.go
直接编译运行
liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ go run helloword.go
hello world
或者先编译成二进制文件,再执行二进制文件
liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ go build helloword.go
liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ ./helloword.exe
hello world
程序结构
Go 源文件以 package 声明开头,说明源文件所属的包,接着使用 import 导入依赖的包,其
次为包级别的变量、常量、类型和函数的声明和赋值。函数中可定义局部的变量、常量等
基本组成元素
标识符
标识符是编程时所使用的名字,用于给变量、常量、函数、类型、接口、包名等进行命名,以建立名称和使用之间的关系,Go语言标识符的命名规则:
- 只能由非空字母、数字、下划线组成
- 只能以字母或者下划线开头
- 不能使用Go关键字
- 避免使用Go语言预定标识符
- 建议使用驼峰命名,不强制
- 标识符区分大小写
Go语言提供一些预定的标识符来表示内置的常量、类型、函数,在定义标识符时应避免使用
- 内置常量:true、false、nil、iota
- 内置类型:bool、byte、rune、int、int8、int16、int32、int64、uint、uint8、unit16、unit32、unit64、uintptr、float32、float64、complex64、complex128、string、error
- 内置函数:make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
- 空白标识符:_
关键字
关键字用于特性的语法解构,Go语言定义25个关键字:
- 声明:import、package
- 实体声明和定义:chan、const、func、interface、map、struct、type、var
- 流程控制:break、case、continue、default、defer、else、fallthrough、for、go、goto、if、range、return、select、switch
字面量
字面量是值的表示方法,常用与对变量/常量进行初始化、主要分为:
- 表示基础数据类型值的字面量,例如:0, 1.1, true, 3 + 4i, ‘a’, “我爱中国”
- 构造自定义的复合数据类型的类型字面量,例如:type Interval int
- 用于表示符合数据类型值的复合字面量,用来构造 array、slice、map、struct 的值,例如:{1, 2, 3}
操作符
- 算术运算符:
+
、-
、*
、/
、%
、++
、--
- 关系运算符:
>
、>=
、<
、<=
、==
、!=
- 逻辑运算符:
&&
、||
、!
- 位运算符:
&
、|
、^
、<<
、>>
、&^
- 赋值运算符:
=
、+=
、-=
、*=
、/=
、%=
、&=
、|=
、^=
、<<=
、>>=
- 其他运算符:
&
(单目)、*
(单目)、.
(点)、-
(单目)、…
、<-
分隔符
小括号()
, 中括号[]
,大括号{}
,分号;
,逗号,
变量
变量是指对一块存储空间定义名称,通过名称对存储空间的内容进行访问或修改,使用 var
进行变量声明,常用的语法为:
- var 变量名 变量类型 = 值
定义变量并进行初始化
var name string = "jerry"
- var 变量名 变量类型
定义变量使用零值进行初始化
var age int
- var 变量名 = 值
定义变量,变量类型通过值类型进行推导
var isMale = true
- var 变量名 1, 变量名 2 , …, 变量名 n 变量类型
定义多个相同类型的变量并使用零值进行初始化
var prefix, suffix string
- var 变量名 1, 变量名 2 , …, 变量名 n 变量类型 = 值 1, 值 2, …, 值 n
定义多个相同类型的变量并使用对应的值进行初始化
var prev, next int = 3, 4
- var 变量名 1, 变量名 2 , …, 变量名 n = 值 1, 值 2, …, 值 n
定义多个变量并使用对应的值进行初始化,变量的类型使用值类型进行推导,类型可不
相同
var name, age = "tom", 18
- 批量定义
定义多个变量并进行初始化,批量复制中变量类型可省略
var (
name string = "jerry"
age int = 18
)
变量简短声明
在函数内可以通过简短声明语句声明并初始化变量,可通过简短声明同时声明和初始化多个
变量,需要注意操作符左侧的变量至少有一个未定义过
gender := "male"
注意,简短声明只能在函数中使用,且变量短声明不能指定类型,类型都是通过推导出来
常量
常量用于定义不可被修改的的值,需要在编译过程中进行计算,只能为基础的数据类型布尔、
数值、字符串,使用 const 进行常量声明,常用语法:
- const 常量名 类型 = 值
定义常量并进行初始化
const pi float64 = 3.1415926
- const 常量名 = 值
定义常量,类型通过值类型进行推导
const e = 2.7182818
- 批量定义
定义多个变量并进行初始化,批量赋值中变量类型可省略,并且除了第一个常量值外其他常量可同时省略类型和值,表示使用前一个常量的初始化表达式
const (
name string = "baihuzi"
desc
)
常量之间的运算,类型转换,以及对常量调用函数 len、cap、real、imag、complex、
unsafe.Sizeof 得到的结果依然为常量
作用域
作用域指变量可以使用范围。go 语言使用大括号显示的标识作用域范围,大括号内包含一连串的语句,叫做语句块。语句块可以嵌套,语句块内定义的变量不能在语句块外使用。作用域内定义变量只能被声明一次且变量必须使用,否则编译错误。在不同作用域可定义相同的变量,此时局部将覆盖全局
常见隐式语句块:
- 全语句块
- 包语句块
- 文件语句块
- if、switch、for、select、case 语句块
基础数据类型
布尔类型
布尔类型用于表示真假,类型名为 bool,只有两个值 true 和 false,占用一个字节宽度,零值为 false,常用操作:
逻辑运算
与(&&),只有左、右表达式结果都为 true,运算结果为 true
func main() {
isTrue, isFalse := true, false
fmt.Println(isTrue && isFalse) // false
fmt.Println(isTrue && isTrue) // true
fmt.Println(isFalse && isTrue) // false
fmt.Println(isFalse && isFalse) // false
}
或(||),只要左、右表达式有一个为 true,运算结果为 true
func main() {
isTrue, isFalse := true, false
fmt.Println(isTrue || isFalse) // true
fmt.Println(isTrue || isTrue) // true
fmt.Println(isFalse || isTrue) // true
fmt.Println(isFalse || isFalse) // false
}
非(!),右表达式为 true,运算结果为 false;右表达式为 false,运算结果为 true
func main() {
isTrue, isFalse := true, false
fmt.Println(!isTrue) // false
fmt.Println(!isFalse) // true
}
关系运算
等于(==),不等于(!=)
func main() { // 定义main函数
isTrue, isFalse := true, false
fmt.Println(isTrue == isFalse) // false
fmt.Println(isTrue != isFalse) // true
}
数值类型
整型
Go 语言提供了5种有符号、5种无符号、1种指针、1种单字节、1种单个unicode字符(unicode码点),共13种整数类型,零值均为0
类型名 | 字节宽度 | 说明&取值范围 |
---|---|---|
int | 与平台有关,32位系统4字节,64位系统8字节 | 有符号整型 |
uint | 与平台有关,32位系统4字节,64位系统8字节 | 无符号整形 |
rune | 4 字节 | Unicode 码点,取值范围同 uint32 |
int8 | 1 字节 | 用 8 位表示的有符号整型,取值范围为:[-128, 127] |
int16 | 2 字节 | 用 16 位表示的有符号整型,取值范围为:[-32768, 32767] |
int32 | 4 字节 | 用 32 位表示的有符号整型,取值范围为:[-2147483648, 2147483647] |
int64 | 8 字节 | 用 64 位表示的有符号整型,取值范围为:[-9223372036854775808, 9223372036854775807] |
uint8 | 1 字节 | 用 8 位表示的无符号整型,取值范围为:[0, 255] |
uint16 | 2 字节 | 用 16 位表示的无符号整型,取值范围为:[0, 65535] |
uint32 | 4 字节 | 用 32 位表示的无符号整型,取值范围为:[0, 4294967295] |
uint64 | 8 字节 | 用 64 位表示的无符号整型,取值范围为:[0, 18446744073709551615] |
byte | 1 字节 | 字节类型,取值范围同 uint8 |
uintptr | 与平台有关,32 位系统 4 字节,64 位系统 8 字节 | 指针值的无符号整型 |
进制
- 十进制
以 10 为基数,采用 0-9 十个数字,逢 10 进位,例如:10 - 八进制
以 8 为基数,采用 0-7 八个数字,逢 8 进位,使用 0 开头表示为八进制表示,例如:010 - 十六进制
以 16 为基数,采用 0-9 十个数字和 A-F 六个字母,逢 16 进位,使用0X 开头表示为十六进制 ,例如:0X10
func main() {
fmt.Println(10, 010, 0x10) // 10 8 16
}
常用操作
算数运算, +、-、*、/、%、++、–
注意:针对
/
,除数不能为 0,且结果依然为整数
func main() {
n1, n2 := 1, 2
fmt.Println(n1 + n2) // 3
fmt.Println(n1 - n2) // -1
fmt.Println(n1 * n2) // 2
fmt.Println(n1 / n2) // 0
fmt.Println(n1 % n2) // 1
n1++
n2--
fmt.Println(n1, n2) // 2 1
}
关系运算:>、>=、<、<=、==、!=
func main() {
n1, n2 := 1, 2
fmt.Println(n1 > n2) // false
fmt.Println(n1 < n2) // true
fmt.Println(n1 >= n2) // false
fmt.Println(n1 <= n2) // true
fmt.Println(n1 == n2) // false
fmt.Println(n1 != n2) //true
}
位运算:&、|、^、&^、<<、>>
对于负整数在计算机中使用补码进行表示,对应正整数二进制表示取反+1,针对左、右移的右操作数必须为无符号整型
&
:按位与,如果两个相应的二进制位都为1,则该位的结果值为1,否则为0|
:按位或,两个相应的二进制位中只要有一个为1,该位的结果值为1^
:按位异或,若参加运算的两个二进制位值相同则为0,否则为1&^
:如果运算符右侧数值的第 i 位为 1,那么计算结果中的第 i 位为 0;如果运算符右侧数值的第 i 位为 0,那么计算结果中的第 i 位为运算符左侧数值的第 i 位的值。<<
:左移,各二进位全部左移若干位,正数高位补0,负数高位补1,低位丢弃>>
:右移,各二进位全部右移若干位,正数高位补0,负数高位补1,低位丢弃
func main() {
n1, n2 := 1, 2
fmt.Println(n1 & n2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n2 = 2 0000 0000 0000 0000 0000 0000 0000 0010
// n1 & n2 0000 0000 0000 0000 0000 0000 0000 0000
// 结果:0
fmt.Println(n1 | n2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n2 = 2 0000 0000 0000 0000 0000 0000 0000 0010
// n1 | n2 0000 0000 0000 0000 0000 0000 0000 0011
// 结果:3
fmt.Println(n1 ^ n2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n2 = 2 0000 0000 0000 0000 0000 0000 0000 0010
// n1 ^ n2 0000 0000 0000 0000 0000 0000 0000 0011
// 结果:3
fmt.Println(n1 &^ n2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n2 = 2 0000 0000 0000 0000 0000 0000 0000 0010
// n1 ^ n2 0000 0000 0000 0000 0000 0000 0000 0001
// 结果:1
fmt.Println(n1 << 2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n1 << 2 0000 0000 0000 0000 0000 0000 0000 0100
// 结果:4
fmt.Println(n1 >> 2)
// n1 = 1 0000 0000 0000 0000 0000 0000 0000 0001
// n1 >> 2 0000 0000 0000 0000 0000 0000 0000 0000
// 结果:0
}
浮点型
浮点数用于表示带小数的数字,Go 提供 float32 和 float64 两种浮点类型
字面量:
- 十进制表示法:3.1415926
- 科学记数法:1e-5
复数型
Go 提供 complex64 和 complex128 两种复数类型,针对 complex64 复数的实部和虚部均使用float32,针对 complex128 复数的实部和虚部均使用 float64
字面量:
- 十进制表示法:
1 + 2i
,i*i = -1
, 1 为实部,2 为虚部
常用函数:
- complex: 工厂函数,通过两个参数创建一个复数
- real:用于获取复数的实部
- imag: 用于获取复数的虚部
func main() {
var (
c1 complex64 = 1 + 2i
c2 complex64 = complex(3, 4)
)
fmt.Println(c1, c2) // (1+2i) (3+4i)
fmt.Println(real(c2), imag(c2)) // 3 4
}
字符串
Go 语言内置了字符串类型,使用 string 表示
字面量:
- 可解析字符串:通过双引号(“)来创建,不能包含多行,支持特殊字符转义序列
- 原生字符串:通过反引号(`)来创建,可包含多行,不支持特殊字符转义序列
特殊字符:
- \:反斜线
- ':单引号
- ":双引号
- \a:响铃
- \b:退格
- \f:换页
- \n:换行
- \r:回车
- \t:制表符
- \v:垂直制表符
- \ooo:3 个 8 位数字给定的八进制码点的 Unicode 字符(不能超过\377) - \uhhhh:4 个 16 位数字给定的十六进制码点的 Unicode 字符
- \Uhhhhhhhh:8 个 32 位数字给定的十六进制码点的 Unicode 字符
- \xhh:2 个 8 位数字给定的十六进制码点的 Unicode 字符
常用操作
- 字符串连接:+
- 关系运算符:>、>=、<、<=、==、!=
- 赋值运算符:+=
- 索引:s[index],针对只包含 ascii 字符的字符串
- 切片:s[start:end] ,针对只包含 ascii 字符的字符串
字符串遍历,遍历的结果是码点,且由于中文和英文占的字节数不同,遍历的索引位也不相同,GBK编码,一个汉字占两个字节。 UTF-8编码是变长编码,通常汉字占三个字节,因此遍历的结果如下所示
func main() {
str := "我不是药神abcde"
for i, v := range str {
fmt.Println(i, v)
}
// 0 25105
// 3 19981
// 6 26159
// 9 33647
// 12 31070
// 15 97
// 16 98
// 17 99
// 18 100
// 19 101
}
枚举类型
go 中并没有枚举类型,但是可以通过 const 来模拟枚举,常使用 iota 生成器用于初始化一系列相同规则的常量,批量声明常量的第一个常量使用iota 进行赋值,此时 iota 被重置为 0,其他常量省略类型和赋值,在每初始化一个常量则加 1
func main() {
const (
SUNDAY int = 0
MONDAY int = 1
TUESDAY int = 2
WEDNESDAY int = 3
THURSDAY int = 4
FRIDAY int = 5
SATURDAY int = 6
)
fmt.Println(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)
// 0 1 2 3 4 5 6
}
func main() {
// 在文件中使用的 const 来模拟枚举,每次都要定义一个数,非常累赘;
// 这里可以引入一个 iota 变量,这个变量有个特点就是在【同一个小括号内】,每调用一次,就会+1,类似一个每次+1的生成器;
const (
SUNDAY = iota
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
)
fmt.Println(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)
// 0 1 2 3 4 5 6
}
注意iota的特性
func main() {
const (
n1 = iota
n2
n3
n4 = 100
n5 = iota
n6
)
fmt.Println(n1, n2, n3, n4, n5, n6)
// 0 1 2 100 4 5
}
指针类型
每个变量在内存中都有对应存储位置(内存地址),可以通过&运算符获取。指针是用来存储变量地址的变量
- 声明:指针声明需要指定存储地址中对应数据的类型,并使用*作为类型前缀。指针变量声明后会被初始化为 nil,表示空指针
- 初始化:
- 使用&运算符+变量初始化:&运算获取变量的存储位置来初始化指针变量
- 使用 new 函数初始化:new 函数根据数据类型申请内存空间并使用零值填充,并返回申请空间地址
- 操作:可通过*运算符+指针变量名来访问和修改对应存储位置的值
func main() {
// 对于数字这种数据类型,当使用一个变量接收另一个变量时,会在内存中开辟一块新的内存地址用来保存,从而无法修改原始变量
var num int = 100
var num1 int = num
num1 = 200
fmt.Println(num, num1) // 100 200
num3 := 500
var num4 *int = &num3 // *int 表示指针类型 同理 *string 也是,在类型前加上*即表示指针 &可以拿到一个变量的指针
*num4 = 600
fmt.Println(num3, *num4) // 600 600
// 可以申请一个空指针,用来存储数据
num5 := new(int) // num5此时是一个指针 *int类型,默认是零值 0
fmt.Println(num5, *num5) // *int 0
*num5 = 999
fmt.Println(num5, *num5) // *int 999
str1 := new(string)
fmt.Println(str1, *str1) // *string ""
}
流程控制
Go语言中的流程控制和其他编程语言基本一样,可能就是语法上的差异而已,流程控制的逻辑都是一样的,需要说明的是,在Go语言中,是没有while循环的,需要使用for循环代替。还有switch case语法,和js是不一样的,这些都是根据语言语法规定的。
func main() {
// if else
condition := true
if condition == true {
fmt.Println("true")
} else {
fmt.Println("false")
}
// if else if
score := 100
if score == 100 {
fmt.Println("pretty good")
} else if score >= 90 {
fmt.Println("good")
} else if score >= 60 {
fmt.Println("ok")
} else {
fmt.Println("bad")
}
// switch case
switch {
case score == 100:
fmt.Println("pretty good")
case score >= 90:
fmt.Println("good")
case score >= 60:
fmt.Println("ok")
default:
fmt.Println("bad")
}
// for循环
sum := 0
for index := 1; index <= 100; index++ {
sum += index
}
fmt.Println(sum)
// go 中没有while循环,使用for循环代替
sum1 := 0
index1 := 1
for index1 <= 100 {
sum1 += index1
index1++
}
fmt.Println(sum1)
// 死循环
sum2 := 0
index2 := 1
for {
if index2 > 100 {
break
}
sum2 += index2
index2++
}
fmt.Println(sum2)
}
跳出循环
- break:跳出当前循环
- continue:跳出本次循环,开始下一次循环
- break [LABEL]:结合label跳出外层循环
func main() {
for i := 0; i <= 10; i++ {
if i == 5 {
break // break 跳出循环
}
fmt.Println(i)
}
fmt.Println("-----------------------")
for j := 0; j <= 10; j++ {
if j == 5 {
continue // continue 跳出本次循环,开始下一次循环
}
fmt.Println(j)
}
fmt.Println("-----------------------")
// 默认 break 只能跳出当前循环,不能跳出外级循环
for k := 0; k <= 10; k++ {
for u := 0; u <= 10; u++ {
if u == 5 {
break // continue 跳出本次循环,开始下一次循环
}
fmt.Println(k, u)
}
}
fmt.Println("-----------------------")
// 如果想要跳出外层循环,需要结合label
LOOP:
for x := 0; x <= 10; x++ {
for y := 0; y <= 10; y++ {
if y == 5 {
break LOOP // continue 跳出本次循环,开始下一次循环
}
fmt.Println(x, y)
}
}
// 和js里面的一样
}
label和goto
goto 表示代码运行过程中跳转到哪里,START 和 END 不是固定的,可以自己取值
func main() {
sum := 0
index := 1
START:
if index > 100 {
goto END
}
sum += index
index++
goto START
END:
fmt.Println(sum)
}
复合数据类型
数组
数组声明需要指定组成元素的类型以及存储元素的数量(长度)。在数组声明后,其长度不可修改,数组的每个元素会根据对应类型的零值对进行初始化。
字面量声明数组
- 指定数组长度: [length]type{v1, v2, …, vlength}
- 使用初始化元素数量推导数组长度: […]type{v1, v2, …, vlength}
- 对指定位置元素进行初始化: [length]type{im:vm, …, sin:in}
func main() {
// go 中数组不可删除和添加 因为长度会影响类型,类型固定后就不能添加和删除了,和js不同
// 长度一旦固定则不可变,一般在go中很少使用,一般使用切片
var arr1 [10]int = [10]int{}
fmt.Printf("%T, %v \n", arr1, arr1) // [10]int, [0 0 0 0 0 0 0 0 0 0]
arr2 := [10]int{}
fmt.Printf("%T, %v \n", arr2, arr2) // [10]int, [0 0 0 0 0 0 0 0 0 0]
arr3 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Printf("%T, %v \n", arr3, arr3) // [10]int, [1 2 3 4 5 6 7 8 9 10]
arr4 := [10]int{1, 2, 3, 4, 5}
fmt.Printf("%T, %v \n", arr4, arr4) // [10]int, [1 2 3 4 5 0 0 0 0 0]
arr5 := [10]int{0: 1, 3: 2, 5: 3}
fmt.Printf("%T, %v \n", arr5, arr5) // [10]int, [1 0 0 2 0 3 0 0 0 0]
str1 := [5]string{}
fmt.Printf("%T, %q \n", str1, str1) // [5]string, ["" "" "" "" ""]
str2 := [5]string{"abc", "def"}
fmt.Printf("%T, %q \n", str2, str2) // [5]string, ["abc" "def" "" "" ""]
str3 := [5]string{0: "abc", 3: "def"}
fmt.Printf("%T, %q \n", str3, str3) // [5]string, ["abc" "" "" "def" ""]
// ...推导
var arr6 [10]int
fmt.Printf("%T, %v \n", arr6, arr6) // [10]int, [0 0 0 0 0 0 0 0 0 0]
arr6 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // ... 会按照后面给定的个数自动推导
fmt.Printf("%T, %v \n", arr6, arr6) // [10]int, [1 2 3 4 5 6 7 8 9 10]
// arr6 = [...]int{1,2,3} // 报错 cannot use [...]int{…} (value of type [3]int) as type [10]int in assignment
for i := 0; i < len(arr6); i++ {
fmt.Println(arr6[i])
}
for index := range arr6 {
fmt.Println(arr6[index])
}
for index, value := range arr6 {
fmt.Println(index, value)
}
for _, value := range arr6 {
fmt.Println(value)
}
}
切片
切片是长度可变的数组(具有相同数据类型的数据项组成的一组长度可变的序列),切片由三部分组成:
- 指针:指向切片第一个元素指向的数组元素的地址
- 长度:切片元素的数量
- 容量:切片开始到结束位置元素的数量
func main() {
// 切片是相同类型长度可变的元素集合
// nil切片, 不赋值,可以理解威切片的零值是nil
var slice []int
fmt.Println(slice, slice == nil) // [] true
// 空切片
slice1 := []int{}
fmt.Println(slice1, slice1 == nil) // [] false
slice2 := []int{0: 1, 10: 10}
fmt.Println(slice2) // [1 0 0 0 0 0 0 0 0 0 10]
slice2[4] = 4
fmt.Println(slice2) // [1 0 0 0 4 0 0 0 0 0 10]
// 通过make函数来创建切片,类似于js中的 new Array(5).fill(0)
slice3 := make([]int, 5)
fmt.Println(slice3) // [0 0 0 0 0]
slice4 := make([]string, 5)
fmt.Printf("%q \n", slice4) // ["" "" "" "" ""]
// 切片容量
// 通过make函数的第三个参数,可以指定切片的容量
// 切片在底层使用数组进行数据存储,数组是有固定长度的,当切片的长度超过了数组的长度,切片将会申请一个更长的数组,再将原来的数组拷贝过来
slice5 := make([]int, 5, 10)
fmt.Println(slice5, len(slice5), cap(slice5)) // [0 0 0 0 0] 5 10
// 切片可以通过数组进行切片操作来获得,切片的长度既截取出来的长度3,切片的容量既数组的长度减去开始索引位 10 - 2 = 8
arr1 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice6 := arr1[2:5]
fmt.Println(slice6, len(slice6), cap(slice6)) // [3 4 5] 3 8
// !!!需要注意的是,通过数组切片操作获得的切片,底层共享数组的数据
// 可以看到我们此时将数组的第五个元素修改成为100,切片slice6同样会发生修改,使用时需要特殊注意
arr1[4] = 100
fmt.Println(arr1, slice6) // [1 2 3 4 100 6 7 8 9 10] [3 4 100]
// 遍历,和遍历数组一样,可以通过下标或者range
slice7 := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice7); i++ {
fmt.Println(slice7[i])
}
for index, value := range slice7 {
fmt.Println(index, value)
}
// 添加 append(slice, item1, item2, item3, ...),append接收一个添加了切片的返回值,不会直接在原始切片中添加元素
slice8 := append(slice7, 6, 7, 8)
fmt.Println(slice7, slice8) // [1 2 3 4 5] [1 2 3 4 5 6 7 8]
// 删除,在go中没有直接提供删除切片元素的方法,例如js中的pop shift splice
// 可以使用切片操作和解构来进行切片的删除
slice9 := []int{1, 2, 3, 4, 5}
// 删除最后一个元素
slice10 := slice9[0 : len(slice9)-1]
fmt.Println(slice9, slice10) // [1 2 3 4 5] [1 2 3 4]
// 删除第一个元素
slice11 := slice9[1:len(slice9)] // 如果取到末尾,len(slice9)可省略
fmt.Println(slice9, slice11) // [1 2 3 4 5] [2 3 4 5]
// 删除中间元素, 比如删除3,索引位2的元素
slice12 := append(slice9[0:2], slice9[3:]...) // 这里注意,会修改原始slice9切片, go中的解包操作符...是写在后面,而js中是写在前面
fmt.Println(slice9, slice12) // [1 2 4 5 5] [1 2 4 5]
// 切片的切片
slice13 := make([]int, 5, 10)
slice13[0] = 1
slice13[1] = 2
slice13[2] = 3
fmt.Println(slice13, len(slice13), cap(slice13)) // [1 2 3 0 0] 5 10
// 切片的endIndex可以超过长度,但不能超过容量,没有赋值的部分取零值
fmt.Println(slice13[1:10]) // [2 3 0 0 0 0 0 0 0]
// copy函数
// 如果src length 和 dst length 相等,则直接进行覆盖
dst := make([]int, 3)
src := []int{1, 2, 3}
copy(dst, src)
fmt.Println(dst, src) // [1 2 3] [1 2 3]
// 如果src length > dst length,则多出的部分会被舍弃
dst1 := make([]int, 3)
src1 := []int{1, 2, 3, 4}
copy(dst1, src1)
fmt.Println(dst, src) // [1 2 3] [1 2 3]
// 如果src length < dst length,则只会覆盖src存在的部分
dst2 := make([]int, 3)
src2 := []int{1, 2}
copy(dst2, src2)
fmt.Println(dst2, src2) // [1 2 0] [1 2]
}
映射
映射是存储一系列无序的 key/value 对,通过 key 来对 value 进行操作(增、删、改、查)。映射的 key 只能为可使用==运算符的值类型(字符串、数字、布尔、数组),value 可以为任意类型 s。
map 声明需要指定组成元素 key 和 value 的类型,在声明后,会被初始化为 nil,表示暂不存在的映射
初始化
- 使用字面量初始化:map[ktype]vtype{k1:v1, k2:v2, …, kn:vn}
- 使用字面量初始化空映射:map[ktype]vtype{ }
- 使用 make 函数初始化
make(map[ktype]vtype),通过 make 函数创建映射
func main() {
// key value 的无序集合, 遍历时不保证顺序,在go中通过hash table实现
// 定义 map[keyType]valueType,零值是nil
// 字面量定义,零值是nil
var map1 map[string]string
fmt.Printf("%T %v \n", map1, map1 == nil) // map[string]string true
// 字面量定义并赋值
map2 := map[string]int{"key1": 1}
fmt.Printf("%v %v \n", map2, map2 == nil) // map[key1:1] false
// 增
// 直接对key进行赋值,map是引用类型
mapNew := map2
map2["newKey"] = 11
fmt.Println(map2, mapNew) // map[key1:1 newKey:11] map[key1:1 newKey:11]
// 删
// 调用内置函数delete
delete(map2, "newKey")
fmt.Println(map2) // map[key1:1]
// 改
// 直接对key的value进行修改
map2["key1"] = 999
fmt.Println(map2) // map[key1:999]
// 查
// 如果key对应的value存在,则直接返回对应的value
value := map2["key1"]
fmt.Println(value) // 999
// 如果key对应的value不存在,则返回value类型的零值
value2 := map2["key2"]
fmt.Println(value2) // 0
// 判断对应的value存不存在,可以接受第二个返回值,布尔类型,即对应key的value存不存在,不需要自己取值手动判断
value3, exist3 := map2["key1"]
fmt.Println(value3, exist3) // 1 true
value4, exist4 := map2["key2"]
fmt.Println(value4, exist4) // 0 false
// 遍历,不保证顺序,没有index,只能用range遍历
for key := range map2 {
fmt.Println(key, map2[key])
} // key1 999
for key, value := range map2 {
fmt.Println(key, value)
} // key1 999
// 通过make创建map
map3 := make(map[int]string)
map3[0] = "0"
map3[1] = "1"
fmt.Println(map3) // map[0:0 1:1]
}
函数
函数用于对代码块的逻辑封装,提供代码复用的最基本方式,函数也可以赋值给变量,存储在数组、切片、映射中,也可作为参数传递给函数或作为函数返回值进行返回。函数的入参是值传递,传递进来的参数会进行一个拷贝,如果在函数内部需要修改传进来的入参,需要传递指针,用来修改传进来的原始入参值
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
// 任意多个参数
func anyArgs(args ...string) {
fmt.Printf("%T %v \n", args, args) // []string [1 2 3]
}
func sumFunc(n ...int) int {
sum := 0
for _, v := range n {
sum += v
}
return sum
}
// 多个返回值
func calc(a, b int) (int, int, int, int) {
return a + b, a - b, a * b, a / b
}
// 命名返回值,返回类型中定义的参数,将直接再函数作用域中申明,并使用零值初始化,在函数体内部不需要重复定义
// 并可以直接return,无需加上返回参数
func sumFunc2(n ...int) (sum int) {
for _, v := range n {
sum += v
}
return
}
func calc2(a, b int) (jia, jian, cheng, chu int) {
jia = a + b
jian = a - b
cheng = a * b
chu = a / b
return
}
// 闭包,闭包就是在一个函数中返回另一个函数,在返回函数的内部使用到了外层函数的变量,这个变量在函数执行完的时候并不会被销毁,留着别删
func bb() func() string {
name := "123"
return func() string {
return name
}
}
func main() {
sum := add(1, 2)
fmt.Println(sum) // 3
anyArgs("1", "2", "3")
fmt.Println(sumFunc(1, 2, 3, 4, 5)) // 15
r1, r2, r3, r4 := calc(4, 2)
fmt.Printf("%v %v %v %v \n", r1, r2, r3, r4) // 6 2 8 2
fmt.Println(sumFunc2(1, 2, 3, 4)) // 10
rt1, rt2, rt3, rt4 := calc2(6, 2)
fmt.Printf("%v %v %v %v \n", rt1, rt2, rt3, rt4) // 8 4 12 3
}
错误处理
Go 语言通过 error 接口实现错误处理的标准模式,通过使用函数返回值列表中的最后一个值返回错误信息,将错误的处理交由程序员主动进行处理
package main
import (
"errors"
"fmt"
)
// go标准建议显式的将错误信息返回给开发者,由调用者自行处理
// 返回错误一般有两种方法,如fact函数中注释处的用法
func fact(n int64) (int64, error) {
if n < 0 {
return 0, fmt.Errorf("计算错误") // 和下面的方式一样
}
if n == 0 || n == 1 {
return 1, nil
}
r, err := fact(n - 1)
if err == nil {
return n * r, nil
}
return 0, errors.New("计算错误") // 和上面的方式一样
}
// panic 抛出错误,通过panic抛出的错误,程序将直接终止,如果错误是一个可恢复的,我们需要保持程序运行,还是需要对错误进行手动处理
// 此时我们还是需要将其转换成为error的方式,通过defer函数调用和recover恢复函数执行,可以将其转换,是一种常见的方式
func fact1(n int64) int64 {
if n < 0 {
panic("n不能小于0")
}
if n == 0 || n == 1 {
return 1
}
return n * fact1(n-1)
}
func main() {
r, err := fact(5)
fmt.Printf("%v %v\n", r, err) // 120 nil
r1, err1 := fact(-1)
fmt.Printf("%v %v\n", r1, err1) // 0 计算错误
fact1(-1) // 直接调用,此时程序会直接终止
}
panic 与 recover 函数
go 语言提供 panic 和 recover 函数用于处理运行时错误,当调用 panic 抛出错误,中断原有的控制流程,常用于不可修复性错误。recover 函数用于终止错误处理流程,仅在 defer 语句的函数中有效,用于截取错误处理流程,recover 只能捕获到最后一个错误
package main
import (
"fmt"
)
// panic 抛出错误,通过panic抛出的错误,程序将直接终止,如果错误是一个可恢复的,我们需要保持程序运行,还是需要对错误进行手动处理
// 此时我们还是需要将其转换成为error的方式,通过defer函数调用和recover恢复函数执行,可以将其转换,是一种常见的方式
// defer执行时机是其所在函数退出的时候执行,并且是一个堆栈解构,defer中的函数会存起来,当所在函数结束后,再取出来执行,由于是堆栈解构,先进后出,会优先执行代码上下文中靠后的defer调用
func fact(n int64) int64 {
if n < 0 {
panic("n不能小于0")
}
if n == 0 || n == 1 {
return 1
}
return n * fact(n-1)
}
func warpedErrorFact(n int64) (r int64, err error) {
defer func() {
errMsg := recover()
if errMsg != nil {
err = fmt.Errorf("%v", errMsg)
}
}()
r = fact(n)
return
}
func main() {
r, err := warpedErrorFact(-5)
fmt.Println(r, err) // 0 n不能小于0
r1, err1 := warpedErrorFact(5)
fmt.Println(r1, err1) // 120 <nil>
}
defer
defer 关键字用户声明函数,不论函数是否发生错误都在函数执行最后执行(return 之前),若使用 defer 声明多个函数,则按照声明的顺序,先声明后执行(堆)常用来做资源释放,记录日志等工作
package main
import "fmt"
func main() {
defer func() {
fmt.Println("函数延迟执行1")
}()
defer func() {
fmt.Println("函数延迟执行2")
}()
defer func() {
fmt.Println("函数延迟执行3")
}()
fmt.Println("main")
}
main
函数延迟执行3
函数延迟执行2
函数延迟执行1
包
包是函数和数据的集合,将有相关特性的函数和数据放在统一的文件/目录进行管理,每个包都可以作为独立的单元维护并提供给其他项目进行使用
Go 源文件都需要在开头使用 package 声明所在包,包名告知编译器哪些是包的源代码用于编译库文件,其次包名用于限制包内成员对外的可见性,最后包名用于在包外对公开成员的访问
声明
包名使用简短的小写字母,常与所在目录名保持一致,一个包中可以由多个 Go 源文件,但必须使用相同包名
导入&调用
在使用包时需要使用 import 将包按路径名导入,再通过包名调用成员,可通过 import 每行导入一个包,也可使用括号包含所有包并使用一个 import 导入
编译&运行
使用 go build 编译二进制文件
命令:go build gpkgmain
说明:编译路径 gpkgmain 下的包,main 包,则在当前目录产生以 pkgmain 命名的
二进制程序
- 使用 go run 运行二进制文件
命令:go run gpkgmain - 使用 go install 编译并发布二进制文件
命令:go install gpkgmain
说明:编译并发布路径 gpkgmain 下的包,main 包,则在将编译后的以 pkgmain 命
名的二进制程序拷贝到 bin 目录 - 使用 go install 编译发布库文件
命令:go install gpkgname/pkg01
说明:编译并发布路径 gpkgname/pkg01 下的包,非 main 包,则在将编译的以包名
命名的库文件拷贝到 pkg/GOOS_GOARCH 目录下 - 使用 go install 编译发布所有二进制和库文件
命令:go install ./…
导入形式
- 绝对路径导入
在 GOPATH 目录中查找包
示例:
import “fmt”
import “gpkgname/pkg01” - 相对路径导入
在当前文件所在的目录查找
示例:
import “./gpkgname/pkg02” - 点导入
在调用点导入包中的成员时可以直接使用成员名称进行调用(省略包名)
package main
import . "fmt"
func main () {
Println("Hello world")
}
- 别名导入
当导入不同路径的相同包名时,可以别名导入为包重命名,避免冲突
package main
import f "fmt"
func main () {
f.Println("Hello world")
}
- 下划线导入
Go 不允许包导入但未使用,在某些情况下需要初始化包,使用空白符作为别名进行导入,从而使得包中的初始化函数可以执行
成员可见性
Go 语言使用名称首字母大小写来判断对象(常量、变量、函数、类型、结构体、方法等)的访问权限,首字母大写标识包外可见(公开的),否者仅包内可访问(内部的)
main 包与 main 函数
main 包用于声明告知编译器将包编译为二进制可执行文件,在 main 包中的 main 函数是程序的入口,无返回值,无参数
init 函数
init 函数是初始化包使用,无返回值,无参数。建议每个包只定义一个。init 函数在 import包时自动被调用(const->var->init)
包管理
介绍
Go1.11 版本提供 Go modules 机制对包进行管理,同时保留 GOPATH 和 vendor 机制,使用临时环境变量 GO111MODULE 进行控制,GO111MODULE 有三个可选值:
- 当 GO111MODULE 为 off 时,构建项目始终在 GOPATH 和 vendor 目录搜索目标程序依赖包
- 当 GO111MODULE 为 on 时,构建项目则始终使用 Go modules 机制,在 GOPATH/pkg/mod目录搜索目标程序依赖包
- 当 GO111MODULE 为 auto(默认)时,当构建源代码不在 GOPATH/src 的子目录且包含go.mod 文件,则使用 Go modules 机制,否则使用 GOPATH 和 vendor 机制
GOPATH+vendor 机制
vendor
将项目依赖包拷贝到项目下的 vendor 目录,在编译时使用项目下 vendor 目录中的包进行编译
解决问题:
外部包过多,在使用第三方包时需要使用 go get 进行下载
第三方包在 go get 下载后不能保证开发和编译时版本的兼容性包搜索顺序
1.在当前包下的 vendor 目录查找
2.向上级目录查找,直到 GOPATH/src/vendor 目录
3.在 GOPATH 目录查找
4.在 GOROOT 目录查找第三方包
可以借助 go get 工具下载和安装第三方包及其依赖,需要安装与第三方包匹配的代码管理工具,比如 git、svn 等
Go modules 机制
优势:
- 不用设置 GOPATH,代码可任意放置
- 自动下载依赖管理
- 版本控制
- 不允许使用相对导入
- replace 机制
初始化模块
命令:go mod init modname
当前模块下的包
对于当前模块下的包导入时需要使用 modname+packagename
第三方包
在使用 go mod tidy、go build、go test、go list 命令会自动将第三方依赖包写入到go.mod 文件中同时下载第三方依赖包到 GOPATH/pkg/mod/cache 目录,并在当前模块目录生成一个构建状态跟踪文件 go.sum,文件中记录当前 module 所有的顶层和间接依赖,以及这些依赖的校验和
常用命令
- go mod tidy:整理依赖模块(添加新增的,删除未使用的)
- go mod vendor: 将依赖模块拷贝到模块中的 vendor 目录
- go build: 编译当前模块
- go build ./…: 编译当前目录下的所有模块
- go build -mod=vendor:使用当前模块下的 vendor 目录中的包进行编译
- go mod download: 仅下载第三方模块
- go mod grapha: 打印所有第三方模块
- go list -m -json all:显示所有模块信息
- go mod edit: 修改 go.mod 文件
require=pakcage@version
replace=old_package@version=new_package@version
可以使用-replace 功能将包替换为本地包,实现相对导入
结构体
自定义类型
自定义类型类似于别名,可以将go的类型自定义为自己的类型,这个类型的特性和原始go的特性一致,也可用于类型转换
type Counter int
定义结构体
结构体定义使用 struct 标识,需要指定其包含的属性(名和类型),在定义结构体时可以为结构体指定结构体名(命名结构体),用于后续声明结构体变量使用。
type User struct {
id string
name string
age int
birth time.Time
tel string
addr string
}
声明结构体
声明结构体变量只需要定义变量类型为结构体名,变量中的每个属性被初始化为对应类型的零值。也可声明结构体指针变量,此时变量被初始化为 nil
package main
import (
"fmt"
"time"
)
type User struct {
id string
name string
age int
birth time.Time
tel string
addr string
}
func main() {
var u1 User
fmt.Println(u1) // { 0 {0 0 <nil>} }
var u2 *User
fmt.Println(u2) // <nil>
}
结构体赋值
使用结构体创建的变量叫做对应结构体的实例或者对象
- 使用结构体零值初始化结构体值对象
package main
import (
"fmt"
"time"
)
type User struct {
id string
name string
age int
birth time.Time
tel string
addr string
}
func main() {
var u1 User = User{}
fmt.Println(u1) // { 0 {0 0 <nil>} }
}
- 使用结构体字面量初始化结构体值对象
- 使用结构体零值初始化结构体值对象
- 使用结构体字面量初始化结构体值对象
- 使用 new 函数进行初始化结构体指针对象
- 使用结构体字面量初始化结构体指针对象
package main
import (
"fmt"
)
type User struct {
id string
name string
age int
tel string
addr string
}
func main() {
var u1 User = User{"001", "jerry", 18, "13299999999", "湖北"}
var u2 User = User{id: "002"}
var u3 *User = new(User)
u3.id = "003"
u4 := &User{id: "004"}
fmt.Println(u1) // {001 jerry 18 13299999999 湖北}
fmt.Println(u2) // {002 0 }
fmt.Println(u3) // &{003 0 }
fmt.Println(u4) // &{004 0 }
}
属性的访问和修改
通过结构体对象名/结构体指针对象.属性名的方式来访问和修改对象的属性值
package main
import "fmt"
type User struct {
id string
name string
age int
tel string
addr string
}
func main() {
var u1 User = User{id: "001", name: "jerry"}
fmt.Println(u1.id, u1.name) // 001 jerry
u1.id = "1001"
u1.name = "tom"
fmt.Println(u1.id, u1.name) // 1001 tom
var u2 *User = new(User)
fmt.Println(u2)
(*u2).id = "002"
u2.name = "jack"
fmt.Println(u2) // &{002 jack 0 }
u2.id = "003" // 这里不取指针也可以对属性进行赋值
fmt.Println(u2) // &{003 jack 0 }
}
匿名结构体
在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置
package main
import "fmt"
func main() {
var connect = struct {
ip string
port string
user string
password string
}{"127.0.0.1", "8080", "root", "root@password"}
fmt.Println(connect)
}
命名嵌入
结构体命名嵌入是指结构体中的属性对应的类型也是结构体,使用.
链式调用修改或者访问值
package main
import "fmt"
type Address struct {
country string
province string
}
type User struct {
id string
name string
addr Address // 命名嵌入
}
func main() {
var u1 User = User{id: "001", name: "jerry", addr: Address{country: "china", province: "hubei"}}
fmt.Println(u1) // {001 jerry {china hubei}}
var u2Addr = Address{"中国", "湖北"}
var u2 User = User{"002", "tom", u2Addr}
fmt.Println(u2) // {002 tom {中国 湖北}}
u1.addr.province = "广东"
fmt.Println(u1) // {001 jerry {china 广东}}
}
匿名嵌入
结构体匿名嵌入是指将已定义的结构体名直接声明在新的结构体中,从而实现对以后已有类型的扩展和修改
package main
import "fmt"
type Address struct {
country string
province string
}
type User struct {
id string
name string
Address
}
func main() {
var u1 User = User{"001", "jerry", Address{"china", "hubei"}}
fmt.Println(u1) // {001 jerry {china hubei}}
var u2 User = User{"002", "tom", Address{country: "china", province: "hubei"}}
fmt.Println(u2) // {002 tom {china hubei}}
var u3 User = User{id: "003", name: "jack", Address: Address{country: "china", province: "hubei"}}
fmt.Println(u3) // {003 jack {china hubei}}
fmt.Println(u1.country) // china
fmt.Println(u2.province) // hubei
fmt.Println(u3.country) // china
// 上述访问或修改属性的方法同样可以使用链式调用,只是可以省略其中匿名嵌套的结构体名
fmt.Println(u1.Address.country) // china
fmt.Println(u2.Address.province) // hubei
fmt.Println(u3.Address.country) // china
}
下面声明方式是不通过的,通过下面的情况可以得出,匿名结构体嵌套实际上不是扩展,而是使用和结构体相同的名称进行命名,只是省略了名字,类似于下面的方式。注意是类似,因为下面这样声明实际上是命名嵌套了。
type User struct {
id string
name string
Address Address
}
// 不能使用下面方式声明
// var u4 User = User{"003", "jack", "china", "hubei"}
// var u5 User = User{id: "003", name: "jack", country: "china", province: "hubei"}
指针类型嵌入
结构体嵌入(命名&匿名)类型也可以为结构体指针,且由于指针的特性,当指针的值修改了之后,将会影响所有使用此指针的结构体
package main
import "fmt"
type Address struct {
country string
province string
}
type User struct {
id string
name string
addr *Address
}
func main() {
addr := &Address{"china", "hubei"}
var u1 User = User{"001", "jerry", addr}
var u2 User = User{"002", "tom", addr}
addr.country = "中国"
fmt.Printf("%#v\n", u1.addr.country) // "中国"
fmt.Printf("%#v\n", u2.addr.country) // "中国"
}
可见性
结构体首字母大写则包外可见(公开的),否者仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否者仅包内可访问(内部的)
组合:
- 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
- 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
- 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时
由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能在包内使用 - 结构体名首字母小写,属性名小写:结构体只能在包内使用
方法
定义
方法是添加了接收者的函数,接收者必须是自定义的类型
调用
调用方法通过自定义类型的对象.方法名
进行调用,在调用过程中对象传递(赋值)给方法的接收者(值类型,拷贝)
package main
import "fmt"
type Dog struct {
name string
}
func (d Dog) Run() {
fmt.Printf("%s running", d.name)
}
func (d Dog) RenameOne(name string) {
d.name = name
}
func (d *Dog) RenameTwo(name string) {
d.name = name
}
func main() {
var d1 Dog = Dog{"dahuang"}
d1.Run() // dahuang running
// 由于传递给方法的变量时值传递,因此这里rename是不会修改传入的变量的原始值的
d1.RenameOne("xiaohei")
d1.Run() // dahuang running
(&d1).RenameTwo("xiaohei")
d1.Run() // xiaohei running
}
结构体指针对象调用值接收者方法
当使用结构体指针对象调用值接收者的方法时,Go 编译器会自动将指针对象”解引用”为值调用方法
package main
import "fmt"
type Dog struct {
name string
}
func (d Dog) Run() {
fmt.Printf("%s running", d.name)
}
func (d Dog) Rename(name string) {
d.name = name
}
func main() {
var d1 *Dog = &Dog{"dahuang"}
(*d1).Rename("xiaohei") // 自动会解引用成为值类型
(*d1).Run() // dahuang running
}
结构体对象调用指针接收者方法
当使用结构体对象调用指针接收者的方法时,Go 编译器会自动将值对象”取引用”为指针调用方法
package main
import "fmt"
type Dog struct {
name string
}
func (d Dog) Run() {
fmt.Printf("%s running", d.name)
}
func (d *Dog) Rename(name string) {
d.name = name
}
func main() {
var d1 Dog = Dog{"dahuang"}
d1.Rename("xiaohei") // 自动会取引用成为指针类型
d1.Run() // xiaohei running
}
注:取引用和解引用发生在接收者中,对于函数/方法的参数必须保持变量类型一一对
应
一般使用规则
该使用值接收者还是指针接收者,取决于是否现需要修改原始结构体
- 若不需要修改则使用值,若需要修改则使用指针
- 若存在指针接收者,则所有方法使用指针接收者
对于接收者为指针类型的方法,需要注意在运行时若接收者为 nil 用会发生错误
带方法的结构体命名嵌入
带方法的结构体命名嵌入,使用链式调用嵌入结构体上的方法
package main
import "fmt"
type User struct {
name string
addr string
}
func (u *User) SetAddr(addr string) {
u.addr = addr
}
type Employee struct {
user User
salary float64
}
func (e *Employee) SetSalary(salary float64) {
e.salary = salary
}
func main() {
var emp1 Employee = Employee{user: User{name: "jerry", addr: "深圳"}, salary: 3000.00}
fmt.Println(emp1) //
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com