package main
import (
"fmt"
)
go基本知识
GoRoot 是 Go 的安装路径。mac 或 unix 是在 /usr/local/go 路径上
GoPath 的作用在于提供一个可以寻找 .go 源码的路径,它是一个工作空间的概念,可以设置多个目录。Go 官方要求,GoPath 下面需要包含三个文件夹:
srcpkgbin
src 存放源文件,pkg 存放源文件编译后的库文件,后缀为 .a;bin 则存放可执行文件。
基本命令
go build 用来编译指定 packages 里的源码文件以及它们的依赖包,编译的时候会到 $GoPath/src/package 路径下寻找源码文件。go build 还可以直接编译指定的源码文件,并且可以同时指定多个。
go install 用于编译并安装指定的代码包及它们的依赖包。相比 go build,它只是多了一个“安装编译后的结果文件到指定目录”的步骤。go install命令会安装程序,在工作区的bin目录下创建二进制执行文件。
go run 用于编译并运行命令源码文件。
Printf
在 Printf 方法中,使用 %T 格式说明符(Format Specifier)
| 占位符 | 功能 |
|---|---|
%v | 按值的默认格式输出 |
%+v | 包括字段名和值(适用于结构体) |
%#v | 输出 Go 语法表示的值 |
%T | 输出值的类型 |
%% | 输出 % 字符 |
- 宽度控制:
%5d:整数最小宽度为 5(右对齐)。%5s:字符串最小宽度为 5(右对齐)。
- 精度控制:
%.2f:保留两位小数。%6.2f:总宽度为 6,保留两位小数。
- 结合使用:
%5.2f:宽度为 5,保留两位小数。
- 左对齐:
%-5d:整数宽度为 5,左对齐。
fmt.Printf("整数:%d\n", i) // 整数:123
fmt.Printf("二进制:%b\n", i) // 二进制:1111011
fmt.Printf("浮点数:%.2f\n", f) // 浮点数:123.46
fmt.Printf("字符串:%s\n", s) // 字符串:hello
fmt.Printf("带引号字符串:%q\n", s) // 带引号字符串:"hello"
fmt.Printf("指针:%p\n", &i) // 指针:0x...
fmt.Printf("类型:%T\n", f) // 类型:float64
包管理
所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中。
在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。其它包只能访问被导出的函数和变量。
首字母大写表示Public,否则private,对包来说
导入了包,却不在代码中使用它,这在 Go 中是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包,从而导致编译时间显著增加。
package main
import (
"geometry/rectangle"
)
var _ = rectangle.Area // 错误屏蔽器
func main() {
}
var _ = rectangle.Area 这一行屏蔽了错误。我们应该了解这些错误屏蔽器(Error Silencer)的动态,在程序开发结束时就移除它们,包括那些还没有使用过的包。由此建议在 import 语句下面的包级别范围中写上错误屏蔽器。
有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量。例如,我们或许需要确保调用了 rectangle 包的 init 函数,而不需要在代码中使用它。这种情况也可以使用空白标识符,如下所示。
package main
import (
_ "geometry/rectangle"
)
func main() {
}
init 函数
所有包都可以包含一个 init 函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。
包的初始化顺序如下:
- 首先初始化被导入的包。因此,首先初始化了 rectangle 包。
- 接着初始化了包级别的变量 rectLen 和 rectWidth。
- 调用 init 函数。
- 最后调用 main 函数。
变量、常量及初始化
golang是用C实现的,并且被称为下一代的C语言,但是golang跟C的差别还是很大的。它定义了一套很丰富的数据类型及数据结构,这些类型和结构或者是直接映射为C的数据类型,或者是用C struct来实现。
下面是 Go 支持的基本类型:
bool
数字类型
int8, int16, int32, int64, int uint8, uint16, uint32, uint64, uint float32, float64 complex64, complex128
- byte byte 是 uint8 的别名。
- rune rune 是 int32 的别名。
- string
int:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型。
Go 有着非常严格的强类型特征。Go 没有自动类型提升或默认类型转换。只能强制转换
/*
变量
*/
func variable() {
//集体定义
var{
a=3
b=4
}
//1. 指定类型
// 有默认值
var a int
println(a)
//也可以同时赋值
var a1 bool=true
println(a1)
//2. 自动判断类型
var b=3
println(b)
//3. 省略var 就有:了
c:=3
println(c)
}
/*
常量
*/
func constVariable(){
const a int=3;
//多类型简写
const c,d,e=1,false,4
//可以做枚举
const(
h=1
i=2
j=3
//如果不赋值,就是上一行的值
)
//itoa
//iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),
// const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
const (
a1 = iota
b = iota
c1 = iota
)
//a1是0
}
| 从 → 到 | 转换方法 |
|---|---|
int → string | strconv.Itoa(n) |
string → int | strconv.Atoi("123") |
byte → string | string(b) |
string → []byte | []byte(str) |
rune → int | int(ch) |
int → rune | string(intVal) |
float64 → int | int(f) |
字符串和rune
字符串是一个字节切片,所以我们可以获取字符串的每一个字节。
但是 字符串是不可变的。 Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。
len(s) 返回字符串中字节的数量
go底层实现
struct String
{
byte* str;
intgo len;
};
一个汉字3个字节,这是因为在UTF-8编码规则中,一个中文占3个字节
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。让我们修改一下上面的程序,用 rune 来打印字符。
rune对应了一个Unicode 码点,是定长的。UTF8是变长的。
rune是固定的4个字节,utf是变长的,但都在4个字节的范围内。因此某字符串的字节长度是小于等于rune数组表示的字节长度的
func printChars(s string) {
runes := []rune(s)
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}
var r rune = '你'
fmt.Printf("rune 值: %U\n", r) // Unicode 码点
fmt.Printf("rune 占用字节数: %d\n", unsafe.Sizeof(r))
str := "你"
fmt.Printf("UTF-8 编码占用字节数: %d\n", len(str))
输出是
rune 值: U+4F60
rune 占用字节数: 4
UTF-8 编码占用字节数: 3
用字节切片构造字符串
byteSlice := []byte{67, 97, 102, 195, 169}//decimal equivalent of {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
str := string(byteSlice)
fmt.Println(str)
用 rune 切片构造字符串
func main() {
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
str := string(runeSlice)
fmt.Println(str)
}
转换
rune ↔ string
var ch rune = '你'
s := string(ch) // rune 转 string
r := []rune(s)[0] // string 转 rune(取第一个字符)
rune ↔ int32
var r rune = '你'
fmt.Println(int32(r)) // 打印 Unicode 编码:20320
fmt.Println(rune(20320)) // 反转为字符:'你'
s ="234" s[0]是 byte 但可以直接 rune(x)进行转换,也可以直接 int(x)转换
字符串的长度
utf8 package 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。
条件语句、循环语句
/*
运算符
*/
func operator() {
/*
运算符没有三目运算符
有赋值运算法及&& || !
有指针操作, & 及 *
*/
}
/*
条件语句
*/
func ifStatement() {
//if
if 3<4{
println("3<4")
}
if(3<4){
println("3<4")
}
if 3<4{
println("3<4")
}else if(4==4){
println("4==4")
}else{
println("Jdd")
}
//switch
/*
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,
从上直下逐一测试,直到匹配为止。。
switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加break
*/
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
//
//switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
//
//Type Switch 语法格式如下:
//switch x.(type){
// case type:
// statement(s);
// case type:
// statement(s);
// /* 你可以定义任意个数的case */
// default: /* 可选 */
// statement(s);
//}
//select
/*
select是Go中的一个控制结构,
类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。
select随机执行一个可运行的case。如果没有case可运行,它将阻塞,
直到有case可运行。一个默认的子句应该总是可运行的。
*/
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}//out: no communication
}
/*
循环语句
*/
func forStatement() {
/*
Go语言的For循环有3中形式,只有其中的一种使用分号。
和 C 语言的 for 一样:
for init; condition; post { }
和 C 的 while 一样:
for condition { }
和 C 的 for(;;) 一样:
for { }
*/
//for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
s := "abc"
// 忽略 2nd value,支持 string/array/slice/map。
for i := range s {
println(s[i])
}
// 忽略 index。
for _, c := range s {
println(c)
}
// 忽略全部返回值,仅迭代。
for range s {
}
m := map[string]int{"a": 1, "b": 2}
// 返回 (key, value)。
for k, v := range m {
println(k, v)
}
//for循环里的变量属于局部变量,与外界不冲突。。。就是for条件的那个位置
/*
和 C 的 while 一样:
for condition { }
*/
}
- range一个变量时如果不是引用 会拷贝整个变量
函数
/*
函数
*/
func function() {
/*
func function_name( [parameter list] ) [return_types] {
函数体
}
*/
/*
func swap(x, y string) (string, string) {
return y, x
}
*/
/*
函数可作为变量
*/
//func f7(){
// /* 声明函数变量 */
// getSquareRoot := func(x float64) float64 {
// return math.Sqrt(x)
// }
//
// /* 使用函数 */
// fmt.Println(getSquareRoot(9))
//}
/*
闭包
*/
//func getSequence() func() int { // return值是func() int
// i:=0
// return func() int {
// i+=1
// return i
// }
//}
///* nextNumber 为一个函数,函数 i 为 0 */
//nextNumber := getSequence()
//
///* 调用 nextNumber 函数,i 变量自增 1 并返回 */
//fmt.Println(nextNumber())
//fmt.Println(nextNumber())
//fmt.Println(nextNumber())
/*
方法
*/
///* 定义结构体 */
//type Circle struct {
// radius float64
//}
////该 method 属于 Circle 类型对象中的方法
//func (c Circle) getArea() float64 {
// //c.radius 即为 Circle 类型对象中的属性
// return 3.14 * c.radius * c.radius
//}
}
数组和指针
Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。
通过将数组作为参数传递给 len 函数,可以得到数组的长度。
/*
数组
*/
func array() {
/*
var balance [10] float32
var balance1 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
//如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:
var balance2 = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}//直接[]也行
//多维数组
var threedim [5][10][4]int
var a = [3][4]int{
{0, 1, 2, 3} ,
{4, 5, 6, 7} ,
{8, 9, 10, 11},
}
*/
/* 未定义长度的数组只能传给不限制数组长度的函数 */
/* 定义了长度的数组只能传给限制了相同数组长度的函数 */
}
指针的零值是 nil。
不要向函数传递数组的指针,而应该使用切片 因为数组是数组拷贝。
假如我们想要在函数内修改一个数组,并希望调用函数的地方也能得到修改后的数组,一种解决方案是把一个指向数组的指针传递给这个函数。
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}
指针
Go 并不支持其他语言(例如 C)中的指针运算。
Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (*emp8).firstName
/*
指针
*/
func pointer() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
println(*ip)
/*
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
*/
//指针数组
//var ptr [MAX]*int;
//指向指针的指针
//var pptr **int
//指针作为函数参数
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
//swap(&a, &b);
//func swap(x *int, y *int) {}
}
结构体
结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
如果结构体包含不可比较的字段,则结构体变量也不可比较。比如map
匿名结构体
emp3 := struct {
firstName, lastName string
age, salary int
}{
firstName: "Andreah",
lastName: "Nikola",
age: 31,
salary: 5000,
}
匿名字段
当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。
type Person struct {
string
int
}
func main() {
p := Person{"Naveen", 50}
fmt.Println(p)
}
提升字段(Promoted Fields)
如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.Address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city) //city is promoted field
fmt.Println("State:", p.state) //state is promoted field
}
/*
结构体
*/
func structStatement() {
/*
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
*/
//variable_name := structure_variable_type {value1, value2...valuen}
//或
//variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
//// 创建一个新的结构体
//fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
//
//// 也可以使用 key => value 格式
//fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
//
//// 忽略的字段为 0 或 空
//fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
//结构体作为函数参数
//func printBook( book Books )
//结构体指针
var struct_pointer *Books
//使用结构体指针访问结构体成员,使用 "." 操作符:
struct_pointer.title;
}
方法
在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值。
- 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
一般来说,指针接收器可以使用在:对方法内部的接收器所做的改变应该对调用者可见时。
指针接收器也可以被使用在如下场景:当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
在其他的所有情况,值接收器都可以被使用。
注意给值接收器传递指针,那也是更改不可见的,只有指针接收器才可以更改可见。能不能更改可见以接收器类型为准。
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() 方法将 Employee 做为接收器类型
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() // 调用 Employee 类型的 displaySalary() 方法
}
可变参数
如果函数最后一个参数被记作 …T ,这时函数可以接受任意个 T 类型参数作为最后一个参数。
请注意只有函数的最后一个参数才允许是可变的。
func append(slice []Type, elems ...Type) []Type
传入后,当成一个数组,range访问就好了
切片slice
slice类型的底层同样是一个C struct。
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
append 函数的定义是 func append(s[]T,x ... T)[]T。
我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式,
是引用传递,要同步内容修改的
当新的元素被添加到切片时,如果容量不够,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
/*
切片
*/
func slice() {
//使用结构体指针访问结构体成员,使用 "." 操作符:
//struct_pointer.title;
//var slice1 []type = make([]type, len)
//
//也可以简写为
//
//slice1 := make([]type, len)
//make([]T, length, capacity)
//s := arr[startIndex:endIndex]
// var numbers = make([]int,3,5)
//一个切片在未初始化之前默认为 nil,长度为 0,
//var numbers []int
numbers := []int{0,1,2,3,4,5,6,7,8}
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
//append() 和 copy() 函数
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
make函数参数分别是len和cap, 当只有一个时,是len,cap=len。
数组引用切片后,对切片读写是操作底层数组, 如果超过cap就会重新分配,地址不一样了。直接创建切片会自动分配底层数组。
go切片不像python那样有步长和负索引,但支持切片容量定制。a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x
}
/*
遍历 range
*/
func rangeStatement() {
/*
range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key及value 值。
*/
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
// only value
for _, num := range nums {
sum += num
}
// index, value
nums := []string{"a", "b", "c"}
for k,v := range nums{
println(k,v) // index, value
}
// only index
for k := range nums{
println(k) // only index
}
}
指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。
map
map 的零值是 nil。如果你想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make 函数初始化。
也就是说必须使用make
获取 map 的长度使用 len 函数。
和 slices 类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
map 之间不能使用 == 操作符判断,== 只能用来检查 map 是否为 nil。
/*
map
*/
func mapStatement() {
/* 声明变量,默认 map 是 nil */
//var map_variable map[key_data_type]value_data_type
//nil map 不能用来存放键值对,会panic
/* 使用 make 函数 */
//map_variable := make(map[key_data_type]value_data_type)
/* 创建map */
//countryCapitalMap := map[string]string{"France": "Paris",
// "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "Paris"
countryCapitalMap [ "Italy" ] = "罗马"
/*查看元素在集合中是否存在 */
captial, ok := countryCapitalMap [ "美国" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(captial) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("美国的首都是", captial)
} else {
fmt.Println("美国的首都不存在")
}
如果key不在map里,取出来的是默认值,比如string类型取出来的是"".
/*删除元素*/
delete(countryCapitalMap, "France")
}
错误处理
/*
错误处理
*/
func errorHandle() {
/*
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
*/
/*
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
*/
}
defer
defer特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
defer 是先进后出
defer的陷阱
陷阱: **声明defer时,defer函数的实参就会赋值,注意赋值的是不是引用,如果是引用, 引用是可以在变化的,但引用重新赋值也是不会传递到defer执行时的。 **
package main
import (
"errors"
"fmt"
)
func foo(a, b int) (i int, err error) {
defer fmt.Printf("first defer err %v\n", err)
defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
defer func() { fmt.Printf("third defer err %v\n", err) }()
if b == 0 {
err = errors.New("divided by zero!")
return
}
i = a / b
return
}
func main() {
foo(2, 0)
}
输出结果:
third defer err divided by zero! 是变量的引用。
second defer err <nil> 这里err是声明时就赋值了,所以是nil
first defer err <nil> 直接调用函数的话是声明时参数就进行求值。所以为nil。
更多,请参考
defer的非recover语句一定会执行,无论是否发生panic。defer中是否包含recover不会影响其他defer的执行,只会影响panic的传播。- 使用
defer的资源清理代码(如关闭文件、释放锁)在panic时仍然是安全的,因为它们会被执行。
defer参数求值时机
defer声明时求值。 在声明
defer时,函数参数会立即求值。这里的someVar会在defer声明时求值,而不是在实际执行时。defer fmt.Println("直接调用:", someVar) 或 defer func(p P) {}(x) 这个时候x已经赋值给p了 或 data := []*field{ {"one"}, {"two"}, {"three"} } for _, v := range data { // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同 go v.print() // 注意这里。 等于函数直接调用 }
defer执行时求值
在 defer 包裹匿名函数时,参数的求值会延迟到真正执行 defer 时。
defer func() {
fmt.Println("匿名函数:", someVar)
}()
这里的 someVar 的值会在 defer 执行时才计算。
另外注意: defer语句会延迟到函数返回时执行。因此,在循环中的defer调用不会在每轮结束时执行
return变量
return代码出现时,就会return的变量进行赋值,它早于defer。
- 如果return的值是常量,那就是值复制到了单独的一个returnValue。defer里对要return的变量进行赋值或者修改,不影响returnValue。
func c() int {
var i int
defer func() {
i++
fmt.Println("defer1:", i)
}()
return i
}
func main() {
fmt.Println("return:", (c()))
}
return非引用,修改不生效
defer1: 1
return: 0
- 如果是引用,return的变量和returnValue可同步变化。赋值和修改皆同步生效 。
func c() *int {
var i int
defer func() {
i++
i = 10
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return &i
}
func main() {
fmt.Println("return:", (*c()))
}
返回引用,defer修改return变量生效。
defer1: 1
defer2: 10
return: 10
- 如果声明了返回的变量,则defer里赋值是生效的,不论是不是引用。比如
func abc() (i int) {
defer func() {
i = 3
}()
return 5
}
以上是返回3
继承 方法集
Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。
匿名字段情况下的方法集不受下面影响,编译器总是查找全部方法,并自动转换 receiver 实参。但接口有影响
• 类型 T 方法集包含全部 receiver T 方法。
• 类型 *T 方法集包含全部 receiver T + *T 方法。
• 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
• 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
• 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。
用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
主旨是Go有一个语法糖: 对指针类型变量可自动求值,因此指针的方法集更大
任何类型的方法集中只要拥有该接口’对应的全部方法签名。就表示它 “实现” 了该接口,无须在该类型上显式声明实现了哪个接口。
接口interface
请牢记接口(interface)是一种类型。接口是一个或多个方法签名的集合。
- interface{}是值传递!。 比如error Error做参数,就是值传递。struct默认也是值传递。
- interface{}如果赋值的是指针类型(如
\*int,\*struct),interface{}存储的是指针,可以通过该指针修改原始数据。 - 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
接口的零值是 nil。对于值为 nil 的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil。
- 接口同样支持匿名字段方法。
- 接口也可实现类似OOP中的多态。
- 空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以存储任意类型的变量。
- 一个类型可实现多个接口。
- 接口命名习惯以 er 结尾。
type writer interface{
Write([]byte) error
}
类型断言
类型断言用于提取接口的底层值(Underlying Value)。
在语法 i.(T) 中,接口 i 的具体类型是 T,该语法用于获得接口的底层值。
func assert(i interface{}) {
s := i.(int) //get the underlying int value from i
fmt.Println(s)
}
接口支持嵌套和匿名方法
type Walker interface {
Walk()
}
type Speaker interface {
Speak()
}
type Human interface {
Walker
Speaker
}
多态
是否能够将一个类型赋值给接口变量,取决于该类型是否实现了接口的所有方法。这里有两个关键点:
- 如果方法使用 值接收者(
func (t T) Method()):- 该方法既可以通过值类型变量调用,也可以通过指针类型变量调用。
- 这样,无论
T还是*T,都可以赋值给接口变量。
- 如果方法使用 指针接收者(
func (t *T) Method()):- 该方法只能通过指针类型变量调用,不能通过值类型调用。
- 值类型
T不满足接口约定,因此T不能赋值给接口变量。
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
}
// 值接收者
func (p Person) Describe() {
fmt.Println("Person:", p.name)
}
type Address struct {
city string
}
// 指针接收者
func (a *Address) Describe() {
fmt.Println("Address:", a.city)
}
func main() {
var d1 Describer = Person{"Alice"} // ✅ 正确:值接收者允许 `T{}` 赋值
var d2 Describer = &Person{"Bob"} // ✅ 正确:值接收者允许 `&T{}` 赋值
d1.Describe()
d2.Describe()
var d3 Describer = &Address{"New York"} // ✅ 正确:指针接收者要求 `&T{}` 赋值
d3.Describe()
var d4 Describer = Address{"Paris"} // ❌ 编译错误:指针接收者 `*T` 不能用 `T{}` 赋值给接口
d4.Describe()
}