GO语言基础语法 | 青训营笔记

94 阅读4分钟

go语言变量

Go中变量名有字母、数字、下划线组成,首个字符不能为数字。

 // 声明变量
 var identifier type
 // 同时命名多个变量
 var indetifier1, identifier2 type 
指定变量类型,没有初始化,变量默认零值
  • 数值类型(包括complex64/128) 为0

  • 布尔类型为false

  • 字符串为""

  • 以下类型为 nil

     var a *int
     var a []int
     var a map[string]int
     var a func(string)int
     var a error
    
根据值自行判断变量类型
 var v_name = value 
:= 声明变量
 v_name := value
 ​
 var intVal int
 intVal = 1
 //相当于
 intVal :=1
多变量声明
 var vname1, vname2, vname3 type
 vname1,  vname2, vname3 = v1, v2, v3
 ​
 //因式分解关键字的写法一般用于声明全局变量
 var(
   vname1 v_type1
   vname2 v_type2
 )

空白标识符_ 被用于抛弃值,是一个只写变量,不可读。

变量的生命周期

全局变量生命周期是程序存活时间

局部变量是函数存活时间

条件语句

switch中,每个case默认break。 如果希望从某个case顺序往下执行,可以使用fallthrough关键字。

数组与切片

如果初始化切片的长度不为0, 那么切片此时存的是 零值。如果是string类型的切片,那么为空串。

go语言循环

go中循环只有for一种。

for循环的形式
  • 类似c语言的for循环

     for init; condition; post { }
    
  • 类似c语言的while

     for condition { }
    
  • 类似C语言的 for(; ;)

     for { }
    
    • Init : 一般为赋值表达式, 给控制变量赋初始值
    • condition: 关系表达式或逻辑表达式, 循环控制条件
    • post: 为赋值表达式,给控制变量增量或减量。
     for i := 0; i < 10; i++{
       fmt.Println(i)
     }
    
for range

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

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

key 和 value 可以省略

 //只读取key
 for key := range oldMap{
 ​
 }
 //只读取value
 for _, value := range oldMap{
 ​
 }
 ​
