写在最前,推荐几个很好的Golang学习链接
局部变量声明必须使用,不使用就编译失败
全局变量可以只声明,不使用
var 变量名 变量类型
变量名 := 变量值
var name string // 全局变量声明
var ( // 批量声明
age int
isOK bool
)
/* 函数外的每个语句都必须以关键字var/const/func开始*/
func test() {
ready := true // 简短声明: 只能在函数中使用
// 不能在全局变量中使用(编译失败)
}
常量,代表永远只读,不能修改(bool、数值、string)
iota是go语言的常量计数器,只能在常量的表达式中使用
iota在const关键字出现时,将被重置为0
const中每新增一行,常量声明将使iota计数累加一次(iota可理解为const语句块中的航索引)
使用iota能简化定义,在定义枚举时很有用
const (
n1 = iota
n2
n3
)
go 采用const+iota实现枚举
// C语言中的枚举变量enum
enum
{
SUNDAY = 0,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
// go中没有枚举类型enum,使用const代替
const (
SUNDAY = 0
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
)
类型别名
TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
类型别名/类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?下面通过一段代码来理解
package main
import ("fmt")
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a NewInt
fmt.Printf("a type: %T\n", a) // 查看a的类型名 a type: main.NewInt
// 将a2声明为IntAlias类型
var a2 IntAlias
fmt.Printf("a2 type: %T\n", a2) // 查看a2的类型名 a2 type: int
}
类型定义
type byte uint8
type add_func func(int, int) int
// 声明自定义类型
type People struct {
name string
age int
}
1.加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
2.加了fallthrough语句后,【紧跟的后一个】case条件不能定义常量和变量
3.执行完fallthrough后直接跳到下一个条件语句,本条件执行语句后面的语句不执行
TODO:补充下make创建slice、map、chan的使用案例
Go语言中的 strconv 包为我们提供了字符串和基本数据类型之间的转换功能
string 与 int 类型之间的转换
Itoa(): int ==> string func Itoa(i int) string
Atoi(): string ==> int func Atoi(s string) (i int, err error)
func main() {
str2 := "s100"
num2, err := strconv.Atoi(str2)
if err != nil {
fmt.Printf("%v 转换失败!", str2)
} else {
fmt.Printf("type:%T value:%#v\n", num2, num2)
}
}
// s100 转换失败!
Parse 系列函数:字符串 ==> 指定类型的值
/* string ==> bool
* @param [in] 参数只能是 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,
* 其他的值返回错误
*/
func ParseBool(str string) (value bool, err error)
/* string ==> int
* @param [in] base 进制,范围是2~36。如果base==0,则会从字符串前置判断
* @param [in] bitSize 指定结果必须能无溢出赋值的证书类型,0、8、16、32、64,分别代表int、int8、int16、int32、int64
* @return 返回的 err 是 *NumErr 类型的,如果语法有误,err.Error = ErrSyntax,如果结果超出类型范围 err.Error = ErrRange。
*/
func ParseInt(s string, base int, bitSize int) (i int64, err error)
// ParseUint() 函数的功能类似于 ParseInt() 函数,但 ParseUint() 函数不接受正负号,用于无符号整型
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
/* string ==> float
* @param [in] bitSize 指定了返回值的类型,32 表示 float32,64 表示 float64;
* @return 返回值 err 是 *NumErr 类型的,如果语法有误 err.Error=ErrSyntax,如果返回值超出表示范围,返回值 f 为 ±Inf,err.Error= ErrRange。
*/
func ParseFloat(s string, bitSize int) (f float64, err error)
Format系列函数:将给定类型数据==>string
// bool ==> string
func FormatBool(b bool) string
/* int ==> string
* @param [in] i 必须是int64类型
* @param [in] base 参数base必须在2~36之间,返回结果中会使用小写字母“a”到“z”表示大于 10 的数字。
*/
func FormatInt(i int64, base int) string
// 与 FormatInt() 函数的功能类似,但是参数 i 必须是无符号的 uint64 类型
func FormatUint(i uint64, base int) string
/* int ==> string
* @param [in] i 必须是int64类型
* @param [in] bitSize 表示参数 f 的来源类型(32 表示 float32、64 表示 float64),会据此进行舍入
* @param [in] fmt 表示格式,可以设置为
* “f”表示 -ddd.dddd
* “b”表示 -ddddp±ddd,指数为二进制
* “e”表示 -d.dddde±dd 十进制指数
* “E”表示 -d.ddddE±dd 十进制指数
* “g”表示指数很大时用“e”格式,否则“f”格式
* “G”表示指数很大时用“E”格式,否则“f”格式。
* @param [in] prec 控制精度(排除指数部分)
* 当参数 fmt 为“f”、“e”、“E”时,它表示小数点后的数字个数;当参数 fmt 为“g”、“G”时,
* 它控制总的数字个数。如果 prec 为 -1,则代表使用最少数量的、但又必需的数字来表示 f。
*/
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
{
var num float64 = 3.1415926
str := strconv.FormatFloat(num, 'E', -1, 64)
fmt.Printf("type:%T,value:%v\n ", str, str) // type:string,value:3.1415926E+00
}
Append系列:① 指定类型 ==> string ② 追加到一个切片中
package main
import (
"fmt"
"strconv"
)
func main() {
// 声明一个slice
b10 := []byte("int (base 10):")
// 将转换为10进制的string,追加到slice中
b10 = strconv.AppendInt(b10, -42, 10)
fmt.Println(string(b10))
b16 := []byte("int (base 16):")
b16 = strconv.AppendInt(b16, -42, 16)
fmt.Println(string(b16))
}
/*
运行结果
int (base 10):-42
int (base 16):-2a
*/
type 别名 = 已经存在的变量类型
string中的每一个元素叫做“字符”,GO预研的字符有以下两种
uint8 \ byte类型:代表了ACSII码的一个字符
var ch byte = 'A' // 字符使用单引号括起来
rune类型:等价于uint32类型。代表一个Unicode(UTF-8字符),当需要处理中文、日文或者其他复合字符时,需要用到rune类型
参考链接
类型转换
定义
格式:
/*
* @param x 一个接口的类型
* @param T 一个具体的类型(也可为接口类型)
* @return 返回 x 的值(也就是 value)和一个布尔值(也就是 ok)
* 可以根据布尔值判断 x 是否为 T 类型
*/
value, ok := x.(T)
代码示例
① 简单案例
注意:
如果不接收第二个参数也就是下面代码中的 ok,断言失败时会直接造成一个 panic
如果 x 为 nil 同样也会 panic
package main
import ("fmt")
func main() {
var x interface{} // 定义接口类型
x = 10
value, ok := x.(int)
fmt.Print(value, ",", ok) // 10, ture
}
② 类型断言还可以配合 switch 使用
package main
import ("fmt")
func main() {
var a int
a = 10
getType(a)
}
func getType(a interface{}) {
switch a.(type) {
case int:
fmt.Println("the type of a is int")
case string:
fmt.Println("the type of a is string")
case float64:
fmt.Println("the type of a is float")
default:
fmt.Println("unknown type")
}
}
// the type of a is int
常量在编译时被创建,即使在函数内部也是如此
常量类型:只能是(bool、数字型、string)
iota常量生成器
常量声明可以使用iota常量生成器初始化,即:它用于生成一组以相似规则初始化的常量(不用每行都写一遍初始化表达式,简化代码)
在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一
type Weapon int // 将 int 定义为 Weapon 类型
const (
Arrow Weapon = iota // 开始生成枚举值, 默认为0
Shuriken
SniperRifle
Rifle
Blower
)
// 输出所有枚举值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower) // 0 1 2 3 4
// 使用枚举类型并赋初值
var weapon Weapon = Blower
fmt.Println(weapon) // 4
将枚举值转化为字符串
package main
import "fmt"
// 声明芯片类型
type ChipType int
const (
None ChipType = iota
CPU // 中央处理器
GPU // 图形处理器
)
func (c ChipType) String() string {
switch c {
case None:
return "None"
case CPU:
return "CPU"
case GPU:
return "GPU"
}
return "N/A"
}
func main() {
// 输出CPU的值并以整型格式显示
fmt.Printf("%s %d", CPU, CPU)
}
值类型:变量直接存储,内存在栈上分配
基本数据类型:int/float/bool/string、数组、struct
- 值传递:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据
引用类型:变量存储的是一个地址(指向内存),内存通常在堆上分配
指针、chan、slice/map/interface等,以引用方式传递
- 引用传递:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方
在Go语言中,return语句在底层并不是原子操作,而是分为两步:返回值赋值、RET指令
1-当函数返回时,执行defer注册的函数 ==> 可以做资源清理
func read() {
file := open(filename)
defer file.Close()
//文件操作
}
2-多个defer语句,按照先进后出的方式执行
3-defer语句中的变量,在defer声明时就决定了
// 案例1: 执行结果 0
func a() {
i := 0
defer fmt.Println(i)
i++
}
// 案例2: 执行结果 5 4 3 2 1
func f() {
for i := 1; i <= 5; i++ {
defer fmt.Printf(“%d “, i)
}
}
详细介绍defer
作用域:函数返回之前调用(而不是在退出代码块作用域
之前执行)
参数预算
defer关键字使用【传值】的方式传递参数时会进行预计算,导致不符合预期的结果
案例1
func main() { startedAt := time.Now() defer fmt.Println(time.Since(startedAt)) time.Sleep(time.Second) } /* $ go run main.go // 错误,不符合预期 0s */
案例1
:defer关键字会立即拷贝函数中引用的外部参数,所以time.Since(startedAt)的结果不是main函数退出之前,而是在defer关键字调用时计算 ==> 最终导致代码输出0s
- 想要解决上面的问题非常简单==>只需要
向defer关键字传入匿名函数
案例2
func main() { startedAt := time.Now() // defer + 匿名函数() defer func() { fmt.Println(time.Since(startedAt)) }() time.Sleep(time.Second) } /* $ go run main.go // 正确,符合预期 1s */
案例2
:虽然defer关键字使用值传递,但是因为拷贝的是函数指针,所以time.Since(startedAt)会在main函数返回前调用并打印出符合预期的结果
宕机:有些错误只能在运行时检查,如数组访问越界、空指针引用
等,这些运行时错误会引起宕机(可能造成体验停止、服务中断)
当宕机发生时
说明:Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。
panic 和 recover 的关系
panic 和 recover 的组合有如下特性:
类比于try-catch机制
使用前提
使用场景
使用案例
package main
import (
"fmt"
"runtime"
)
// 崩溃时需要传递的上下文信息
type panicContext struct {
function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
defer func() { // 延迟处理的函数
err := recover() // 发生宕机时, 由defer+recover捕获异常, 进行后面的处理
switch err.(type) {
case runtime.Error:
fmt.Println("runtime error:", err)
default:
fmt.Println("error:", err)
}
}()
entry()
}
func main() {
ProtectRun(func() {
fmt.Println("手动宕机前")
panic(&panicContext{"手动触发panic"}) // 手动触发宕机: 抛出异常, 后面的语句不会被执行
fmt.Println("手动宕机后") // 这句话不会被打印
})
}
/*
手动宕机前
error: &{手动触发panic}
*/
func sc() {
figer := 3
switch figer {
case 1, 2:
case 3:
default:
}
}
只有true、falase两个值
break + 标签
func main() {
FOR: // 退出for循环
for i := 0; i < 10; i++ {
fmt.Println(i)
if i == 5 {
break FOR // break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块
// 标签要求必须定义在对应的for、switch和 select的代码块上
}
}
}
fallthrough
func switchDemo(age int) {
switch {
case age < 25:
fmt.Println("switch1")
fallthrough // 仅仅向下走一层
case age > 25 && age < 35:
fmt.Println("switch2")
fallthrough
case age > 60:
fmt.Println("switch3")
default:
fmt.Println("switch4")
}
}
switchDemo(10)
/*
switch1
switch2
switch3
*/
相同类型、固定长度(一旦定义,长度不能变)
值传递 (数组作为参数时,是另一份拷贝)
预想修改数组的值,可以使用切片作为参数(切片是数组的一个引用)
package main
import "fmt"
// 数组-->值传递: 拷贝另外一份相同的副本, 原数组不会被修改
func modify_arr_1(arr [3]int, len int) {
for i := 0; i < len; i++ {
arr[i] = -1 // 修改数组失败, 因为是值传递
}
}
// 切片-->引用传递:形参\实参,指向同一份数据,一个修改,另外一个也会修改
func modify_arr_2(arr []int, len int) {
for i := 0; i < len; i++ {
arr[i] = -1 // 修改数组失败, 因为是值传递
}
}
func main() {
arr := [3]int{1,2,3}
fmt.Print(arr) // [1 2 3]
// 参数: 数组
modify_arr_1(arr, len(arr))
fmt.Print(arr) // [1 2 3]
// 参数: 引用slice
modify_arr_2(arr[:], len(arr))
fmt.Print(arr) // [-1 -1 -1]
}
声明
var 数组变量名 [元素数量]Type
[10]int
[200]interface{}
初始化
var age [5]int{1,2,3}
var age = [5]int{1,2,3}
var age = [5]int{0:1, 2:3} // 指定下标
var age = [...]int{1,2,3}
比较两个数组是否相等
==
和!=
)来判断两个数组是否相等切片是对数组的一个连续片段的引用,所以切片是引用类型
切片比较
==
来判断两个切片是否含有全部相等的元素len(slice)==0
,不应该使用slice==nil
声明
var 切片变量名 []切片类型
[]int
[]interface{}
初始化
var slice []int = arr[start:end] // 从已知数组中切除[start,end)区间作为slice
slice []int = []int{1,2,3} // 使用字面量初始化新的切片
var slice []int = make([]int, len) // 使用关键字 make 创建切片
slice := make([]int, len)
slice := make([]int, len, cap)
说明:切片的初始化,相较于数组,有一个微笑的差别,就是[]中没有指明长度
数组:类型 [n]T 表示拥有 n 个 T 类型的值的数组
切片:类型 []T 表示一个元素类型为 T 的切片
切片
切片相关内置函数+操作函数
插入
注意:在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
/* 尾插 */
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
/* 头插 */
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
/* 中间插入 */
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
删除
删除开头的元素
// 方式1:通过直接移动数据指针
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个元素
// 方式3:用 copy() 函数来删除开头的元素
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间位置删除
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:
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个元素
从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
遍历:range迭代遍历
for idx, val := range slice {...}
易错使用点:range 返回的是每个元素的副本,而不是直接返回对该元素的引用,如下所示。
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
/*
Value: 变量值不一样 (value接收的是数据的拷贝)
Value-Addr: 用一个变量value接受,地址一样(都是value的地址)
ElemAddr: 用下标slice[index]接受,地址不一样(都是每个元素的地址)
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
*/
分析:value-Addr都是一样的(10500168),该值是一个新的拷贝,并不指向原来的元素地址slice[index] ==> 因此,要想获取每个元素的地址,需要使用&slice[index]
拷贝
格式:copy( destSlice, srcSlice []T) int
s1 := []int{1,2,3,4,5}
s2 := make([]int, 10)
copy(s2, s1)
string与slice
string底层就是一个byte的数组,因此,也可以进行切片操作
str := “hello world”
s1 := str[0:5]
fmt.Println(s1)
如何改变string中的字符值
str := “hello world” // 字符串
s := []byte(str) // 类型转换: string-->切片
s[0] = 'O' // 修改idx=0的值
str = string(s) // 类型转换: 切片-->string
数组arr、切片slice对比测试
func test_arr() {
var x[3]int = [3]int{1,2,3}
var y[3]int = x
fmt.Println(x,y) // [1 2 3] [1 2 3]
y[0]=999
fmt.Println(x,y) // [1 2 3] [999 2 3]
}
func test_slice() {
var x[]int = []int{1,2,3}
var y[]int = x
fmt.Println(x,y) // [1 2 3] [1 2 3]
y[0]=999
fmt.Println(x,y) // [999 2 3] [999 2 3]
}
重要: 结构体转map\string\interface{}的若干方法
格式:map[key]value
==
和 !=
操作。 key的常用数据类型
==
和 !=
操作)注意:key与value可以有不同的数据类型 ==> 如果想不同,则使用interface作为value
map基本操作
创建
// 1 字面值
{
m1 := map[string]string{
"m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
}
}
// 2 使用make函数
{
m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
m2["m2"] = "v2" // 添加元素
}
// 定义一个空的map
{
m3 := map[string]string{}
m4 := make(map[string]string)
}
增删改查
清空:有意思的是,Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
/* 增加 or 修改*/
m["age"] = 100
/* 查询 */
v := m["age"] // 从m中取键k对应的值给v,如果k在m中不存在,则将value类型的零值赋值给v
v, ok := m["age"] // 从m中取键k对应的值给v,如果k存在,ok=true,如果k不存在,将value类型的零值赋值给v同时ok=false
{
// 查1 - 元素不存在
v1 := m["x"]
v2, ok2 := m["x"]
fmt.Printf("%#v [%#v, %#v]\n", v1, v2, ok2) // 0 [0, false]
// 查2 - 元素存在
v3 := m["age"]
v4, ok4 := m["age"]
fmt.Printf("%#v [%#v, %#v]\n", v3, v4, ok4) // 100 [100, true]
}
/* 删除 */
delete(m, "age") // 若key不存在,不执行任何操作
遍历
/* 遍历
* 1) 遍历顺序是随机的
* 2) 使用for range遍历时,k/v使用的是同一块内存, 这也是容易出现错误的地方
*/
for k, v := range m {
fmt.Printf("k:[%v].v:[%v]\n", k, v)
}
map的多键索引
sync.Map(在并发环境中使用的map)
sync.Map 有以下特性:
补充说明:sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
package main
import (
"fmt"
"sync"
)
func main() {
var scene sync.Map
// 将键值对保存到sync.Map:sync.Map 将键和值以 interface{} 类型进行保存
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值: 将查询到键对应的值返回
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对: 使用指定的键将对应的键值对删除
scene.Delete("london")
// 遍历所有sync.Map中的键值对:Range() 方法可以遍历 sync.Map,
// 遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{}
// 每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}
列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
说明:① 列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,②这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。
初始化列表:(1) New函数 (2) var关键字声明
变量名 := list.New()
var 变量名 list.List
插入元素
双链表支持从队列前方/后方插入元素,分别对应的方法是 PushFront 和 PushBack
l := list.New()
l.PushBack("fist")
l.PushFront(67)
方 法 | 功 能 |
---|---|
InsertAfter(v interface {}, mark * Element) * Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark * Element) *Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
删除元素
格式:链表对象.Remove(del_elem * Element)
遍历链表
for i := list.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
string 不能被修改,预想修改,需要先转换为 []byte 或 []rune
s1 := "hello world"
// 强制转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
常用API
len / 拼接+ / strings.Split / strings.contains / strings.HasPrefix strings.HasSuffix / strings.Index() strings.LastIndex() / string.Join(a[] string, sep string)
func main() {
s1 := "D:\\Users\\cygwin" // 双引号
s2 := `D:\Users\cygwin` // 反引号 `` 反引号中所有的转义字符都失效,文本原样输出
s3 := `第一行
第二行
第三行
`
}
定义结构体类型
type Student struct {
name_ string
age_ int
class_ string
}
struct Tag 结构体标签
目的:结构体成员首字母小写对外不可见,但是若把首字母大写,这样与外界数据交互时会带来极大的不便 ⇒ 使用场景:json/sql/ini等
为结构体的成员添加说明,以便于使用==>这些说明可以通过反射获取到
type Student struct {
Name string "the name of student"
Age int "the age of student"
Class string "the class of student"
}
package main
import (
"encoding/json"
"fmt"
)
type person struct {
// Name必须是大写: 否则下面使用会编译报错
Name string `json:"name" db:"name" ini:"name"`
Age int `json:"age"`
}
func main() {
// 序列化: struct --> json
p1 := person{
Name: "Tom",
Age: 100,
}
b, err := json.Marshal(p1) // p1传给json包,p1中的成员属性首字母要大写
if err != nil {
fmt.Printf("Marshal failed, err:%v", err)
return
}
fmt.Printf("%#v\n", string(b))
// 反序列化: json --> struct
str := `{"name": "James", age: 30}`
var p2 person
json.Unmarshal([]byte(str), &p2)
fmt.Printf("%#v\n", p2)
}
匿名成员结构体(没有变量名的成员)
多用于临时场景
同一种类型的匿名成员,最多只允许存在一个
当匿名成员是结构体时,且两个结构体中都存在相同字段==>优先选择最近的字段
func main() {
var s struct { // 使用var定义匿名结构体,而不是使用type
x string
y int
}
s.x = "hello"
s.y = 100
}
type Person struct {
Name string
Age int
}
type Student struct {
Age int
Person //匿名内嵌结构体, 二者都有Age字段
}
func main() {
var stu = new(Student)
stu.Age = 34 //优先选择Student中的Age
fmt.Println(stu.Person.Age, stu.Age) // 0, 34
}
声明/初始化
// 非指针类型
var stu1 Student
// ins := &T{} 对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
var stu2 *Student= &Student{}
stu2 := &Student{}
// new
var stu3 *Student = new(Student)
stu3 := new(Student)
(1) 键值对初始化结构体的书写格式
ins := 结构体类型名{
字段1: 字段1的值,
字段2: 字段2的值,
…
}
下面示例中描述了家里的人物关联,正如儿歌里唱的:“爸爸的爸爸是爷爷”,人物之间可以使用多级的 child 来描述和建立关联,使用键值对形式填充结构体的代码如下:
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
(3) 多个值列表初始化结构体的书写格式
使用这种格式初始化时,需要注意:
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
struct没有构造函数,但是我们可以自定义构造函数(一般采用工厂模式自定义构造函数)
5.1. 普通的构造函数
func Newstu(name string, age int, class string) *Student { // 构造函数
return &Student{
name_ : name,
age_:age,
class_:class
}
}
func main() {
stu := Newstu("darren", 34, "math")
fmt.Println(stu.name)
}
5.2. 带有父子关系的结构体的构造和初始化——模拟父级构造调用
type Cat struct { /* 父类 */
color string
name string
}
type BlackCat struct { /* 子类 */
Cat // 嵌入Cat, 类似于派生
age int // 子类新添加的变量
}
// “构造基类”
func NewCat(name string) *Cat {
return &Cat{
name: name,
}
}
// “构造子类”
func NewBlackCat(name string, age int) *BlackCat {
cat := &BlackCat{}
cat.color = "black"
cat.name = name
cat.age = age
return cat
}
// 测试案例
func main() {
blk_cat := NewBlackCat("Tom", 11)
fmt.Println(blk_cat) // &{{black Tom} 11}
}
func 函数名(参数列表) (返回值列表)
{ }
func add(a, b int) int {
return a + b;
}
func modify(a int) { // 无返回值, 返回值列表不用写
a = 100
}
// 命令的返回值,就相当于在函数中声明一个变量
func f(x, y int) (ret int) {
ret = x + y
return // 默认返回的是ret
}
内置函数
close\len\new\make\append
panic/recover
匿名函数
匿名函数的定义:就是没有名字的普通函数定义
func(参数列表)(返回参数列表){
函数体
}
在定义时调用匿名函数
func(data int) {
fmt.Println("hello", data)
}(100)
将匿名函数赋值给变量
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量
被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应
函数 + 引用环境 = 闭包,闭包是一个函数,该函数包含了它外部作用域的一个变量
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber1 为一个函数指针,函数 i 为 0 */
nextNumber1 := getSequence()
/* 调用 nextNumber1 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber1()) // 函数指针,能够保存返回值结果
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
/* 创建新的函数 nextNumber2 ,并查看结果 */
nextNumber2 := getSequence()
fmt.Println(nextNumber2())
fmt.Println(nextNumber2())
}
/*
1
2
3
1
2
*/
可变参数
func add(arg ...int) int { // arg是一个slice
}
格式:func (recv_name 接受者类型)函数名(参数列表) (返回值列表) { }
① 接收器变量命名:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名
② 使用格式:方法只能被 “接收者对象” 调用
type Person struct {
Name string
Age int
}
/*
* 方法: 与Person结构体绑定, 类似于OOP中的类
* @param [in] p 接收者: p代表结构体本身的实列,类似python中的self,这里p可以写为self
*/
func (p Person) Getname() string {
fmt.Println(p.Name)
return p.Name
}
func main() {
var person1 = new(Person)
person1.Age = 34
person1.Name = "darren"
person1.Getname()
}
简单的理解:golang中的(一个类型 + 方法) = C++中的类
定义:方法是作用在接收者(个人理解成作用对象)上的一个函数,其中,接收者是某种类型的变量
接收者的类型
invalid receiver type…
)”一个类型+方法 “ 等价于 ”面向对象中的一个类“
一个重要的区别:
注意事项
接收器类型(指针*T / 非指针T)
总结:指针/非指针接收器的使用
继承 ---- 结构体内嵌模拟类的继承
package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人类
type Human struct {
Walkable // 内嵌行走结构体: 继承了行走特性
}
// 鸟类
type Bird struct {
Walkable // 鸟类能行走
Flying // 鸟类能飞行
}
func main() {
// 实例化鸟类
b := new(Bird)
b.Fly()
b.Walk()
// 实例化人类
h := new(Human)
h.Walk()
}
interface是方法的集合
interface是一种类型,并且是指针类型
interface更重要的作用是:多态的实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用
type 接口名称 interface {
method1 (参数列表) 返回值列表
method2 (参数列表) 返回值列表
...
}
// 1-接口定义
type Skills interface {
Running() // 包含多个函数的声明, 无需实现函数
Getname() string
}
// 2-结构体定义:用于实现接口Skills
type Student struct {
Name string
Age int
}
// 3-实现“方法”(连接“接口”与“结构体”之间的桥梁):实现Skills接口的函数, 接收器类型为Student
func (p Student) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Student) Running() { // 实现 Running方法
fmt.Printf("%s running", p.Name)
}
// 4-使用接口
func main() {
var skill Skills // 4.1-定义接口对象
stu1 := Student{ // 4.2-定义结构体对象
Name : "darren",
Age : 34
}
skill = stu1 // 4.3-将结构体对象, 强制转换为接口对象
skill.Running() // 4.4-使用“接口对象”调用接口, 输出结果: darren running
}
接口嵌套,可以理解为继承 ==> 子接口拥有父接口的所有方法
若使用该子接口,必须将父接口和子接口的所有方法都实现
type Skills interface {
Running()
Getname() string
}
type Test interface {
sleeping()
Skills //继承Skills
}
接口多态
接口是实现多态的利器:同一个接口interface,不同结构体类型实现,且不同结构体对象都能执行调用
// 2.4 interface多态
package main
import "fmt"
type Skills interface {
Running()
Getname() string
}
type Student struct {
Name string
Age int
}
type Teacher struct {
Name string
Salary int
}
func (p Student) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Student) Running() { // 实现 Running方法
fmt.Printf("%s running", p.Name)
}
func (p Teacher) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Teacher) Running() { // 实现 Running方法
fmt.Printf("\n%s running", p.Name)
}
func main() {
var skill Skills // Student和Teacher都实现了各自的接口类Skills
stu := Student{"Student", 18}
tcr := Teacher{"Teacher", 30}
// 调用前, 先给skill对象赋值; 之后, 再使用skill对象调用接口
skill = stu
skill.Running() // Student running
skill = tcr
skill.Running() // Teacher running
}
方法集与方法调用问题
两个结论:T
与 *T
(1) 对于T
类型,它的方法集只包含接收者类型是T
的方法 ==> 否则,会编译错误
(2) 对于*T
类型,它的方法集则包含接收者为T
和*T
类型的方法,也就是全部方法
类型与接口的关系
在Go语言中类型和接口之间有一对多和多对一的关系,即:
(1) 一个类型可以实现多个接口
(2) 多个类型可以实现相同的接口
空接口类型 interface{}
接口中没有任何方法,就叫空接口。
因为空接口可以存储任意类型的值,所以空接口在Go语言中的使用十分广泛!
(1) 将值保存在空接口
var any interface{}
any = 1
any = "hello"
any = false
(2) 从空接口中获取值 ==> 类型断言
语法格式:
/*
* x: 表示类型为interface{}的变量
* T: 表示断言x可能是的类型
*
* v: x转换为T类型后的变量
* ok:断言成功/失败
*/
v, ok = x.(T)
空接口使用: switch…case…
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
case int:
default:
}
}
// 声明a变量, 类型int, 初始值为1
var a int = 1
// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{} = a
// 声明b变量, 尝试赋值i
var b int = i // ==> 编译失败
// 解决编译失败的问题,使用类型断言的方式
var b int = i.(int)
编译失败::cannot use i (type interface {}) as type int in assignment: need type assertion
,即编译器告诉我们,不能将i变量视为int类型赋值给b
(3) 空接口的值比较
空接口在保存不同的值后,可以和其他变量值一样使用==
进行比较操作。空接口的比较有以下几种特性。
3-1. 类型不同的空接口间的比较 ==> 结果不相同
3-2. 不能比较空接口中的动态值
当接口中保存有动态类型的值时,运行时将触发错误,代码如下:
// c保存包含10的整型切片
var c interface{} = []int{10}
// d保存包含20的整型切片
var d interface{} = []int{20}
// 这里会发生崩溃
fmt.Println(c == d)
(4) 空接口实现可以保存任意值的字典
参考链接
参考链接
参考链接
参考链接