go基础语法

2018/11/01 Go 共 16907 字,约 49 分钟
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 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。

包的初始化顺序如下:

  1. 首先初始化被导入的包。因此,首先初始化了 rectangle 包。
  2. 接着初始化了包级别的变量 rectLen 和 rectWidth。
  3. 调用 init 函数。
  4. 最后调用 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 → stringstrconv.Itoa(n)
string → intstrconv.Atoi("123")
byte → stringstring(b)
string → []byte[]byte(str)
rune → intint(ch)
int → runestring(intVal)
float64 → intint(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
}

多态

是否能够将一个类型赋值给接口变量,取决于该类型是否实现了接口的所有方法。这里有两个关键点:

  1. 如果方法使用 值接收者(func (t T) Method()):
    • 该方法既可以通过值类型变量调用,也可以通过指针类型变量调用。
    • 这样,无论 T 还是 *T,都可以赋值给接口变量。
  2. 如果方法使用 指针接收者(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()
}

Search

    Table of Contents