for range的问题
  • for range 获取不到所有元素的地址

     arr := [2]int{1, 2}
     res := []*int{}
     for _, v := range arr{
       res = append(res, &v)
     }
     fmt.Println(*res[0], *res[1]) //expect 1 2
     //实际这里输入的是 2 2
    

    可以看到for-range 其实是语法糖, 内部还是for循环, 初始化会拷贝要遍历的列表(如array、slice、map), 每次遍历的v都是对同一元素的遍历赋值,也就是如果对v取地址, 最后只会是同一个地址,对应的值就是遍历的元素赋值给v的值。

    go编译源码

     // len_temp := len(range)
     // range_temp := range
     // for index_temp = 0; index_temp < len_temp; index_temp++ {
     //     value_temp = range_temp[index_temp]
     //     index = index_temp
     //     value = value_temp
     //     original body
     //   }
    

    如果想要得到预期结果

     // 使用局部变量v1拷贝v
     for _, v := range arr{//局部变量v1替换了v,也可用别的局部变量名
       v1 := v
       res = append(res, &v1)
     }
     //直接使用索引获取原来的元素
     for k := range arr{
       res = append(res, &arr[k])
     }
    
  • 循环是否会停止?

     v := []int{1,2,3}
     for i := range v{
       v = append(v, v[i])
     }
    

    在循环遍历的同时往遍历的切片中追加元素,循环会停止么?

    答案:

    for range 在遍历前对 遍历的数组进行了拷贝,所以期间对原来数组的修改不会影响到遍历中。

     a := []int{1, 2, 3}
     ​
     temp := a
     lenTemp := len(temp)
     ​
     for index := 0; index < lenTemp; index++{
       valueTemp := temp[index]
       _ = index
       value := valueTemp
       
       a = append(a, value)
       fmt.Println("len(a)=", len(a))
     }
    

    使用for range 在对切片a遍历的时候, 显示将a复制给一个临时变量temp,然后根据temp的长度做遍历,最后在append的时候将取到value追加到原切片a中。

  • 对大数组遍历的问题

     // 假设值都为1, 这里只赋值3个
     var arr = [204800]int{1, 1, 1}
     for i, n := range arr{
       _ = i
       _ = n
     }
    

    因为for range时,其实是对原数组做了一次深拷贝, 但是像这种大数组拷贝一次,对内存是极大浪费,应该做如何优化?

    1. 对数组取地址遍历 for i,n := range &arr,因为数组去的是地址,不是整个数组,所以在拷贝的时候是拷贝的地址,比整个数组小的多。
    2. 对数组做切片引用for i, n := range arr[:]
  • 对大数组重置的效率问题

     // 假设值都为1, 这里只赋值3个
     var arr = [204800]int{1, 1, 1}
     for i, n := range arr{
       arr[i] = 0
     }
    

    重置的效率更高

    go对这种重置元素值为默认值的遍历是有优化。go源码

  • 对map遍历时删除元素能遍历到么?

     var m = map[int]int{1:1, 2:2, 3:3}
     var once sync.Once
     ​
     for i := range m{
       once.Do(func(){
         for _, key := range []int{1,2,3}{
           if key != i{
             fmt.Printf("当前 i:%d, del key:%d\n", i, key)
             delete(m, key)
             break
           }
         }
       })
       fmt.Printf("%d%d\n", i, m[i])
     }
     ​
     //结果
     当前 i:1, del key:2
     11
     33
    

    map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置,如果删除的元素开始没被遍历到,后边就不会出戏啊 。

  • 对map遍历时 新增元素能遍历到么?

     var m = map[int]int{1:1, 2:2}
     for i, _ := range m{
       m[4] = 4
       fmt.Printf("%d%d\n", i, m[i])
     }
     ​
    

    可能会,map的hash表初始化时会随机一个遍历开始的位置。

  • for rangez中 这样起goroutine 能得到想要的结果么?

     var m = []int{1,2,3}
     for i := range m{
       go func(){
         fmt.Print(i)
       }()
     }
     // 阻塞1分钟 等待所有goroutine运行完
     time.Sleep(time.Millisecond)
     ​
     //答案
     2 2 2
    

    可能是因为当for 循环执行完之后, goroutine才开始执行,这时val的值指向切片中最后一个元素,所以三个goroutine打印出来的结果相同。

    • 以参数方式传入

       for i := range m{
         go func(i int){
           fmt.Print(i)
         }(i)
       }
      
    • 使用局部变量拷贝

       for i := range m{
         j := i
         go func(){
           fmt.Print(j)
         }()
       }
       ​
      

go语言函数

go语言的函数的作用 为了代码复用, 是一个基本的代码块,执行一个任务。

函数定义
 func function_name([parameter list])[return_type]{
   //函数体
 }
 ​
  • func:函数关键字,任何一个函数都有func关键字开始声明。
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。
  • 注意:go语言函数区别于c++和java语言的地方是可以有多个返回值,多个返回值用小括号括起来,中间用逗号分隔。
函数变量

函数变量作回调函数的例子:

 // 声明一个函数类型
 type fc func(int) int
 ​
 func main() {
   CallBack(1, callBack) //执行函数 -- CallBack
 }
 ​
 func CallBack(x int, f fc) { //定义一个函数testCallBack
   f(x) //由于传进来的是callBack函数,该函数执行需要传入一个int类型函数,因此传入x
 }
 ​
 func callBack(x int) int {
   fmt.Printf("我是回调 x: %d\n", x)
   return x
 }
函数闭包

闭包就是匿名函数,go语言支持匿名函数。

匿名函数的优越性在于可以直接使用函数内的变量,不必声明,可以使代码更简单,增强代码的可读性。

 ​
 func getNumber() func() int {
   i := 0
   return func() int {
     i += 1
     return i
   }
 }
 ​
 func main(){
   // nextNumber 作为一个函数,函数i0
   nextNumber := getNumber()
 ​
   //调用nextNumber函数, i 变量自增1 并返回
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
 ​
   //创建新的函数 nextNumber1
   nextNumber1 := getNumber()
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
 ​
   fmt.Println(nextNumber())
 }
 ​
 //输出
 1
 2
 3
 1
 2

i 变量是存储在 getNumber 函数的闭包中的。当调用 getNumber 函数时,它会返回一个内部的匿名函数,这个匿名函数就是闭包函数。

闭包函数中的 i 变量被捕获并绑定到闭包函数中,形成了一个封闭的作用域。每次调用闭包函数时,都可以访问和修改这个 i 变量。由于 i 存在于闭包函数的作用域中,它的值会在每次调用闭包函数时被保留下来,不会随着函数的返回而销毁。

