目录
- 1. 快速入门
- 2. 类型
- 3. 表达式
- 4. 数据
- 5. 函数 方法 接口
- 6. Go特性
学习资料
书籍:
1. 快速入门
1.1. 特性
1.2. 常用写法
参考:
2. 类型
-
概述
- 按数据类型划分:
- 基本数据类型: int, float, string, bool
- 复合数据类型: array, slice, map, struct, interface …
- 按数据存储特点:
- 值类型:
int, float64, bool, string, array … - 引用类型: 操作数据的地址
slice, map, chan
- 值类型:
- 按数据类型划分:
-
派生类型
(a) 指针类型(Pointer)
(b) 数组类型
© 结构化类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型
2.1. 初始化顺序
- 初始化顺序:
- 当一个go源程序被初始化时,首先去初始化 所依赖的其他包,
- 然后初始化该go源码文件的全局变量的初始化 和 执行初始化函数,
- 其中该包所有的全局变量初始化在前,该包的初始化函数init在后。
- 当所有包的初始化函数执行完毕之后, 才执行main函数。
- 一个go源码文件中可以有多个初始化函数。但是不保证同一个代码中初始化函数的执行顺序。
package main import "fmt" var a = hello() func hello() int { fmt.Println("hello") return 0 } func init() { fmt.Println("world") } func main() { fmt.Println("Hello World") fmt.Println(a) //return 0 } //结果: hello world Hello World 0
参考:
2.2. 变量
2.2.1. 变量定义
-
定义
//1. 数组定义 var variable_name [SIZE] variable_type balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} var balance [10]float32 b := [...]int{0: 1, 1: 2, 2: 3} // 不定长数组; //2. 多维数组 a := [...][2]int{ {0: 1}, {1: 2}, }
-
匿名变量
- 匿名变量 _ 本身不会进行空间分配,也不会占用一个变量的名字;
2.2.2. 变量声明
-
当一个变量被声明之后,系统自动赋予它该类型的零值;
-
所有的内存在 Go 中都是经过初始化的。
- 与C不同, C 在变量声明时, 并不会对对应的内存区域进行清理工作;
-
多个 短变量 声明和赋值中, 至少有一个新声明的变量出现在左值中;
- 即便其他变量可能是重复声明的, 编译器也不会报错;
conn, err := net.Dial("tcp","127.0.0.1:8080") conn2, err := net.Dial("tcp","127.0.0.1:8080")
-
make() 与 new() 属于显式声明并初始化;
// 全局变量声明 --该写法一般用于声明全局变量 var ( name1 type name2 type )
-
类型与默认零值
类型 零值 int float 0 bool false string “” 空字符串 struct 内部字段的零值 slice nil map nil 指针 nil 函数 nil chan nil interface nil
2.2.3. 初始化
-
初始化 赋值
赋值操作符 = 和 := 的区别: = 不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量, 如果没有搜索到相同的变量名,则报编译错误。 := 必须出现在函数或者类型方法内部。 := 至少要创建一个局部变量并初始化。 // 示例: var g int, e int = 6, 7 // 多赋值语句中每个变量后面不能都带上类型 错误 // syntax error: unexpected comma at end of statement var a, b int = 1, 2 var c, d = 3, 4 // 不带类型时,编译器自动推断
-
comma,ok(
, ok
)表达式== 常见的 , ok 表达式 == 1. 获取map键值 2. chan 3. 类型断言 v, ok = m[key] // map lookup v, ok = x.(T) // type assertion v, ok = <-ch // channel receive v = m[key] // map查找,失败时返回零值 v = x.(T) // type断言,失败时panic异常 v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败) _, ok = m[key] // map返回2个值 _, ok = mm[""], false // map返回1个值 _ = mm[""] // map返回1个值
参考:
2.2.4. 作用域
-
全局作用域: 在任意命名空间可见
- Go 语言内置的预声明标识符(包括预声明的类型名、关键字、内置函数等)
- 包内以大写字母开头的标识符( 包括变量、常量、函数和方法名、自定义类型、结构字段等)
-
-
包内作用域
- 本包可见, 其他包不可见;
- 包内定义的以小写字母开头的标识符(变量、常量、函数和方法名、自定义类型、结构字段等〉
-
隐式作用域
- 局部变量; 当前代码块可见;
- 不要将作用域和生命周期混为一谈:
- 作用域 是编译的属性;
- 生命周期是运行时变量的有效时间, 是一个运行时的概念;
- 注意 闭包 ;
2.3. 常量
2.3.1. 定义及初始化
-
常量值 必须是编译期可确定的数字、字符串、布尔值。
-
代码示例
// 类型推断 const s = "Hello World" const ( //常量组 defaultMySQLConfigSection = "client" defaultConfigFile = "~/.my.cnf" defaultBulkSize = 1000 )
2.3.2. iota
-
概述:
- 每当
const
出现时, 都会使iota
初始化为0 const
中每新增一行常量声明将使iota
计数一次.- iota 是一个从 0 开始递增的整形值;
- 可以使用在 const块的任意位置; 其 取值 取决于iota所在的位置
- 每当
-
示例代码
const a0 = iota // a0 = 0 // const出现, iota初始化为0 const ( i1, j1,k1 = iota, iota, iota // 0 0 0 // 位于同一行, 只计算一次 ) // 递增特性 const ( a1 = iota // a1 = 0 // 又一个const出现, iota初始化为0 a2 = iota // a1 = 1 // const新增一行, iota 加1 a3 = 6 // a3 = 6 // 自定义一个常量 a4 // a4 = 6 // 不赋值就和上一行相同 a5 = iota // a5 = 4 // const已经新增了4行, 所以这里是4 // 0 1 2 3 4 ) // 取值 const ( a = 4 // 显式的指定值 b = 5 // 显式的指定值 c = iota // c = 2,因为这里的 iota 位于第3个ConstSpec,2=3-1 d // d = 3,因为iota递增了1,等价于 d = iota )
参考:
2.4. 字符串
参考:
2.4.1. 字符串string
-
概述:
- Go 语言中字符串的内部实现使用 UTF-8 编码,
- 通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。
- 当然, Go 语言也支持按照传统的 ASCII 码方式逐字符进行访问
-
常用语法:
// 底层实现 // runtime/string.go type stringStruct struct { str unsafe . Pointer //指向底层字节数组的指针 len int //字节数组长度 } // 下标访问, 但不能修改; var a string = "hello,world" b := a[0] a[1] = "a" // error // 字符串判空: 2种方法 var str string //string 类型变量在定义后默认的初始值是空,不是 nil。 if str == "" { // str 为空 } if len(str) == 0 { // str 为空 } ==注意:= 1. len() 返回字符串的 字节数目 (不是rune字符数目); 2. 字符串末尾不包含 NULL 字符,与 C/C++ 不一样; 3. 获取字符串中某个字节的地址属于非法行为,例如 &str[i] ; 4. 跨行的字符串使用 ``; 而不是""
-
字符串转换
- 一个值在从 string 类型向
[]byte
类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节; - 一个值在从 string 类型向
[]rune
类型转换时代表着字符串会被拆分成一个个 Unicode 字符。
- 一个值在从 string 类型向
2.4.2. 字符rune类型
-
字符编码:
Go 默认的字符编码就是 UTF-8 类型。Go 语言的字符有以下两种:- uint8 类型,或者叫 byte 型( byte 是 unit8 的别名),
- 代表了 ASCII 码的一个字符,占用 1 个字节。
- rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。
- rune 类型等价于 int32 类型,占用 4 个字节。
- uint8 类型,或者叫 byte 型( byte 是 unit8 的别名),
-
rune字符 概述
- rune 类型变量默认初始值是 0;
2.4.3. byte字节类型
-
概述:
- byte 类型变量默认初始值是 0;
- byte 类型是 uint8 类型的一个别名;
-
示例
func main() { s := "hello 世界" runeSlice := []rune(s) // len = 8 byteSlice := []byte(s) // len = 12 // 打印每个rune切片元素 for i:= 0; i < len(runeSlice); i++ { fmt.Println(runeSlice[i]) // 输出104 101 108 108 111 32 19990 30028 } fmt.Println() // 打印每个byte切片元素 for i:= 0; i < len(byteSlice); i++ { fmt.Println(byteSlice[i]) // 输出104 101 108 108 111 32 228 184 150 231 149 140 } }
补充:
字符串转换为 byte数组, 会发生内存拷贝吗?
1. 会, 严格来说, 只要发生类型转换都会发生内存拷贝;
2. 如何节省内存操作, 以提升性能?
go // stringHeader 的地址 强转成 SliceHeader ==> 底层转换二者; a := "aaa" ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) b := *(*[]byte)(unsafe.Pointer(&ssh))
-
统计字符串长度的方法:
当含有中文时:1. 使用 bytes.Count() 统计 2. 使用 strings.Count() 统计 3. 将字符串转换为 []rune 后调用 len 函数进行统计 4. 使用 utf8.RuneCountInString()统计
2.5. 指针类型
2.5.1. 概述
-
使用建议:
- 不要对 map、slice、channel 这类引用类型使用指针;
- 大的结构体的参数传递, 考虑使用指针;
- int bool 小数据没有必要使用指针;
- 并发安全的前提下使用;
- 最好不用嵌套使用;
- 什么时候使用:
- 需要改变参数的值;
- 避免复制操作;
- 节省内存;
-
注意:
- Go 不支持 -> 操作符;
- Go 不支持指针运算;
2.5.2. 语法
- 基本语法:
// 1. 声明: var ip *int /* 指向整型*/ var fp *float32 /* 指向浮点型 */ -- 变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。 // 2. 空指针 var intP *int *intP =10 //====空指针nil 不能赋值;==== // 3. 常用方法 var intP *int = new(int) //更推荐简短声明法,这里是为了演示 intP:=new(int) // 4. 判断: if(ptr != nil) // ptr 不是空指针 5. 指针数组 var ptr [3]*int; // 声明了整型 指针数组 //数组指针 var pf *[6]int = &arr //指针数组 pfArr := [...]*int{&x,&y}
2.5.3. 转换
2.6. 自定义类型
2.6.1. 自定义类型
- 自定义类型与类型别名
//类型定义 type NewInt int //类型别名 : 与原类型等效; type MyInt = int
- 类型 与 新类型 区别:
- 其中 newType 是一种新的类型, newType 本身依然具备 Type 类型的特性。
- 类型声明语句一般出现在包一级,因此 如果新创建的类型名字的首字符大写,则在包外部也可以使用。
- 一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。
若新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
// 类型定义 type newType Type
参考:
2.6.2. type用法
- type 有如下几种用法:
定义结构体
定义接口
类型定义
类型别名
类型查询
2.7. 嵌入类型
2.7.1. 概述
- Go语言允许用户扩展或者修改已有类型的行为。–通过嵌入类型(type embdding)
- 嵌入类型是将已有的类型直接声明在新的结构类型里。
2.7.2. 示例
-
代码示例:
// 实现 当内部类型和外部类型要 // 实现同一个接口 ==== 实现同一个接口 package main import ( "fmt" ) // notifier是一个定义了 // 通知类行为的接口 == 接口定义的是行为; type notifier interface { notify() } // 定义user用户类型 type user struct { name string email string } // 通过user类型值的指针 调用方法 func (u *user) notify() { fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email) } // admin代表一个拥有权限的管理员用户 type admin struct { user level string } // 通过admin类型值指针调用的方法 func (a *admin) notify() { fmt.Printf("Sending admin email to %s<%s>\n", a.name, a.email) } // main() func main() { ad := admin{ user: user{ name: "john smith", email: "john@yahoo.com", }, level: "super", } // 给admin用户发送一个通知 // 接口的嵌入的内部类型实现并没有提升到外部类型 sendNotification(&ad) //直接访问内部类型的方法 ad.user.notify() // 内部类型的方法没有提升 ad.notify() } // 接受一个实现了notifier接口的值 // 并发送通知 func sendNotification(n notifier) { n.notify() } //输出 // 该程序可以看出 admin类型如何实现 notifier接口, 以及 // 如何由 sendNotification() 以及直接使用外部类型变量ad // 来执行 admin类型实现的方法; /* Sending admin email to john smith<john@yahoo.com> Sending user email to john smith<john@yahoo.com> Sending admin email to john smith<john@yahoo.com> */ == 1. 多个同一类型嵌入时, 注意使用别名, 以区分调用;
总结:
-
若 外部类型 实现了notify方法, 内部类型的实现不会被提升;
-
但是 内部类型的值一直存在, 还可以通过直接访问内部类型的值,
来调用没有被提升的内部类型实现的方法; -
嵌入类型接口问题:
- 嵌入类型 接口: 内部类型实现的接口 会自动提升的外部类型;
即: 内部类型实现了某个接口, 外部类型同样就实现了该接口;
- 嵌入类型 接口: 内部类型实现的接口 会自动提升的外部类型;
2.7.3. 区分
2.8. 类型转换
2.8.1. 数字类型
- 尽管在某些特定的运行环境下 int 、 uint 和 uintptr 的大小可能相等,但是它们依然是不同的类型,比如 int 和 int32 ,虽然 int 类型的大小也可能是 32 bit,但是在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换,反之亦然。
- 整数类型转换
-
对于整数类型值、整数常量之间的类型转换
- 原则上只要源值在目标类型的可表示范围内就是合法的。
- 当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。
-
内置的
len()
函数返回一个有符号的 int , 可以实现 for的逆序循环。
-
2.8.2. 等价类型
- Unicode 字符 rune 类型是和 int32 等价的类型,通常用于表示一个 Unicode 码点。
- byte uint8: byte 一般强调数字是一个原始数据, 而不是一个小整数;
2.8.3. 复数
- 内置的 complex 函数用于构建复数,内建的 real 和 imag 函数分别返回复数的实部和虚部:
y := 3 + 4i
参考:
3. 表达式
3.1. 关键字/保留字
- 概述
var const :变量和常量的声明 var varName type 或者 varName : = value package and import: 导入 func: 用于定义函数和方法 return :用于从函数返回 defer someCode :在函数退出之前执行 go 用于并行 select 用于选择不同类型的通讯 interface 用于定义接口 struct 用于定义抽象数据类型 break、case、continue、for、fallthrough、else、if、switch、goto、default 流程控制 chan 用于channel通讯 type 用于声明自定义类型 map 用于声明map类型数据 range 用于读取slice、map、channel数据
3.2. 运算符
3.2.1. 概述
- 全部运算符 分隔符
+ & += &= && == != ( ) - | -= |= || < <= [ ] * ^ *= ^= <- > >= { } / << /= <<= ++ = := , ; % >> %= >>= -- ! ... . : &^ &^=
3.2.2. 运算符 实战
-
&^ 清楚标志位
-
“++”、"–" 是语句 而非表达式。
1. &^ x = 1011 &^ y = 1001 为0, 则使用 x 的值; ———————— z = 0010 2. ++ -- 自增自减 ++i 错 a=i++ 错
3.3. 控制流
-
语法
1. 不需要使用括号; var ten int = 11 if ten > 10 { fmt.Println(">10") } else { fmt.Println("<=10") }
-
lable标签:
- break 和标签一起使用,用于跳出标签所标识的 for 、 switch 、 select 语句的执行,可用于跳出多重循环,但标签和 break 必须在同一个函数内。
- continue 和标签一起使用,用于跳出标签所标识的 for 语句的本次选代,但标签和 continue 必须在同一个函数内。
3.3.1. range
-
概述
- 类似 迭代器操作,返回 (索引, 值) 或 (键, 值)。
- _ 可用于忽略 返回值;
- break 可用于 for、switch、select,而 continue 仅能用于 for 循环。
-
使用:
- Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或 集合(map)的元素。
- 在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
// 遍历字符串 for _, v := range s { if v > 127 { return false } if strings.Count(s, string(v)) > 1{ return false } }
-
迭代实质
- range 是使用一个副本重复赋值的方式来遍历每一个目标元素的,
- 可以将其视为一个目标元素类型的变量,每一次遍历迭代就会把目标元素拷贝到range准备的副本,并作返回。
3.3.2. switch
- 语法:
== 变量类型 == // 在switch中使用 变量名.(type) 查询变量是由哪个类型数据赋值。 // 每个case中含有一种情况 var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int }
4. 数据
4.1. array数组
4.1.1. 概述
-
注意
• 数组是值类型,赋值和传参会复制整个数组,⽽不是指针。
• 数组⻓度必须是常量,且是类型的组成部分。[2]int 和 [3]int 是不同类型。
数组的长度是数组类型的一个组成部分
• ⽀持 “==”、"!=" 操作符,因为内存总是被初始化过的。
• 指针数组 [n]*T,数组指针 *[n]T -
语法
a := [3]int{1, 2} // 未初始化元素值为 0。 b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组⻓度。 c := [5]int{2: 100, 4: 200} // 使⽤索引号初始化元素。 d := [...]struct { name string age uint8 }{ {"user1", 10}, // 可省略元素类型。 {"user2", 20}, // 别忘了最后⼀⾏的逗号。 } // 数组长度 len(a)
4.2. 切片类型 slice
4.2.1. slice
-
概述
- Go 语言切片是对数组的抽象。
- Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
-
特点
- 是一种引用;
-
语法
// 结构: type slice struct { array unsafe.Pointer len int cap int } ==创建切片:== // 1. make方法 make([]type, length, capacity) //capacity是可选参数 ==注意:== 1. length: 分配元素的个数; capacity: 预分配的元素数量; 只是提前分配空间, 不影响 size; 2. length 是数组的长度, 也是切片的长度; 3. 使用make时, 若不指定 capacity, 则 capacity=len b := make([]int, 2) b := make([]int, 2, 10) // 2. 常用方法: 从下标 startIndex 到 endIndex-1 下的元素 创建一个新的切片 s := arr[startIndex:endIndex] var a = [3]int{1, 2, 3} //数组 fmt.Println(a, a[1:2]) // [1 2 3] [2] //== 1. 索引从 0 开始; 元素 自 startIndex 到 endIndex-1
- 创建切片:
==初始化:== data := [...]int{0, 1, 2, 3, 4, 5, 6} slice := data[1:4:5] // [low : high : max] 见下图: // 计算方法: len = high - low cap = max - low // 切片初始化 s1 := s[startIndex:endIndex] ==注意:== 1. 在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片; // 创建有3个元素的整型数组 array := [3]int{10, 20, 30} // 创建长度和容量都是3的整型切片 len=cap=3 slice := []int{10, 20, 30} 2. 使用 make() 函数生成的切片一定发生了内存分配; 但 slice := data[1:4:5] 的方法 只是创建新的切片结构, 不会进行内存分配;
- 创建切片:
-
函数
// 相关函数 builtin.go len() cap()
-
总结
- 从 数组 或 切片 生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
- 两者同时缺省时,与切片本身等效
- 从 数组 或 切片 生成新的切片拥有如下特性:
-
实战:
func main() { var s1 = []int{1, 2, 3, 4, 5, 6, 7} var s2 = s1[2:5] fmt.Println(s1) fmt.Println(s2) fmt.Println(cap(s1), cap(s2)) /* unsafe.Pointer(&s1) 取到s1的首地址的指针, 其实也就是s1 array位置的指针 unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s1))) 将s1 array存放的值取出来, 也就是底层数组的首元素的地址 unsafe.Pointer(uintptr(s1SliceArrayPointer) + unsafe.Sizeof(&s1[0])*2) 向后偏移2位, 也就是取到3的位置 */ s1SliceArrayPointer := unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s1))) fmt.Println("s1[0]:", *(*int)(s1SliceArrayPointer)) fmt.Println("s1 address:", s1SliceArrayPointer) s2SliceArrayPointer := unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s2))) fmt.Println("s2[0]:", *(*int)(s2SliceArrayPointer)) fmt.Println("s2 address:", s2SliceArrayPointer) offset2Pointer := unsafe.Pointer(uintptr(s1SliceArrayPointer) + unsafe.Sizeof(&s1[0])*2) fmt.Println("offset s1[2]:", *(*int)(offset2Pointer)) fmt.Println("offset s1 address:", offset2Pointer) } // 结果: [1 2 3 4 5 6 7] [3 4 5] 7 5 s1[0]: 1 s1 address: 0xc000010240 s2[0]: 3 s2 address: 0xc000010250 offset s1[2]: 3 offset s1 address: 0xc000010250
参考:
4.2.2. reslice
- 概述
- 基于已有的 slice , 创建新slice对象,以便在cap允许范围内调整属性;
4.2.3. append()
append属于内置函数,用于 slice 的元素添加操作。
append()
扩容- 在无需扩容时,append 函数返回的是指向原底层数组的新切片;
- 在需要扩容时,append 函数返回的是指向新底层数组的新切片。
- 扩容一般按照容量的两倍进行扩容;
- 当原切片的长度(以下简称原长度)大于或等于
1024
时,Go 语言将会以原容量的1.25
倍作为新容量的基准。 - 若一次追加元素过多, 超过原容量的两倍, 则新容量以新长度为基准;
- 当原切片的长度(以下简称原长度)大于或等于
-
语法
append() 支持链式操作, 可以将多个append()组合起来; //== 语法: == slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...) // 打散拼进去?? // As a special case, it is legal to append a string to a byte slice, like this: slice = append([]byte("hello "), "world"...) //在中间插入元素 var a []int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片 //示例: var a = []int{1, 2, 3} a = append([]int{0}, a...) // 在开头添加1个元素 a = append([]int{-3, -2, -1}, a...) // 在开头添加1个切片 fmt.Println(a) // [-3 -2 -1 0 1 2 3] var b []int b = append(a[:0], append([]int{10}, b[0:]...)...) // 在第0个位置插入10 b = append(a[:0], append([]int{1, 2, 3}, b[0:]...)...) // 在第0个位置插入切片 fmt.Println(b) // [1 2 3 10]
-
切片的底层数组何时被替换??
- 确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。
注意:
- append() 属于命令,而非函数;具体函数实现: StackOverflow
4.2.4. copy()
-
语法:
- 在两个slice间复制数据; 复制长度以 len 小的为准;
- 两个slice 可以指向同一个底层数组, 允许元素区间重叠;
//语法 copy(destSlice, srcSlice) int slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{5, 4, 3} copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中 copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
-
注意:
- 切片索引生成的切片与原来的切片是同一个地址;
- 多个切片指向同一底层数组,那么对其中一个切片的改变影响其它的切片。
- 即: 改变其中的值, 影响其他切片;
s1 := []int{1,2,3,4,5} s2 := s1[0:3] // 只是基于同一个底层数组生成了一个新的切片(或者说窗口)
- make() 会生成全新切片;
s2 := make([]int, 3)
- 切片索引生成的切片与原来的切片是同一个地址;
-
思想
- 应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。
4.2.5. 删除元素
- 语法
1. 头部删除
// 1. 可以通过直接移动数据指针;
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
// 2. append() 不会导致内存空间变化;
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
// copy()
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
2. 中间删除
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间 1个元素
a = append(a[:i], a[i+N:]...) // 删除中间 N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间 1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间 N个元素
3. 尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
4.3. Map
参考:
- title: Golang Map 实现 (四) --> 前面还有3篇
4.3.1. 语法
-
概述
- 引用类型, 哈希表;
- Map 是一种无序的 键值对 的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
- Map 是一种集合,可以像迭代数组和切片那样迭代它。
- 不过,Map 是无序的,无法决定它的返回顺序,是因为 Map 是使用 hash 表来实现的。
-
特点
- 从 map 中取回的是一个value的临时复制品,对齐成员的修改无任何意义;
- map 被设计成 not addressable 通过原value 指针修改成员值, 会报错;
- 完整替换 value 或 使用 指针 ; --Go学习笔记;
-
语法格式
/* 声明变量,默认 map 是 nil */ var mapName map[mapKey]mapValue /* 使用 make 函数 */ mapName := make(map[mapKey]mapValue) 1. 初始化 1. 如果不初始化 map ,那么就会创建一个 nil map 。 nil map 不能用来存放键值对。 2. 使用内置函数 make 创建的 map 可以用于存储键值对。 // nil 键值对 // 通过声明映射创建一个 nil映射 var book map[string]string book["author"] = "wohu" // nil map 不能用来存放键值对; Runtime Error: panic: runtime error: assignment to entry in nil map //正确初始化: d1 := make(map[string]string) var d2 = map[string]string{} // 注意:后面带了个大括号 d1["age"] = "18" d2["name"] = "wohu" // 示例 maxValues = map[string]int64{ "tinyint": 0x7F, "smallint": 0x7FF, "mediumint": 0x7FFFF, "int": 0x7FFFFFF, "integer": 0x7FFFFFF, "float": 0x7FFFFFFF, "decimal": 0x7FFFFFFF, "double": 0x7FFFFFFF, "bigint": 0x7FFFFFFFFFFFFFF, } 2. 查询 // 第一种情况,断言查询 val,ok := map[key] // 第二种情况直接查询 -- 若键值不存在, val为默认零值 val := map[key] 3. 删除 delete(map, key) 4. 清空 map 1. Go 语言中并没有为 map 提供任何清空所有元素的函数、方法, 2. 清空 map 的唯一办法就是重新 make 一个新的 map , 3. 不用担心垃圾回收的效率, Go 语言中的并行垃圾回收效率比写一个清空函数要高效的多。 5. 单 key 对应多个value //1 key 多 value mp1 := make(map[int][]int) mp2 := make(map[int]*[]int) 6. 追加元素 someMap["someKey"]="someValue"
4.3.2. 注意
- 语法
1. 2. 获取 键值对数量 println(len(m)) //cap 无效
- Go 语言字典的键类型不可以是 函数类型、字典类型 和 切片类型。
- 要特别注意: 键的类型是接口类型时, 键值的实际类型也不能是以上三种;
- map 默认 无序, 与存储的顺序也没有关系;
- 键类型的 值 要支持判等操作; 即: 键的值之间可以使用
==
和!=
;
4.3.3. 并发安全
并发安全map – sync.map
参考:
4.4. struct
4.4.1. 基本语法
-
概述
- 值类型, 赋值和传参会复制全部内容;
- 可以用 “_” 补位字段; 支持指向自身类型的指针;
-
特点:
- 顺序初始化 需要包含全部字段; 否则会出错;
- 只有当结构体实例化时,才会真正地分配内存;
-
语法
1. 声明 type Student struct { Id int Name string Age int Mobile int Major string } 2. 创建变量 // 创建Student指针类型变量 s1 := new(Student) // 创建Student指针类型变量 s2 := &Student{} // 创建Student类型变量 s3 := Student{} 补充: ==实例化与初始化== java 实例化 一般是由类创建的对象,在构造一个实例的时候需要 在内存中开辟空间,即 Student s = new Student(); 初始化 实例化的基础上,并且对 对象中的值进行赋一下初始值 3. 实例化 == 实例化 == 以var的方式声明结构体即可完成实例化; var ins T 1. 基本实例化 type Point struct { X int Y int } var p Point // p是值 p.X = 10 p.Y = 20 2. 指针类型结构体 实例化 ins := new(T) // 形成指针类型的结构体; . 访问成员变量; type Player struct{ Name string HealthPoint int MagicPoint int } tank :=new(Player) tank.Name = "Canon" tank.HealthPoint =300 // 注: 结构体 和 结构体指针 是两种类型; // 对于接口的实现来讲, 是不一样的 3. 取地址实例化 ins := &T{} // 看做 new() 操作; 应用较广 type Command struct{ Name string Var *int Comment string } var version int = 1 cmd := &Command{} // 取结构体地址进行实例化 cmd.Name = "version" cmd.Var = &version cmd.Comment = "show version" 4. 初始化: 1. 键值对初始化--"填充字段多的情况" varName := structName{key1: value1, key2: value2..., keyn: valuen} // 键值之间以:分隔,键值对之间以,分隔。 type People struct{ name string child * People } relation := &People{ name: "爷爷", child: &People{ name: "爸爸", child: &People{ name: "我", }, }, } 2. 列表初始化--"填充字段少的情况" --多个值使用 , 分隔开; ==注意: 初始化结构体所有字段, 顺序 addr := Address{ "四川", "成都", 610000, "0", } 3. 初始化匿名结构体 ins := struct{ //匿名结构体字段定义 字段1 字段类型1 字段2 字段类型2 ... }{ //字段初始化值-可选的 初始化字段1: 字段1的值, 初始化字段2: 字段2的值, ... } // 参考中 结构体嵌套 ==> 派生
参考:
4.4.2. 匿名结构体
- 语法
1. 初始化 // 实例化一个匿名结构体 msg := &struct { // 定义部分 id int data string }{ // 值初始化部分 1024, "hello", }
4.4.3. 空结构体
-
作用:
- 如果使用的是map,而且map又很长,通常会节省不少资源
- 空struct{}也在向别人表明,这里并不需要一个值, 仅包含需要方法
// 空结构体 大小 a := struct{}{} println(unsafe.Sizeof(a)) // Output: 0 var x [1000000000]struct{} fmt.Println(unsafe.Sizeof(x)) // prints 0 // 切片元素不占用空间 // 空结构体切片 var x = make([]struct{}, 1000000000) //只是头部数据占用空间 fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground // 空结构体 定义方法 type Lamp struct{} func (l Lamp) On() { println("On") } func (l Lamp) Off() { println("Off") } func main() { // Case #1. var lamp Lamp lamp.On() lamp.Off() // Output: // on // off // Case #2. Lamp{}.On() Lamp{}.Off() // Output: // on // off }
参考: 参考描述很棒
4.4.4. 其他用法
- 结构体中包含map
1. 正确示例1 type Querystruct { Attributes []string Modifiers map[string][]Modifier Sources map[string][]string SourceAliasesmap[string]string ConditionTree *ConditionNode } // 取地址初始化 func NewQuery() *Query { return &Query{ Attributes: make([]string, 0), Modifiers: make(map[string][]Modifier), Sources:map[string][]string{ "include": make([]string, 0), "exclude": make([]string, 0), }, SourceAliases: make(map[string]string), ConditionTree: nil, } } 2. 错误示例: type Param map[string]int type Show struct { Param } func main(){ s:=new(Show) s.Param ["RMB"]=1000 fmt.Println(*s) } // new关键创字无法初始化 Show 结构体中的 Param 属性; // 直接使用 s.Param 会报错;
5. 函数 方法 接口
- 函数&方法&接口 区别:
-- 函数
func function_name( [parameter list] ) [return_types] {
函数体
}
--方法:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
注:
1. 参数列表() 在函数名前面;
5.1. 函数
5.1.1. 函数概述
5.1.1.1. 概述
-
函数的本质:
- 函数作为一种复合数据类型, 可以看作是一种特殊的变量;
- 函数名(): 将函数进行调用;
- 函数名: 指向函数体的内存地址;
-
函数类型
- 有名函数
- 匿名函数
5.1.1.2. 函数定义
-
语法格式
func function_name( [parameter list] ) [return_types] { 函数体 } func:函数由 func 开始声明 function_name: 函数名称,函数名和参数列表一起构成了函数签名。 parameter list: 参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。 参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。 return_types: 返回类型,函数返回一列值。return_types 是该列值的数据类型。 有些功能不需要返回值,这种情况下 return_types 不是必须的。 函数体:函数定义的代码集合 eg: /* 函数返回两个数的最大值 */ func max(num1, num2 int) int { /* 定义局部变量 */ var result int if (num1 > num2) { result = num1 } else { result = num2 } return result } // 打印函数类型 fmt.Printf("%T")
5.1.1.3. 参数传递
- 值传递: 传递数据的副本
- 值类型的数据, 默认是值传递: 基础类型, array, struct
- 引用传递: 传递的是数据的地址:
- 引用类型的数据, 默认使用引用传递: slice, map, chan
new make 进行初始化
- 引用类型的数据, 默认使用引用传递: slice, map, chan
==================
- 不定参数
- 定义:
- 不定参数又叫 可变参数, 指函数传入的参数是可变的;
- 本质: …type 本质上是一个数组切片, 即 []type; –
reflect.Valueof(args).Kind()
查看类型是 slice - 注意:
- 不定参数类型相同 且 必须是函数的最后一个参数;
- 切片可以作为参数传递给不定参数,切片名后要加上 … ;
- 形参为不定参数的函数和形参为切片的函数类型是不相同的;
- 可变参数存在 多种数据类型,则可以将可变参数类型设置成 interface{}
- 代码演示:
// 声明: param ...type // 可变参数的函数 func myfunc(args ...int) { for _, arg := range args { fmt.Println(arg) } } // 参数传递 n := []int{1, 2, 3, 4, 5} result := sumOne(n...) // 切片元素作为参数传递给函数的不定参数,需要在切片名后加上 ... fmt.Println("result is ", result) ret := sumTwo(n) // 传递切片自身 // 存在多种数据类型 func demo(a string, i int, other ...interface{}) { }
- 定义:
====================
5.1.1.4. 返回值
-
return 用于函数返回值, 或终止当前函数
-
支持 多值返回;
1. 支持有名的返回值,参数名就相当于函数体内最外层的局部变量, 命名返回值变量 会被初始化为类型零值,最后的 return 可以不带参数名直接返回; // sum 相当于函数内的局部变量,被初始化为 0 func add(a, b int) (sum int) { sum = a + b return // return sum 的简写模式 // sum := a + b 则相当于新声明一个 sum 变量名,原有的 sum 变量被覆盖 // return sum 需要显式地调用 return sum } 2. 支持 多返回值, 1. 不能使用 容器对象 接收多返回值, 只能使用 多个变量或 "_" 忽略; 2. 多返回值 可直接作为其他函数调用实参; 即: 函数可以直接做为形参; x, _ := test() println(add(test()))
5.1.1.5. 其他
- 注意
- 参数列表 不支持默认值参数; Go 没有默认参数值;
- 不支持函数重载;
- 不支持 命名函数嵌套, 但支持匿名函数嵌套;
- 如果实参包括引用类型,如 指针、 slice、 map、 function、 channel 等类型,实参可能会由于函数的间接引用被修改。非引用实参通过值传递;
// 1. 命名函数嵌套 func add(a , b int) (sum int) { anonymous:= func(x , y int) int { return x + y } return anonymous(a , b) }
5.1.2. 函数调用
5.1.2.1. 其他使用
-
其返回值作为其他函数的入参;
-
有具体名称的函数 可以看做是函数类型的变量;
-
Go语言中函数也是类型, 可以作为参数传递给别的函数;
// 2. 有名函数可以直接赋值给变量; f := sum // sum ret := f(3, 4) //通过该变量直接调用函数; // 3. 函数作为参数传递: n := foo(add, 1,2) //add() 直接作为参数进行传递;
5.1.2.2. 延迟调用 defer
-
多个defer注册, 按照 FILO 顺序执行; 即使某一个出错也会继续执行;
-
defer 函数必须先注册 才能执行;
- 若 defer 位于 return 之后, 因为defer没有注册, 不会被执行;
func main() { defer func() { fmt.Println("First") }() // defer 会在return 之前执行; return //后面的都不会执行 defer func() { fmt.Println("Second") }() fmt.Println("This is main func body") //不会执行 } // 包含调用函数 defer calc("1", a, calc("10", a, b)) // defer 在定义的时候会计算好调⽤函数的参数 即: 先计算出calc("10", a, b)的值, 再进行注册;
-
主动调用
os.Exit(int)
退出进程时, defer即使已经注册, 也不会执行; -
相对普通函数调用有一定的性能损耗; 不能在for循环中进行使用;
-
defer 后面调用的函数如果有返回值, 返回值会被丢弃;
-
返回值
- defer 函数的 有名返回值可视为 引用返回, 能被defer修改;
- 匿名返回值时, 相当于拷贝赋值, 不能够被修改;
// 有名返回值 func func1() (i int){ defer func() { i++ fmt.Println("defer2:", i) }() //声明匿名函数并直接执行 return 0 //结果为1 }
-
defer 通常用于释放资源和错误处理;
// 释放资源 valueByKeyGuard.Lock() // valueByKeyGuard sync.Mutex defer valueByKeyGuard.Unlock() return valueByKey[key] // 打开文件 f, err := os.Open(filename) if err != nil { return 0 } // 延迟调用Close, 此时Close不会被调用 defer f.Close() // defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。 // 不能将这一句代码放在 nil 之前,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
参考:
5.1.3. 匿名函数
-
概述
- 匿名函数: 没有函数名的函数实现;
- 可以直接赋值给变量;
- 可以当 实参; 可以作为返回值;
- 可以直接被调用;
//定义格式: func(参数列表) (返回值){ 函数体 } //定义时 匿名函数用作回调函数 // 声明匿名函数并直接执行 func(args){ ...CODE... }(parameters)
参考:
5.1.4. 闭包
-
Go 语言闭包函数的定义:当匿名函数引用了外部作用域中的变量时就成了闭包函数,
- 闭包函数是函数式编程语言的核心; 闭包是运行期动态的概念;
- 也就是匿名函数可以会访问其所在的外层函数内的局部变量。
- 当外层函数运行结束后,匿名函数会与其使用的外部函数的局部变量形成闭包。
-
闭包:
- 一个外层函数中, 有内层函数, 该内层函数中, 会操作外层函数的局部变量(外层函数中的参数, 或者外层函数中直接定义的变量), 并且这个外层函数的返回值就是这个内层函数;
这个内层函数和外层函数的局部变量, 统称 闭包结构; - 局部变量的生命周期会发生改变, 闭包中的局部变量不会随着外层函数的结束而终止, 因为内层函数还要使用;–变量会跟随闭包的生命期一致存在;
- 因为匿名函数捕捉的局部变量的声明周期随闭包一直存在, 所以
参考: 千锋教育–闭包
- 一个外层函数中, 有内层函数, 该内层函数中, 会操作外层函数的局部变量(外层函数中的参数, 或者外层函数中直接定义的变量), 并且这个外层函数的返回值就是这个内层函数;
-
闭包结构
- 当是两个不同的闭包实例时, 拥有不同的地址; 闭包的记忆将重新开始;
func add() func(int) int { var x int return func(y int) int { x += y return x } } func main() { var f = add() fmt.Println(f(1)) //1 fmt.Println(f(2)) //3 fmt.Println(f(3)) //6 f1 := add() fmt.Println(f1(4)) //4 fmt.Println(f1(5)) //9 }
参考:
5.1.5. 错误处理
io.EOF是io包中的变量, 表示文件结束的错误:
-
panic()
- 谨慎使用 panic() 抛出异常, 如果没有捕获异常, 将会导致程序异常退出;
- panic() 发生前注册的defer, 会在panic() 之前 执行;
- 异常 可以直接调用 panic() 产生, 也可以有运行时的错误产生;(如: 越界访问数组)
-
recover()
- 该函数可以让 宕机流程中的 goroutine 恢复过来;
注意
:1. 仅在 defer 中有效; 2. 当前 goroutine 陷入panic, 调用recover() 可以捕捉panic的输入值, 并恢复到正常执行; 1. recover 只有在 defer 调用的函数内部时, 才能阻止 panic 抛出的异常信息继续向上传递; 3. 延迟调用中引发的错误, 可被后续延迟调用捕获, 但仅 `最后一个错误可被捕获`; 4. 任何未捕获的错误都会沿调用堆栈向外传递。
- go 语言中的recover() 的宕机恢复机制就对应其他语言中的 try/catch 机制;
-
代码演示
// recover() 捕捉异常 package main import "fmt" func test() { defer func() { fmt.Println(recover()) }() defer func() { panic("defer panic") //仅最后一个panic会被捕捉; }() panic("test panic") } func main() { test() } // 结果输出 $ go run test_recover.go defer panic
-
实战应用
package main import ( "fmt" "time" ) // 抛出异常,模拟实际 Panic 的场景 func throwException() { panic("An exception is thrown! Start Panic") } // Go 的 defer + recover 来捕获异常 func catchExceptions() { defer func() { if e := recover(); e != nil { fmt.Printf("Panicing %s\n", e) } }() go func() { // 做具体的实现任务 fmt.Print("do something \n") }() throwException() fmt.Printf("Catched an exceptions\n") } func main() { fmt.Printf("==== start main =====\n") // 执行一次 catchExceptions() num := 1 for { num++ fmt.Printf("\nstart circle num:%v\n", num) // 循环执行,如果实际项目中,这个函数是主任务的话,需要一个 for 来循环执行,避免捕获一次 Panic 之后就不再继续执行 catchExceptions() time.Sleep(3 * time.Second) if num == 5 { fmt.Printf("==== end main =====\r\n") return } } }
参考:
5.1.6. 高阶函数 回调函数
- 概述
- 高阶函数: 接收一个函数作为参数;
- 回调函数: 作为另一个函数参数的函数;
5.2. 方法
5.2.1. 方法定义
-
简介:
- Go 语言中同时有 函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
- 所有给定类型的方法属于该类型的方法集。
-
格式:
--方法: type mytype struct{} func (recv mytype) my_method(para) return_type {} func (recv *mytype) my_method(para) return_type {} func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ } 注: 1. 参数列表() 在函数名前面; -- 函数 func function_name( [parameter list] ) [return_types] { 函数体 } --接口 type interface_name interface { method_name1 [return_type] method_name2 [return_type] method_name3 [return_type] ... method_namen [return_type] } // 具体见接口
5.2.2. 匿名字段
5.2.3. 方法集
-
指针与值的问题:
-
方法调用
- 值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法
5.3. 接口
5.3.1. 接口定义
-
概述
- 描述1
- Go 语言提供了另外一种 数据类型 即接口,它把所有的具有共性的方法定义在一起;
- 任何其他类型只要实现了这些方法就是实现了这个接口。 – 类型与类型之间
- 描述2
- 接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,
- 而是通过方法由用户定义的类型实现。 --<Go语言实战>
- 描述1
-
格式
/* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] method_name3 [return_type] ... method_namen [return_type] } //定义接口 type Stringer interface { String() string } /* 定义结构体 */ type struct_name struct { /* variables */ } /* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_type] { /* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_type] { /* 方法实现*/ }
-
接口示例
package main import ( "fmt" ) type Phone interface { //phone接口 -- 特殊类型 call() // call() 方法 } type NokiaPhone struct { //类型 } func (nokiaPhone NokiaPhone) call() { fmt.Println("I am Nokia, I can call you!") } type IPhone struct { //类型 } func (iPhone IPhone) call() { fmt.Println("I am iPhone, I can call you!") } func main() { var phone Phone phone = new(NokiaPhone) phone.call() phone = new(IPhone) phone.call() } // 备注: 定义了一个接口Phone,接口里面有一个方法call()。\ 然后在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。 然后调用call()方法,输出结果如下: I am Nokia, I can call you! I am iPhone, I can call you!
参考:
5.3.2. 类型断言
- 语法格式:
// 需转换为接口, 再进行断言 // 1. 直接赋值 st := &St{"abcd"} var i interface{} = st // 判断i 绑定的实例是否实现了接口类型Inter o := i.(Inter) //2. comma,ok 表达式 var i1, ok := interface{}(i).(TypeNname) // comma, ok 表达式 func main() { st := &St{"abcd"} var i interface{} = st // 判断i 绑定的实例是否实现了接口类型Inter if o, ok := i.(Inter); ok { o.Ping() o.Pang() } // i 没有实现接口 Anter if p, ok := i.(Anter); ok { p.String() } // 判断 i 绑定的实例是否就是具体类型 St if s, ok := i.(*St); ok { fmt.Printf("%s", s.Name) } }
5.3.3. 执行机制
-
接口表(interface table)指针
-
数据指针
- 目标对象的 制度复制品, 复制完整对象或指针;
5.3.4. 接口转换
5.3.5. 接口区分:
5.3.5.1. C++
- golang 实现类和抽象接口之间不需要硬性连接(即声明继承或虚函数),只要你的实现类实现了某接口规定的方法,那么该类的使用方就可以实例化该类并赋值给接口,然后可以通过接口直接调用具体方法。
- C++
- C++的接口可被称之为抽象类,即设定一个抽象基类去描述一组基本行为(接口),然后通过多样化的派生类去完成该接口。
- 接口作为不同组件之间的契约存在,且对契约的实现是强制的,即语言必须声明实现了该接口。
参考:
5.4. 公开或未公开的标志符
- 概述
- Go语言支持从包里公开或隐藏标志符;
5.5. 示例
// 包 间访问结构体类型
==说明:==
1. 由于内部类型 user 未公开, 无法直接通过结构字面量的方式初始化该内部类型;
2. 内部类型里面的字段是公开的,由于提升到外部, 可以通过外部类型 来访问;
6. Go特性
6.1. 面向对象
-
面向对象编程:
Go 面向对象编程的三大特性:封装、继承和多态。- 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
- 继承:使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
- 多态:不同对象中同种行为的不同实现方式
-
Go编程
- Go 语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。
参考:
6.1.1. 封装
6.1.2. 继承
-
概述:
- 确切地说,Go 语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go 语言的继承方式采用的是匿名组合的方式。
-
代码示例
// 结构体之间的组合, 方法的隐藏 package main type Base struct { Name string } func (base *Base) Foo() { ... } func (base *Base) Bar() { ... } type Foo struct { Base ... } func (foo *Foo) Bar() { foo.Base.Bar() ... }
6.1.3. 多态
-
概述
- 在面向对象中,多态的特征为:不同对象中同种行为的不同实现方式。在 Go 语言中可以使用接口实现这一特征。
-
示例代码
package main import ( "fmt" ) // 正方形 type Square struct { side float32 } // 长方形 type Rectangle struct { length, width float32 } // 接口 Shaper type Shaper interface { Area() float32 } // 计算正方形的面积 func (sq *Square) Area() float32 { return sq.side * sq.side } // 计算长方形的面积 func (r *Rectangle) Area() float32 { return r.length * r.width } func main() { // 创建并初始化 Rectangle 和 Square 的实例,由于这两个实例都实现了接口中的方法, //所以这两个实例,都可以赋值给接口 Shaper r := &Rectangle{10, 2} q := &Square{10} // 创建一个 Shaper 类型的数组 shapes := []Shaper{r, q} // 迭代数组上的每一个元素并调用 Area() 方法 for n, _ := range shapes { fmt.Println("矩形数据: ", shapes[n]) fmt.Println("它的面积是: ", shapes[n].Area()) } } /* 矩形数据: &{10 2} 它的面积是: 20 图形数据: &{10} 它的面积是: 100 */
6.2. 泛型
空接口
- 概述:
- Go 语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型,
- 空接口不是真的为空,接口有类型和值两个概念
- Go 语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型,