Go学习(基础)

数据类型

数字类型

类型 描述
int8/int16/int32/int64 有符号整数
uint8/uint16/uint32/uint64 无符号整数
float32/float64 IEEE-754 32/64位浮点型数;
与Java的对应:float32 => float;float64 => double
自动类型推导为 float64
开发中建议尽量使用 float64,因为 math 包下面的计算都是用此类型
byte 类似uint8
rune 类似 int32
uint 32或64位
int 与 uint 一样大小
unitptr 无符号整型,用于存放一个指针

八进制:增加前缀 0 来表示八进制数(如:077)

十六进制:增加前缀 0x 来表示十六进制数(如:0xFF)

10的幂运算:使用 E 来表示10的连乘(如:1E3 = 1*10^3 = 1000

永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等
(转化为二进制时会损失精度)

// 浮点数比较方案
func CompareFloat() {
    var floatValue1 float32
	floatValue1 = 10
	floatValue2 := 10.0
	p := 0.00001
    // 使用 math 函数代替 == 比较
	if math.Dim(float64(floatValue1), floatValue2) < p {
		fmt.Println("floatValue1 和 floatValue2 相等")
	}
}

复数类型

类型 描述
complex64/complex128 32/64位实部和虚部

与复数相对,我们可以把整型和浮点型这种日常比较常见的数字称为实数

复数是实数的延伸。通过两个实数(计算机中使用浮点数表示)构成,实部(real)和虚部(imag)

// a b 均为实数,i 称为虚数单位
// a = 0,z 为普通实数
// a ≠ 0,z 为纯虚数
z = a + bi
z := complex(a, b)
// 获取实部
a := real(z)
// 获取虚部
b := imag(z)

推荐使用 complex128 作为计算类型,因为相关函数大都使用这个类型的参数

字符串

标准库API:strings package - strings - pkg.go.dev

默认通过UTF-8编码的字符序列,当字符为ASCII码时则占用1个字节,其它字符根据需要占用2-4个字节
(可以包含非ANSI字符,如:「Hello, 学院君」)
Go 语言标准库并没有内置的编码转换支持

是一种不可变值类型

不支持单引号

对特定字符进行转义,可以通过 \ 实现

常见需要转义字符:

  • \n :换行符
  • \r :回车符
  • \t :tab 键
  • \u 或 \U :Unicode 字符
  • \\ :反斜杠自身

可以使用`构建多行字符串(+也可以实现)

func TestString() {
    results := `
	first line
	second line
	third line`
	fmt.Println("results:", results)
}

切片:

// 是一个左闭右开的区间
func strSplice() {
    str := "hello, world"
    str1 := str[:5]  // 获取索引5(不含)之前的子串
    str2 := str[7:]  // 获取索引7(含)之后的子串
    str3 := str[0:5]  // 获取从索引0(含)到索引5(不含)之间的子串
}

遍历方式:

  • Unicode字符遍历:[for 循环](#for 循环)
  • 字节数组遍历
// 两种方式中英文字符串遍历结果不同
// 字节数组遍历
for i := 0; i < len(str); i++ {
    // 依据下标取字符串中的字符,值类型为 byte
    fmt.Println("index:", i, ",value:", str[i])
}
// Unicode 字符遍历
str := "Hello, 世界" 
for i, ch := range str { 
    // ch 的类型为 rune 
    fmt.Println(i, ch)    
}

底层字符类型(对字符串中的单个字符进行了单独的类型支持):

  • byte,代表UTF-8编码中单个字节的值
    uint8 类型的别名,两者是等价的,因为正好占据 1 个字节的内存空间)
  • rune,代表单个 Unicode 字符
    int32 类型的别名,正好占据 4 个字节的内存空间。rune 操作可查阅 Go 标准库 unicode

Unicode字符编码转化为对应的字符,可以使用 string 函数进行转化

数组

数组是值类型

一维

// 声明(支持语法糖省略长度声明,编译期自动计算长度)
// 声明时数组的长度为一个常量或一个常量表达式(编译期即可计算结果的表达式)
var variable_name [capacity]data_type{element_values}
var variable_name [...]data_type{element_values}
var variable_name [capacity]data_type{index:value,index:value} //设置指定下标的值
// 初始化
// SIZE可以不写(括号必需保留),会自动根据值的个数进行推到并设置
var variable_name = [SIZE]variable_type{val_1,val_2,...,val_SIZE}
var variable_name = []variable_type{val_1,val_2,...,val_SIZE}
// 访问略

// 示例
var balance [10] int
balance := []int{1, 2, 5, 7, 8, 9, 3}
// 长度不满零值填充
balance := [10]int{1, 2, 5, 7, 8, 9, 3}
// 指定下标值,其余零值填充
balance := [10]int{1:2, 5:7}

多维

// 声明
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

// 示例(二维数组)
var a1 [3][4] int
var a2 = [3][4]int{
    {0, 1, 2, 3},   /*  第一行索引为 0 */
    {4, 5, 6, 7},   /*  第二行索引为 1 */
    {8, 9, 10, 11}, /*  第三行索引为 2 */
}

遍历参考[for 循环](#for 循环)

不指定长度的定义或引用,其实是切片(Slice)

作为形参

作为形参时,参数数组的长度必需与传入的数组一致

func method(arr [SIZE]type) [return_types] {}
// 没有长度的是切片
func method(arr []type) [return_types] {}

把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:

  • 传递数组的指针
  • 使用数组的切片

切片(Slice)

切片与数组的区别:

  1. 切片的类型字面量中只有元素的类型,没有长度
  2. 切片的长度可以随着元素数量的增长而增长(但不会随着元素数量的减少而减少)
  3. 切片是对数组的抽象
  4. 切片长度不固定,支持追加元素,支持动态扩容(数组长度不可变,切片就是为了解决这个问题)
  5. 切片未初始化之前会填充元素类型对应的零值,此时:len=cap
  6. 遍历参考[for 循环](#for 循环)
  7. 删除元素:通过切片的切片实现伪删除
    操作图示:Go Slice Tricks Cheat Sheet (ueokande.github.io)
  8. 数据共享:基于切片创建切片时(两者数组指针都指向同一个数组),修改任意一个切片都会影响到另一个切片(解决:使用append方法append方法会重新分配新的内存
  9. 切片是引用类型
// 声明
// 1、常规声明(相比数组不定义长度)
var identifier []type = []data_type{v1, v2, v3, ... , vN}
// 2、使用 make() 声明
var identifier []type = make([]type, len)
identifier := make([]type, len [,cap])
// 3、基于数组|切片(通过数组|切片截取产生)
identifier := predefined_array|predefined_slice[startInclude:endExclude]
identifier := predefined_array|predefined_slice[startInclude:]
identifier := predefined_array|predefined_slice[:endExclude]
identifier := predefined_array|predefined_slice[:]

// 元素追加
old_slice = append(numbers, 1,2,3,4)
// 追加另一个切片
old_slice = append(numbers, other_slice...)
// 拷贝(以较小的长度为准;都是复制前N个元素)
copy(numbers1,numbers)
// 元素删除
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 使用append创建新的slice会分配新的内存空间,可以避免数据共享问题
slice4 := append(slice3[:0], slice3[3:]...) // append实现,删除前三个元素
slice5 := append(slice3[:1], slice3[4:]...) // append实现,删除中间的三个元素
slice6 := append(slice3[:0], slice3[:7]...) // append实现,删除最后三个元素
slice7 := slice3[:copy(slice3, slice3[3:])] // copy实现,删除开头前三个元素

事实上,使用直接创建的方式来创建切片时,Go 底层还是会有一个匿名数组被创建出来,然后调用基于数组创建切片的方式返回切片,只是上层不需要关心这个匿名数组的操作而已。

所以,最终切片都是基于数组创建的,切片可以看做是操作数组的指针

指针&长度&容量解释:

指针:指向数组起始下标

长度:对应切片中元素的个数

容量:切片起始位置到底层数组结尾的位置(可分配的存储空间)

一个切片的容量初始值根据创建方式的不同而不同:

  • 对于基于数组和切片创建的切片,默认容量是从切片起始索引到对应底层数组的结尾索引
  • 对于通过内置 make 函数创建的切片,在没有指定容量参数的情况下,默认容量和切片长度一致

字典(Map)

  1. 使用hash实现,是一种无序的键值对集合
  2. 如果仅仅是声明,此时map = nil,不能赋值;必需初始化后才能进行赋值
    【初始化方式:make()或直接初始化】
  3. 声明mapkey类型时,要求数据类型必须是支持通过==!=进行判等操作的类型;为了提高性能,类型长度越短越好(通常设置为整型或长度较短的字符串)
    【底层使用哈希表实现;出现哈希冲突时,使用原始键判等】
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */ 
map_variable = make(map[key_data_type]value_data_type)

// 判断key是否存在于map中
// 如果exists=true,则value为对应的值
value, exists := map_variable[key]
// 删除元素
delete(map_variable, key)

// 示例
// 分别定义(或者 := 定义并初始化)
var testMap = map[string]int
// 此时 testMap == nil,不能添加键值对
// testMap["three"] = 3 // panic: assignment to entry in nil map	
// 1、直接初始化
testMap = map[string]int{
    "one":   1,
    "two":   2,
}
// 2、使用 make() 初始化
testMap = make(map[string]int)
testMap["one"] = 1
testMap["two"] = 2

指针与unsafe.Pointer

变量本质是对一块内存空间的命名,可以通过引用变量名来使用这块内存空间存储的值;指针则用来指向这些变量值所在的内存地址的值

  1. 指针变量通常缩写为ptr
  2. 赋值必需是对应变量的内存地址,即ptr = &var_name
  3. 在指针类型前面加上*号(val = *ptr,间接引用符)来获取指针所指向的内容
  4. 格式化输出时,可以通过 %p 来标识指针类型
  1. 为开发者提供操作变量对应内存数据结构的能力
  2. 提高程序的性能

指针指向的内存地址的大小是固定的,32位机器上占 4 个字节,64位机器上占 8 个字节
(与指向内存地址存储的值类型无关)

// 声明
var var_name *var_type
var var_name new(var_type)

// 示例
a := 100
var ptr *int      // 声明为指针类型 ==> 本身是一个内存地址值,需要通过内存地址进行赋值
ptr = &a          // 初始化指针类型值为变量a的内存地址(& 可以获取变量所在的内存地址)
fmt.Println(ptr)  // 内存地址:0xc0000aa058
fmt.Println(*ptr) // 该地址内存储的值:100

var ip *int        /* 指向整型 */
var fp *float32    /* 指向浮点型 */

空指针

  1. 当一个指针被定义后没有分配到任何变量时,它的值为nil,即空指针。
  2. 在概念上nil和其它语言的null、None、nil、NULL一样,都指代零值或空值。

unsafe.Pointer

  1. unsafe.Pointer是一个万能指针,可在任何指针类型之间做转化,绕过了Go的安全机制,
    是一个不安全的操作
  2. uintptr 是 Go 内置的可用于存储指针的整型,而整型是可以进行数学运算的!因此,将 unsafe.Pointer 转化为 uintptr 类型后,就可以让本不具备运算能力的指针具备了指针运算能力

是特别定义的一种指针类型,可以包含任意类型变量的地址

官方说明:

  1. 任何类型的指针都可以被转化为 unsafe.Pointer
  2. unsafe.Pointer 可以被转化为任何类型的指针;
  3. uintptr 可以被转化为 unsafe.Pointer
  4. unsafe.Pointer 可以被转化为 uintptr
// 规则 1 2
i := 10
var p *int = &i
// int 类型指针先转换为 unsafe.Pointer,再转换为 *float32
var fp *float32 = (*float32)(unsafe.Pointer(p))
*fp = *fp * 10
fmt.Println(i)  // i=100

// 规则 3 4
arr := [3]int{1, 2, 3}
ap := &arr
// unsafe.Sizeof 获取数组元素偏移量
// 获取到arr的指针,通过unsafe.Pointer转化为uintptr类型,再加上数组元素偏移量,
// 即得到该数组第二个元素的内存地址,后通过unsafe.Pointer将其转化为int类型指针赋值给sp指针,并进行修改
sp := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
*sp += 3
fmt.Println(arr) // arr=[1 5 3]

指针数组

var ptr [SIZE]*type

// 示例:整数型指针数组
var ptr [5]*int

指向指针的指针

一个指针变量存放的是另一个指针变量的地址

var ptr **type

// 示例
var ptr **int

访问指向指针的指针变量值需要使用两个*

func method() {
    a := 3000
	var ptr *int
	var pptr **int
	/* 指针 ptr 地址 */
	ptr = &a
	/* 指向指针 ptr 地址 */
	pptr = &ptr
	/* 获取 pptr 的值 */
	fmt.Printf("变量 a = %d\n", a)
	fmt.Printf("指针变量 *ptr = %d\n", *ptr)
	fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}

指针作为形参

func method(x, y *var_type) {
    // fixme 内部使用都需要满足 *x *y 格式(都是通过内存地址操作对应的值)
}

流程控制

条件语句

// if
if condition { 
    // do something 
}

// if...else...
if condition { 
    // do something 
} else {
    // do something 
}

// if...else if...else...
if condition1 { 
    // do something 
} else if condition2 {
    // do something else 
} else {
    // catch-all or default 
}
  • 条件语句不需要使用圆括号将条件包含起来 ()
  • 无论语句体内有几条语句,花括号 {} 都是必须存在的;
  • 左花括号 { 必须与 if 或者 else 处于同一行;
  • if 之后,条件语句之前,可以添加变量初始化语句,使用 ; 间隔,
    比如: if score := 100; score > 90 { work_code }

分支语句

Switch

  • 不需要用 break 来明确退出一个 case
    只有在 case 中明确添加 fallthrough 关键字,才会继续执行紧跟的下一个 case

  • 单个 case 中,可以出现多个结果选项(通过逗号,分隔)

  • 所有case候选值必需同switch变量(表达式)相同类型(否则编译错误)

  • 有两种写法

    //变量 var_name 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值
    // 1、精确匹配
    switch var_name {
        case val1:
        	// 匹配项中不需要添加 break(在下一个case出现之前当前case自动结束)
            ...
        case val2, val3:
        	// 合并分支;case 中可以存在多个 value
            ...
        case val4:
        	// 如果一个case中没有业务逻辑,Go认为这是一个空语句,会直接退出(也不会执行default)
        	// 如果希望当前case执行完成后继续执行下一个case,声明一个 fallthrough 即可
        case val5:
        	// 业务代码
        	...
        default:
            ...
    }
    // 2、条件匹配(不设定switch之后的表达式)
    var_name := some value
    switch {
        case condition(A):
        	// 业务代码
        case condition(B):
        	// 业务代码
        default:
        	// 默认处理
    }
    

Type Switch(fixme)

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型

switch x.(type){
    case type:
       statement(s)     
    case type:
       statement(s)
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s)
}

实例:

func method() {
    // 定义接口
    var x interface{}
	switch i := x.(type) {
	case nil:
		fmt.Printf(" x 的类型 :%T", i)
	case int:
		fmt.Printf("x 是 int 型")
	case float64:
		fmt.Printf("x 是 float64 型")
    // case 项也可以是 func
	case func(int) float64:
		fmt.Printf("x 是 func(int) 型")
    // 可以测试多个可能符合条件的值
	case bool, string:
		fmt.Printf("x 是 bool 或 string 型")
	default:
		fmt.Printf("未知型")
	}
}

Select(fixme)

  1. 与操作系统中的 select 比较相似
  2. switch 有相似的控制结构,但这些 case 中的表达式必须都是Channel的收发操作
  1. 是一个控制结构;
  2. select 随机执行一个可运行的 case;
  3. 如果没有 case 可运行,select 将阻塞,直到有 case 可运行;
  4. 默认的 default (如果存在)必需可以正常执行
select {
    case communication clause  :
       statement(s)      
    case communication clause  :
       statement(s)
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s)
}
  1. 每个 case 都必须是一个Channel
  2. 所有Channel表达式都会被求值
  3. 所有被发送的表达式都会被求值
  4. 如果任意某个Channel可以执行,它就执行;其他被忽略
  5. 如果有多个 case 可以运行,select 会随机公平的选出一个执行;其他被忽略
  6. 如果没有 case 可以运行:
    1. 存在 default ,执行 default
    2. 没有 default,阻塞直到某个Channel可以进行(Go 不会对 channel 或值进行求值)

select的知识点小结如下:

  1. select 语句只能用于信道的读写操作
  2. select 中的 case 条件(非阻塞)是并发执行的,select 会选择先操作成功的那个 case 条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的 case 语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行
  3. 对于 case 条件语句中,如果存在信道值为 nil 的读写操作,则该分支将被忽略,可以理解为从 select 语句中删除了这个 case 语句
  4. 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的 case ,则执行这个超时 case 。如果此段时间内出现了可操作的 case ,则直接执行这个 case 。一般用超时语句代替 default 语句
  5. 对于空的 select{} ,会引起死锁
  6. 对于 for 中的 select{} ,也有可能会引起 cpu 占用过高的问题

循环语句

  • 不支持 whiedo-while 结构的循环语句
  • 可以通过 for-range 结构对可迭代集合进行遍历
  • 支持 continuebreak 来控制循环
  • 支持高级的break停止指定循环:break labellabel是自定义的循环名称,同Java

循环类型

for 循环
// 三种形式,只有一种使用分号
// 1、与 C 的 for 同(同 Java ,没有括号)
for init; condition; post { }
for i := start; i < end; i++ { }
// 2、与 C 的 while 同
for condition { }
// 3、与 C 的 for(;;) 同
for { }

// 使用 break 结束循环

for-range】for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环

for key, value := range oldMap {
    newMap[key] = value
}

示例:

func method() {
    numbers := [6]int{1, 2, 3, 5}
    for i, x := range numbers {
		fmt.Printf("第 %d 位 x 的值 = %d\n", i, x)
	}
}
嵌套循环
// 同 Java ,没有括号
for [condition |  ( init; condition; increment ) | Range]
{
   for [condition |  ( init; condition; increment ) | Range]
   {
      statement(s)
   }
   statement(s)
}

跳转语句

break & contine

  • 通过 break 语句跳出循环,通过 continue 语句进入下一个循环
  • 高级的break停止指定循环:break labellabel是自定义的循环名称,同Java

goto(不建议使用)

可以无条件的转移到过程中指定的行

通常与条件语句配合使用,实现条件转移,构成循环、跳出循环的等功能

一般不建议使用goto,以免造成程序流程混乱,使程序难以理解或调试困难

面向对象

  1. 由一系列具有相同类型或不同类型的数据构成的数据集合(类似Java中的
  2. 没有 classextendsimplements 之类的关键字和相应的概念,借助结构体实现类的声明
  3. 不支持构造函数、析构函数,通过定义NewXXX这样的全局函数作为类的初始化函数
  4. 指针方法值方法(Go 语言不支持隐藏的 this 指针,所有的东西都是显式声明)
  5. toString实现:方法名固定为String;手动实现;无需显示调用
  6. 使用点号 (.) 操作符访问结构体成员,格式为:"结构体.成员名"

new() vs. make()

都在堆上分配内存,但行为不同,适用于不同的类型

  • new(T)为每个新的类型 T 分配一片内存,初始化为 0 并返回类型为 *T 的内存地址
    返回一个指向类型为 T,值为 0 的地址的指针,适用于值类型数组、结构体,相当于 &T{}
  • make(T)返回一个类型为 T 的初始值,只适用于三种内建的的引用类型:切片、map、channel

定义与初始化

// 结构体声明
type struct_variable_type struct {
   field1 type
   field2 type
   ...
   field3 type
}
// 结构体变量声明
var variable_name struct_variable_type
variable_name := structure_variable_type {value1, value2...valueN}
// 或者定义 NewXXX 方法


/* 结构体定义 */
type Circle struct {
  radius float64
}
func NewCircle(radius float64) *Circle{
    return &Circle(radius: radius)
}

成员方法

func 和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明)

// 该方法属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
  //c.radius 即为 Circle 类型对象中的属性
  return 3.14 * c.radius * c.radius
}
// 方法定义没错,但不属于 Circle 类型对象的方法
func getArea2(c Circle) float64 {
	return math.Pi * c.radius * c.radius
}
// 方法可以接收入参
func (c Circle) getPerimeter(x, y int) float64 {
	fmt.Println("x=", x, ",y=", y)
	return math.Pi * c.radius * 2
}
// circle.SetRadius1(100),circle的radius不会变
func (c Circle) SetRadius1(radius float64) {
    c.radius = radius
}
// circle.SetRadius2(100),circle的radius会设置为参数值
func (c *Circle) SetRadius2(radius float64) {
    c.radius = radius
}

func main() {
  var c1 Circle
  c1.radius = 10.00
  // 方法调用
  fmt.Println("Area of Circle(c1) = ", c1.getArea())
  // getArea2 不是 Circle 类对象的方法
  // fmt.Println("Area of Circle(c1) = ", c1.getArea2())
  area := getArea2(c1)
  fmt.Println("area = ", area)
  fmt.Println("Perimeter of Circle(c1) = ", c1.getPerimeter(10, 15))
  // c1 属性值不变
  c1.SetRadius1(100)
  // c1 属性值正常修改
  c1.SetRadius2(100)
}

指针方法:接收者类型为指针的成员方法(如:func (c *Circle) SetRadius(){}

值方法:接收者类型为非指针的成员方法(如:func (c Circle) SetRadius(){}
(传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部结构体实例的值拷贝,修改不会作用到外部结构体实例)

区别:

  1. 归属于struct_variable_type的成员方法只是该类型下所有可用成员方法的子集
    归属于*struct_variable_type的成员方法才是该类型下的完整可用方法集合
  2. 调用指针方法时,Go 底层会自动将struct_variable_type转换为对应的指针类型*struct_variable_type,即(&struct_variable_type).method()
  • 自定义数据类型的方法集合中仅会包含它的所有「值方法」
  • 该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」
  • 指针方法可以修改所属类型的属性值,而值方法则不能

组合实现继承与方法重写

封装:结构体

继承:组合(将一个类型嵌入另一个类型,构建新的类型结构)
推荐使用指针方式实现继承,组合指针类型性能更好

多态:方法重写
(组合的不同类型包含同名方法,若子类没有重写则无法直接调用【只能显式调用】)

type Animal struct {
	Name string
}
type Pet struct {
    Name string
}
func (a Animal) Call() string {
	return "Animal的叫声..."
}
func (a Animal) GetName() string {
	return a.Name
}
func (p Pet) GetName() string {
	return p.Name
}
// 通过组合实现继承
type Dog struct {
    // 设置别名
	animal Animal
    // 以指针方式继承某个类型的属性和方法(调用不变;性能更好)
    *Pet
}
// ”继承“实现方法重写
func (d Dog) Call() string {
	return "汪汪汪。。。"
}

func main() {
    animal := models.Animal{Name: "中华田园犬"}
	dog := models.Dog{Animal: animal}
    // 调用重写的方法
	fmt.Println(dog.Call())
    // 调用”父类“方法
	fmt.Println(dog.Animal.Call())
    // 组合的多个结构体拥有同名方法且子类没有重写的话,只能够显式调用
    //fmt.Println(dog.GetName())
    fmt.Println(dog.Pet.GetName())
    fmt.Println(dog.Animal.GetName())
}

属性&成员方法可见性

  1. 不是传统的面向对象编程语言的可见性,而是基于的维度

  2. 包与文件系统的目录结构存在映射关系

  3. 归属同一个包的 Go 代码具备以下特性:

    • 归属于同一个包的源文件包声明语句要一致,即同一级目录的源文件必须属于同一个包;
    • 在同一个包下不同的源文件中不能重复声明同一个变量、函数和类(结构体);
  4. main 函数作为程序的入口函数,只能存在于 main 包中

  5. Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的
    (所有变量、函数、自定义类属性或成员方法,可见性都根据其首字母大小写来决定。大写则包外可访问)

接口

如果说 goroutine 和 channel 是支撑起 Go 语言并发模型的基石,那么接口就是 Go 语言整个类型系统的基石

  1. 侵入式&非侵入式
    侵入式接口:实现类必须明确声明自己实现了某个接口(如:Java
    非侵入式接口:类与接口的实现关系不通过显式声明,而是系统根据两者的方法集合进行判断
  2. Go 从设计上避免了侵入式接口
  3. 一个类只要实现了某个接口要求的所有方法,我们就说这个类实现了该接口(不用显式声明)
    (如果一个接口的方法集合是某个类成员方法集合的子集,我们就认为该类实现了这个接口)
  4. 通过关键字 interface 来声明接口
  5. 通过组合实现接口继承
  6. 组合的情况下,只有实现了全部接口定义才会判定为完整实现(否则为单接口实现或不实现)
    (接口的实现不是强制的,是根据类实现的方法来动态判定的)
  7. interface{}是一个空接口,可以用于表示任意类型
    (范围太宽泛了,需要在运行时通过反射对数据进行类型检查)
// 接口定义
type IFile interface {
	Read(buf []byte) (n int, err error)
}
// 接口实现
type File struct {
}
func (f *File) Read(buf []byte) (n int, err error) {
	return 0, nil
}

// 继承 & 完整实现
type A interface {
	Foo()
}
type B interface {
	A
	Bar()
}
type T struct {}
//func (t T) Foo() {}
// 只有同时实现组合类中的所有方法(包含依赖类)才会被判定为实现
func (t T) Bar() {}
  1. 如果实现类中的成员方法都是值方法,进行接口赋值时,传递类实例的值类型或指针类型均可,否则只能传递指针类型实例
    从代码性能角度来说,值拷贝需要消耗更多的内存空间,统一使用指针类型代码性能会更好
type Integer int

type Math interface {
    Add(i Integer) Integer
}
// 这种情况属于是 Integer 实现了接口,可以通过 Integer 或 &Integer 调用
type (i Integer) Add(b Integer) Integer {
    return 
}
// 这种情况属于是 *Integer 实现了接口,只能通过 &Integer 调用
type (i *Integer) Add(b Integer) Integer {
    return 
}

func main() {
    var a Integer = 1
    // 将类实例赋值给接口
    // 值类型实现方法,可以通过值或指针类型调用
    var m1 Math = a
    // 指针类型实现方法,只能通过指针类型调用
    var m1 Math = &a
}
  1. 在 Go 语言中,只要两个接口拥有相同的方法列表(与顺序无关),那么它们就是等同的,可以相互赋值
    (前提:接口变量持有的是基于对应实现类的实例值,即接口与接口间的赋值基于类实例与接口间的赋值)

类型断言

实现方式:

  1. .(type)type 对应的就是要断言的类型,一般用于基本数据类型
  2. 反射:reflect包提供的TypeOf函数,一般用于结构体

类型断言是否成功需要在运行期才能够确定

断言语法.左边的变量必需是接口类型(建议使用空接口转换,避免引入多余的接口定义)

Go中的父类与子类(不确定官方是否也这么称呼)与面向对象编程语言(如Java)中的概念完全不同,决不能够映射过去理解

Go语言结构体类型的断言,即使子类和父类属性名和成员方法列表完全一致,子类的实例并不归属于父类;
同理,父类实现了某个接口,并不代表组合类的子类也实现了这个接口

Go 使用组合而非继承来构建类与类之间的层级关系,所以子类实例并不是同时父类类型

type Integer int
type Number interface {}

// 接口断言
var i Integer = 1
var n Number = &i
// n 必须是接口才能进行类型断言
if _, ok := n.(Number); ok {
    // 只能在运行期间确定结果
}

// 子类的实例并不是父类实例
type IAnimal interface {}
type Animal struct {}
type Dog struct {
    animal *Animal
    name string
    // 同时 Dog 实现接口 IAnimal
}
// var dog IAnimal = dog
// 引入空接口,避免引入冗余接口
var dog interface{} = dog
// ok=true,类型断言:满足接口及自身类型
_, ok := dog.(IAnimal|Dog) 
// ok=false,类型断言:”子类型“并不是”父类型“  ==》 组合
_, ok := dog.(Animal) 

// 使用反射获取实际类型
func myPringf(args ...interface{}) {
    for _, arg := range args {
        switch reflect.TypeOf(arg).Kind() {
            case reflect.Int:
            // codes
            case reflect.Int:
            // codes
        }
    }
}
// 通过 reflect.TypeOf(arg) 可获取真实类型

空接口、反射、泛型

Go 打破了传统面向对象编程中类与类之间继承的概念,通过组合实现方法和属性的复用,也不存在类似的继承关系树,也没有所谓的祖宗类(如:java.lang.Object);接口与实现也没有关键字进行约

空接口

  1. 类与接口的实现关系是通过类所实现的方法在编译期推断出来的
  2. 所有类都实现了空接口,反过来,空接口也可以指向任意类型
  3. 最典型的应用场景是声明函数支持任意类型的参数
// 空接口可以指向任意类型
var v1 interface{} = "这里可以是任意类型"

func method(args ...interface{}) {}

反射

常用的,分别可以通过 reflect.TypeOfreflect.ValueOf 函数获取变量的类型与存储任何类型的值

反射的解析在运行时完成,对性能有一定影响;如非必须,尽量不要使用反射

// 通过反射获取成员变量、方法及执行方法
// 获取类型值:如果包含指针方法则使用如下方法获取类型值
//dogValue := reflect.ValueOf(&dog).Elem()
dogValue := reflect.ValueOf(dog)
// 获取所有属性和成员方法
for i := 0; i < dogValue.NumField(); i++ {
    fmt.Println("name:", dogValue.Type().Field(i).Name)
    fmt.Println("type:", dogValue.Type().Field(i).Type)
    fmt.Println("value:", dogValue.Field(i))
}
// 获取所有方法并执行
for i := 0; i < dogValue.NumMethod(); i++ {
    fmt.Println("name:", dogValue.Type().Method(i).Name)
    fmt.Println("type:", dogValue.Type().Method(i).Type)
    fmt.Println("exec result:", dogValue.Method(i).Call([]reflect.Value{}))
}

泛型(空接口&反射)

当前版本go version go1.17.2官方还未支持泛型

fixme TODO

空结构体

struct {}

该类型实例值只有一个,即struct{}{},且 Go 程序中永远只会存一份,且占据的内存空间是0

典型应用:通道(channel)作为传递简单信号的介质时使用空结构体进行声明

错误处理

error 类型

  1. 标准模式,error接口
  2. 自定义错误:组合error接口并实现Error()方法
// 「卫述语句」 模板
n, err := Foo(0)
if err != nil { 
    // 错误处理 
} else {
    // 使用返回值 n 
}

// 构建错误实例
err := errors.New('错误信息')
// fmt.Errorf() 格式化错误信息

// 自定义错误
type PathError struct {
	Op   string
	Path string
	Err  error
}
func (pe PathError) Error() string {
	return pe.Op + pe.Path + pe.Err.Error()
}

panic & recover & defer

类比:

panic recover defer组合起来实现了面向对象编程中的try...catch...finally功能

一个完整的示例:

func divide() {
	// 通过 defer 提前定义兜底逻辑(先入后出;不论是否发生 panic 都会执行)
	defer func() {
		// 通过 recover 捕获 panic(当前函数退出执行,回到调用的地方继续)
		if err := recover(); err != nil {
			fmt.Printf("Runtime panic caught: %v\n", err)
		}
		// 不使用 recover 恢复的话,整个程序会直接停止
		fmt.Println("程序异常")
	}()
	var i = 1
	var j = 0
	// 抛出 panic 的函数(手动或默认)
    if j == 0 {
		panic("参数异常")
	}
	var k = i / j
	fmt.Printf("%d / %d = %d\n", i, j, k)
}
func main() {
    // 执行可能发生 panic 的业务
    divide()
    // 恢复以后继续执行
	fmt.Println("继续执行main函数")
}

panic

相当于是Go语言版的异常(类比Java中的try

当代码运行异常且又没有在编码时显式返回错误时,Go 会抛出panic,或「运行时恐慌」

panic函数支持的入参是interface{}

遇到panic的执行逻辑:

  1. 中断当前协程后续代码执行
  2. 执行终端代码之前定义的defer语句(按先入后出顺序)
  3. 程序退出并输出panic错误信息
func main() {
    i := 1
    j := 0
    if j == 0 {
        // 手动抛出 panic
        panic("除数不能是0")
    }
}

recover

通过recover()函数对panic进行捕获和处理(类比Java中的catch

defer中捕获panic运行时恐慌,defer执行完成后,退出抛出panic的当前函数再回到调用它的地方继续执行后续代码

defer

  1. 用于释放资源或程序运行过程中抛异常执行的兜底逻辑(似Java中的finally;不论是否异常都会执行)

  2. 可以是简单的一行语句或使用匿名函数

  3. 一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则
    (最后一个 defer 语句将最先被执行;即使在循环中,依然遵循先进后出)

  4. 尽量在函数/方法的前面定义defer,避免遗漏

  5. 抛出异常后,Go 会中断后续代码的执行,因此定义在异常代码以后的defer不会执行

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    // 在函数执行完成或执行抛异常时执行
    defer f.Close()
    defer func () {
        // 一条语句无法执行的动作可使用匿名函数实现
    }
}

变量

  1. init函数在main函数之前执行

    func init() {
        fmt.Println("This is init function")
    }
    
    func main() {
        fmt.Println("This is main function")
    }
    
    // 输出内容
    // This is init function
    // This is main function
    
  2. 不同类型的值不能使用==!=运算符比较

  3. 标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,那使用这种标识符的对象就可以被外部包的代码所使用(客户端需导入),这被称为导出
    (类似于面向对象中的public);

  4. 标识符如果以小写字母开头,则对包外不可见,但在整个包的内部是不可见且可用的
    (类似于面向对象中的private

  5. 一行代表一个语句结束(不需要以分号;结束,由编译器自动完成)
    如果打算将多个语句写在同一行,则必须使用;进行区分(不建议使用

  6. 变量

    1. 全局变量与局部变量可以同名,参考全局变量

    2. 类型推导动作在编译期完成 ==> Go是静态语言

    // 变量声明
    var identifier type
    // 1、指定变量类型(不赋值则使用类型默认值)
    var v_name v_type
    var v_name v_type = value
    // 2、类型推导
    var v_name = value
    // 3、省略 var,使用 := 进行声明;只能用于声明局部变量
    v_name := value
    
    
    // 下面使用 := 的定义是正确的
    var outer = true
    func main() {
        // a、打印方法外定义的变量定义
        fmt.Println(outer)
        // b、如果是全局变量,可以进行重新定义(包括类型可以不一样)(fixme 应该不是同一个变量了)
        outer := "使用 := 重新定义变量"
    	fmt.Println(outer)
        // c、如下定义不能编译通过
        // var inner [string] = "局部变量"
    	// fmt.Println(inner)
    	// inner := "使用 := 重新定义局部变量无法编译通过"
    	// fmt.Println(inner)
    }
    
    
    // 局部变量:同类型多个变量
    // 1、指定类型(三个变量类型一致;可以是全局变量)
    var vname1,vname2,vname3 type
    vname1,vname2,vname3 = v1, v2, v3
    // 2、类型推导(可以是不同的类型;可以是全局变量;并行|同时赋值)
    var vname1,vname2,vname3 = v1, v2, v3
    // 3、初始化声明(可以是不同的类型;变量必须不能是已经在方法内部声明过的;并行|同时赋值)
    vname1,vname2,vname3 := v1, v2, v3
    // 全局变量:类型不同的多个变量声明(只能是全局变量)
    var (
    	v_name_1 type1
        v_name_2 type2
    )
    
  7. 值类型与引用类型

    1. 值类型
      1. 所有像intfloatboolstring这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
      2. 当使用等号=将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
      3. 可以通过&i来获取变量 i 的内存地址(取址符)
      4. 值类型的变量的值存储在栈中
    2. 引用类型
      1. 更复杂的数据通常会需要使用多个值,这些数据一般使用引用类型保存
      2. 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个值所在的位置(这个内存地址也称指针
      3. 同一个引用类型的指针指向的多个值在内存中可以是连续的,也可以是分散的
  8. 局部变量禁止只声明不使用;全局变量允许只声明不使用

    var outer string = "全局变量允许只声明不使用"
    func main() {
        // Unused variable 'inner'
        var inner = "局部变量禁止只声明不使用"
    }
    
  9. 如果想要简单的交换两个变量的值,可以使用a, b=b, a

  10. 空白标识符_也被用于抛弃值,如值 5 在_, b := 5, 7中被抛弃
    _ 实际上只是一个可写变量,不能获取其值
    (Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值)

  11. 并行赋值也被用于当一个函数返回多个返回值

    val, err = Func1(var1)
    

常量

  1. 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

  2. 不能出现任何需要运行期才能获取结果的表达式(无需担心常量之间的类型转换,它们都是非常理想的数字)

  3. 定义格式

    // 类型说明符可省略
    const identifier [type] = value
    // 支持同时定义多个同类型变量
    const c_name_1, c_name_2 = value1, value2
    
    
    // 可以定义同名的局部和全局常量
    const a1, a2, a3 = "a1", 23, true
    func method() {
        // 这么定义不会报错
        const a1, a2 = "aa1", "aa2"
        // 优先输出局部变量定义,没有则输出全局变量值
        fmt.Println(a1, a2, a3)
    }
    
  4. 常量还可以用作枚举

    const (
    	Unknown = 0
    	Female  = 1
    	Male    = 2
    )
    
  5. 常量定义中可以使用len()cap()unsafe.Sizeof()计算表达式的值(必需是内置函数

    const (
    	a = "abc"
    	b = len(a)
    	c = unsafe.Sizeof(a)
    )
    
  6. iota,特殊常量,可以认为是一个可以被编译器修改的常量
    在每一个const关键字出现时,被重置为0,然后再下一个const出现之前,每出现一次iota,其所代表的数字就会自动增加1

    // 定义一
    const (
    	a = iota
    	b = iota
    	c = iota
    )
    // 接上定义二
    // const出现,值被重置
    const (
    	d = iota
    	e = iota
    	f = iota
    )
    // a=0,b=1,c=2
    // d=0,e=1,f=2
    // 简写为
    const (
    	a = iota
    	b
    	c
    )
    
  7. 复杂一些的用法

    // 示例一
    const (
    	a = iota // a=0
    	b        // b=1
    	c        // c=2
    	d = "ha" // d="ha",iota+=1=3
    	e        // e="ha",iota+=1=4
    	f = 100  // f=100,,iota+=1=5
    	g        // g=100,,iota+=1=6
    	h = iota // 恢复计数 h=7
    	i        // i=8
    )
    // 示例二 
    const (
    	i = 1 << iota // i=1<<0=1
    	j = 3 << iota // j=3<<1=6
    	k             // k=3<<2=12
    	l             // l=3<<3=24
    )
    

运算符

  • 算术运算符

    +-*/%++--

    %(取余运算只能用于整数)

    不同类型的整型值不能直接进行算术运算,必需先转化相同类型再执行计算

    自增/自减运算符,只能作为语句,不能作为表达式,且只能用作后缀,不能放到变量前面

    func method() {
        a := 10
    	b := 20
    	var c int
        
        c = a + b
    	c = a - b
    	c = a * b
    	c = a / b
    	c = a % b
        // 等效于:a=a+1
    	a++
        // 等效于:a=a-1
    	a--
        
        // 没有 ++a 或 --a 的操作
    }
    
  • 关系运算符

    ==!=><>=<=

    各种类型的整型变量都可以直接与字面常量进行比较

    var intVal1 int8 = 1
    // 字面常量为 int
    if intVal1 == 8 {
        fmt.Println("intValue1 = 8")
    }else {
        fmt.Println("intValue1 != 8")
    }
    
  • 逻辑运算符

    &&||!

    1. 没有单与、单或
    2. go的风格是不必将逻辑运算符计算使用括号包裹
    func method() {
        a := true
        b := true
        // go 的风格是不必将逻辑运算使用括号包裹
        if a || b {
    		fmt.Println("a||b is true")
    	}
        // 逻辑运算符中没有单与、单或
        // if a | b{}
    }
    
  • 位运算符

    可以对整数在内存中的二进制位进行操作

    &|^

    运算符 说明
    & 与运算,全真为真
    ` `
    ^ 异或运算,相同为假,不同为真
    << 左移运算符,左移n位就是乘以2的n次方(高位丢弃,低位补0)
    >> 右移运算符,右移n位就是除以2的n次方
  • 赋值运算符

    运算符 说明 实例
    = 将一个表达式的值赋给一个左值 C = A + B
    += 相加后再赋值 C += A 等于 C = C + A
    -= 相减后再赋值 C -= A 等于 C = C - A
    *= 相乘后再赋值 C *= A 等于 C = C * A
    /= 相除后再赋值 C /= A 等于 C = C / A
    %= 求余后再赋值 C %= A 等于 C = C % A
    <<= 左移后赋值 C <<= 2 等于 C = C << 2
    >>= 右移后赋值 C >>= 2 等于 C = C >> 2
    &= 按位与后赋值 C &= 2 等于 C = C & 2
    ^= 按位异或后赋值 C ^= 2 等于 C = C ^ 2
    ` =` 按位或后赋值
  • 其他运算符

    运算符 说明 实例
    & 变量内存地址 &var_name返回变量的实际地址
    *(fixme) 指针变量 *var_name是一个指针变量
  • 运算符优先级(由高到低)

    优先级 运算符
    7 ^ !
    6 * / % <> & &^
    5 + - | ^
    4 == != < = >
    3 <-
    2 &&
    1 ||

函数

  1. 最少有一个 main 函数
  2. 函数名称,参数列表和返回值(类型,个数,顺序)一起构成了函数签名
  3. 标准库提供了多种可动用的内置的函数(存在于builtinunsafe标准库)
    Go 语言内置函数可以参考 built 包文档
  4. 函数本身也是 Go 的一种数据类型

分为三种:

  • 普通函数
  • 匿名函数(闭包)
  • 类方法

定义:

// 1、可以返回多个值
// 2、如果没有返回值,return_types 可缺省  
// 3、入参类型相同可使用简单定义
func function_name( [parameter list] ) [return_types] {
   // function body
}
func function_name( [parameter list] ) (type1,type2,...,typeN) {
   // function body
}

func max(a, b int) int {
    // 方法体省略
    return ret
}
func swap(x, y string) (string, string) {
	return y, x
}

类方法:相当于Java中的一个类拥有的自定义方法

函数参数

按值传参

Go 语言默认使用按值传参来传递参数,也就是传递参数值的一个副本,在调用过程中不会影响到实际参数

引用传参

可以实现在函数中修改形参值的同时修改实参值

默认使用引用传参的类型:切片(slice)、字典(map)、接口(interface)、通道(channel)等

传递给函数的参数是一个指针,而指针代表的是实参的内存地址,修改指针引用的值即修改变量内存地址中存储的值,所以实参的值也会被修改

(这种情况下,传递的是变量地址值的拷贝,所以从本质上来说还是按值传参)

引用传递的一个例子:

// 引用传递入参及方法体有所不同
func swap(x *int, y *int) {
	var tmp int
	tmp = *x
	*x = *y
	*y = tmp
}

变长参数

在参数类型前加上 ... 前缀(只能作为形参存在,且必须是最后一个)

//定义:numbers 为变长参数
func myfunc(vars ...data_type) {
    for _, val := range vars {
        fmt.Println(val)
    }
}
// 调用:加后缀表明变长参数
slice := []data_type{v1,v2,v3}
myfunc(slice...)

任意类型的变长参数(泛型)

指定变长参数类型为 interface{}

Go 语言并没有在语法层面提供对泛型的支持(当前go version = go1.17.2 windows/amd64)

interface{}是一个空接口,可以用于表示任意类型
(范围太宽泛了,需要在运行时通过反射对数据进行类型检查)

(多)返回值

  • 多返回值
  • 命名返回值
// 多返回值之间通过英文逗号分隔;使用括号包裹
func method(var1 type,var2 type) (return_type1,return_type2) {
    // work code
}

// 命名返回值
func method(var1 type,var2 type) (ret type, err error) {
    // 在函数中直接对返回变量进行赋值
    // work code
    // return 后面不用再写返回变量
}

func add(a, b *int) (c int, err error) {
	if *a < 0 || *b < 0 {
		err = errors.New("Error message")
		return
	}
	c = *a + *b
	return
}

匿名函数与闭包

// 1、将匿名函数赋值给变量
add := func(a, b int) int {
    return a + b
}

// 调用匿名函数 add
fmt.Println(add(1, 2))  

// 2、定义时直接调用匿名函数
func(a, b int) {
    fmt.Println(a + b)
} (1, 2) 

闭包:引用了自由变量(未绑定到特定对象的变量,通常在函数外定义)的函数,被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的上下文环境也不会被释放(比如传递到其他函数或对象中)
「闭」的意思是「封闭外部状态」,即使外部状态已经失效,闭包内部依然保留了一份从外部引用的变量

闭包只能通过匿名函数实现,我们可以把闭包看作是有状态的匿名函数
反过来,如果匿名函数引用了外部变量,就形成了一个闭包(Closure)

  1. 匿名函数作为形参
// 匿名函数作为参数传递
add := func(a, b int) int {
    return a + b
}
// 立即调用;关键字:call;函数对应的定义是:func(int, int) int
func(func_name func(int, int) int) {
    fmt.Println(func_name(1, 2))
}(add)

// 匿名函数作为参数传递:抽离匿名函数
func main(){
    add := func(a, b int) int {
		return a + b
	}
    handleAdd(1, 2, add)
}
// 函数对应的定义是:func(int, int) int
func handleAdd(a, b int, func_name func(int, int) int) {
	fmt.Println(func_name(a, b))
}
  1. 匿名函数作为返回值
    可以通过将函数返回值声明为函数类型来实现业务逻辑的延迟执行
func main() {
    // 获取返回的匿名函数
    addFunc := deferAdd(1, 2)
    // 执行真正的函数动作
	fmt.Println(addFunc())
}

// 匿名函数对应的定义是:func() int
func deferAdd(a, b int) func() int {
	return func() int {
		return a + b
	}
}
  1. 高阶函数:接收其他函数作为形参,或者把其他函数作为结果返回的函数

Map-Reduce-Filter 模式

准确的说是一种处理思想(不与固定场景绑定)

多用于处理集合

Map-Reduce-Filter 并不是一个整体,而是要分三步实现:Filter、Map 和 Reduce(以字典类型切片为例)

  1. 首先将字典类型切片按照条件过滤,即Filter
  2. 再将过滤后的字典类型切片转化为一个字符串类型切片(Map,字面意思就是映射)
  3. 最后再将转化后的切片元素转化为目标类型执行计算
    Reduce,字面意思就是将多个集合元素通过迭代处理减少为一个)

示例:

// 计算一个 []map[string]string 中 age 字段的累加
// 1、过滤
func itemsFilter(items []map[string]string, f func(map[string]string) bool) []map[string]string {
	newSlice := make([]map[string]string, len(items))
	for _, item := range items {
		if f(item) {
			newSlice = append(newSlice, item)
		}
	}
	return newSlice
}
// 2、映射
func mapToString(items []map[string]string, f func(map[string]string) string) []string {
	newSlice := make([]string, len(items))
	for _, item := range items {
		newSlice = append(newSlice, f(item))
	}
	return newSlice
}
// 3、计算
func fieldSum(items []string, f func(string) int) int {
	var sum int
	for _, item := range items {
		sum += f(item)
	}
	return sum
}

管道 & 流式编程

func SumAge(users []User, pipes ...func([]User) interface{}) int {
	var ages []int
	var sum int
    // 传入函数切片
	for _, f := range pipes {
		result := f(users)
		// fixme result.(type)
		switch result.(type) {
		case []User:
            // 判断结果类型并更新形参
			users = result.([]User)
		case []int:
			ages = result.([]int)
		}
	}
	if len(ages) == 0 {
		log.Fatalln("没有在管道中加入 mapAgeToSlice 方法")
	}
	for _, age := range ages {
		sum += age
	}
	return sum
}

变量用域

变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

局部变量

  1. 在函数体内声明
  2. 作用域只在函数体内
  3. 参数和返回值变量也是局部变量

全局变量

  1. 在函数体外声明
  2. 可以在整个包甚至外部包(被导出后)使用

全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑

形式参数(形参)

出现在函数/方法形参表中的变量(当作局部变量使用)

初始化局部和全局变量

数据类型 初始化默认值
int 0
float32 0
pointer(fixme) nil

语言范围

range关键字用于for循环中迭代数组array、切片slice、链表channel或集合map等元素

个人理解:使用for循环遍历数组、切片、链表或集合用到的一个关键字

Range表达式 第一个值 第二个值[可选的]
Array 或者 slice a [n]E 索引 i int a[i] E
String s string type 索引 i int rune int
map m map[K]V 键 k K 值 m[k] V
channel c chan E 元素 e E none
// 迭代string时,index为索引,value为字符(Unicode值)
for index|key, value := array|slice|channel|map|string {
    // 业务代码
}

类型转换

  1. 将一种数据类型的变量转换为另外一种类型的变量
  2. 不支持隐式类型转换
// type_name 为类型,expression 为表达式
type_name(expression)

i := 32
float32(i)

//var a int32 = 3
//var b int64
//b = a
//fmt.Printf("b 为 : %d", b)

参考资料:

posted @ 2021-11-04 15:43  yangcxx  阅读(141)  评论(0)    收藏  举报