go语言常量

常量的定义: 是一个简单值的标识符,在程序运行时,不可以被修改。(布尔类型、数字类型(浮点型、整数型和复数)和字符串)

 const identifier [type] = value
 //类型说明符 [type]可以省略,因为编译器可以根据变量的值来推断其类型。
常量用作枚举

常量可以用len(), cap() , unsafe.Sizeof() 函数计算表达式的值。

字符串类型 unsafe.Sizeof() 函数一直是16, 字符串类型对应一个结构体, 结构体有两个域,第一个域 指向该字符串的指针,第二个域是字符串的长度,每个域占8个字节。

 const (
   a = "abc"
   b = len(a)
   c = unsafe.Sizeof(a)
 )
 func main(){
   fmt.Println(a, b, c)
 }
 ​
 //答案
 abc 3 16
iota

iota是一个特殊常量, 可以被编译器修改的常量, iota 在 const关键字出现时 将被重置为0(const 内部的第一行之前), const中每新增一行常量声明将使iota 计数一次。

 func main() {
   const (
     a = iota
     b
     c
     d = "ha"
     e
     f = 100
     g
     h = iota
     i
   )
   fmt.Println(a, b, c, d, e, f, g, h, i)
 }
 //输出结果
 0 1 2 ha ha 100 100 7 8

在 Go 语言中,如果常量没有显式地指定值,则会采用上一个常量的值作为默认值。

 const (
   j = 1 << iota //<<表示左移操作
   k = 3 << iota
   l
   m
 )
 func main() {
   
   fmt.Println(j, k, l, m)
 }
 //输出
 1 6 12 24

go语言运算符

  • 算术运算符

    A=10, B=20

    运算符描述实例
    +相加A+B = 30
    -相减A-B=-10
    *相乘A * B = 200
    /相除B / A = 2
    %求余B % A = 0
    ++自增A++ = 11
    --自减A-- =9
  • 关系运算符

    A= 10, B= 20

    运算符描述实例
    ==左边的值是否等于右边值,如果是返回True否则返回False(A==B) 为False
    !=左边的值是否不等于右边值,如果是返回True否则返回False(A != B)为True
    左边的值是否大于右边值,如果是返回True否则返回False(A>B)为False
    <左边的值是否小于右边值,如果是返回True否则返回False(A<B)为True
    >=左边的值是否大于等于右边值,如果是返回True否则返回False(A>=B)为False
    <=左边的值是否小于等于右边值,如果是返回True否则返回False(A<=B)为True
  • 逻辑运算符

    A = true, B = false

    运算符描述实例
    &&逻辑AND 运算符,两边的操作数都是True,则为True,否则False(A && B) false
    逻辑OR运算符,两边操作数都是False,则为False,否则为True(AB)为 true
    !逻辑NOT运算符,如果条件为True, 则逻辑NOT条件为False,否则为True!(A && B)为true
  • 位运算符

    位元算符是对内存中二进制数进行按位运算

    A=60(00111100) B = 13(00001101)

    运算符描述实例
    &按位与运算符“&”是双目运算符,参与运算的两数各对应的二进位相与A&B 结果为12 二进制为00001100
    按位或元算符""是双目元算符,参与运算的两数各对应的二进位相或A|B结果为61二进制为00111101
    按位异或运算符,双目运算符,参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。A^B结果为49,二进制为00110001
    >>右移运算符,右移n位就是除以2的n次方。把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。A>>2结果为15,二进制为00001111
    <<左移运算符,左移n位就是乘以2的n次方。把"<<"左边的运算数的各二进位全部左移若干位,"<<"右边的数指定移动的位数,右移运算符,右移n位就是除以2的n次方。把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。A<<2结果为240,二进制为11110000

    右移运算符,右移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
    =按位或后赋值C=2等于 C = C2
  • 其他运算符

    有取地址运算符&和指针运算符*

    运算符描述实例
    &返回变量存储地址&a 将给出变量a的实际地址
    *指针变量*a 是一个指针变量
运算符优先级

有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。

5* / % << >> & &^
4+ - ^
3== != <<= >>=
2&&
1

通过使用括号来临时提升某个表达式的整体运算优先级。