Go(geekr.dev)学习一

本文详细介绍了Go语言中的各种数据类型,包括整型、浮点型、复数、字符串、切片、数组、指针、字典以及基本的流程控制结构。特别强调了切片与数组的区别,以及指针和 unsafe.Pointer 的使用。此外,还涵盖了循环语句、条件语句以及循环控制语句。
摘要由CSDN通过智能技术生成

数据类型

数字类型

类型描述
int8/int16/int32/int64有符号整数
uint8/uint16/uint32/uint64无符号整数
float32/float64IEEE-754 32/64位浮点型数;
与Java的对应:float32 => float;float64 => double
自动类型推导为 float64
开发中建议尽量使用 float64,因为 math 包下面的计算都是用此类型
byte类似uint8
rune类似 int32
uint32或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/complex12832/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,以免造成程序流程混乱,使程序难以理解或调试困难

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值