【go学习笔记二】变量、数据类型、流程控制、数组切片


golang变量

声明变量

go的变量需要声明后才能使用,同一作用域不支持重复声明,且需要声明后使用。

语法:

var 变量名 类型
如:var name string

var:声明变量关键字

name:变量名称,或任何符合命名规范的其他名称

string:变量类型,类似的如int

如:

func main() 
    var name string
    var age int
    var _sys int
}

也可以批量声明

func main() {
    var (
        name string
        age int
        level int
    )
}

声明后不赋值也会有默认值:

package main

import "fmt"

func main() {
    var (
        name string
        age  int
        ok   bool
    )
    fmt.Printf("name: %v\n", name)
    fmt.Printf("age: %v\n", age)
    fmt.Printf("ok: %v\n", ok)
}
// name: 
// age: 0
// ok: false

变量初始化

声明变量时,会自动初始化成类型的默认值,如整型是0,字符串是空字符串,布尔型是false。

var 变量名 类型 = xx
如:var name string = "hqinglau"

类型推断:从初始化的值就可以得知变量的类型

func main() {
    var name = "hqinglau"
    fmt.Printf("name: %v\n", name)
}

批量初始化

func main() {
    var name, age, ok = "hqinglau", 20, true
    fmt.Printf("name: %v\n", name)
    fmt.Printf("age: %v\n", age)
    fmt.Printf("ok: %v\n", ok)
}
// name: hqinglau
// age: 20
// ok: true

短变量声明

只能用在函数内部,用:=对变量进行初始化和声明。

func main() {
    name := "hqinglau"
    age := 20
    ok := false
    fmt.Printf("name: %v\n", name)
    fmt.Printf("age: %v\n", age)
    fmt.Printf("ok: %v\n", ok)
}

// name: hqinglau
// age: 20
// ok: true

匿名变量

用下划线,表示不感兴趣。例如对函数的age变量不感兴趣:

import "fmt"

func getNameAndAge() (string, int) {
    return "hqinglau", 20
}

func main() {
    name, _ := getNameAndAge()
    fmt.Printf("name: %v\n", name)
}

// name: hqinglau

golang常量

在编译期间就确定下来的值,在运行时无法改变该值。

语法:

const 常量名 类型(可省略)= value

例子:

除了一旦定义了不能再修改,其他用法和变量差不多。

func main() {
    const PI float64 = 3.14
    // const PI = 3.14 类型可省略
    // PI = 3.145 错误,不能对常量赋值
    const PI2, PI3 = 3.1415, 3.1415926
    const (
        a = 100
        b = 200
    )
}

iota

是一个可以被编译器修改的常量,默认开始值是0,调用一次加1,只在当前const有效。

func main() {
    const (
        a = iota
        b = iota
        _ //表示跳过一个值
        c = iota
    )

    const d = iota

    fmt.Printf("a: %v\n", a)
    fmt.Printf("c: %v\n", c)
    fmt.Printf("b: %v\n", b)
    fmt.Printf("a: %v\n", a)

    fmt.Printf("d: %v\n", d)
    fmt.Printf("a: %v\n", a)
    fmt.Printf("c: %v\n", c)
    fmt.Printf("b: %v\n", b)
    // a: 0
    // c: 3
    // b: 1
    // a: 0
    // d: 0
    // a: 0
    // c: 3
    // b: 1
}

中间插队,不影响其他的。

const (
    a = iota //0
    b        //1
    c = 100  //100
    d = iota //3
    e        //4
)
// 但是如果没有d = iota,就会接着100排序
func main() {
    const (
        a = iota //0
        b        //1
        c = 100  //100
        d        //100  :100+0
        e        //101  :100+1
    )

    fmt.Printf("d: %v\n", d)
}

为何有iota这个东西?

go中没有枚举类,可以用iota创造出一组值不同的常量。

//一般写法
const (
    CategoryBooks    = 0
    CategoryHealth   = 1
    CategoryClothing = 2
)

//用iota的写法
const (
    CategoryBooks    = iota  //0
    CategoryHealth           //1
    CategoryClothing         //2
)

链接:https://www.zhihu.com/question/274481696/answer/374881523

还有特殊场景,例如表达式:

func main() {
    const (
        a = iota*2 + 1 //1
        b              //3
        c              //5
        d              //7
        e              //9

    )

    fmt.Printf("d: %v\n", d) // 7
}

golang数据类型

按类别分:

  • 布尔型
  • 数字型
  • 字符串类型
  • 派生类型,如指针,数组,结构体,切片。。。

demo

import "fmt"

func fa() { // func()类型

}

func main() {
    name := "tom"                 // string: tom
    age := 20                     // int: 20
    p := &age                     // *int: 0xc000014098 int类型的指针
    ok := true                    // bool: true
    var array = [...]int{1, 2, 3} // [3]int: [1 2 3]
    //切片,动态数组
    slice := []int{1, 2, 4} // []int: [1 2 4]
    fmt.Printf("%T: %v\n", name, name)
    //...
    // %T打印类型

}

数字类型

整数:int,uint(无符号),uintptr(足够存放一个指针即可)。如:uint8,uint16,uint32,uint64,int8,int16,int32,int64。

int和uint在32位系统上是4个字节,64位系统是8个字节。

浮点型:float32,float64,complex64,complex128实数和虚数。

package main

import (
    "fmt"
    "math"
    "unsafe"
)

func main() {
    var i8 int8   //int8 1B -128~127
    var i32 int32 //int32 4B -2147483648~2147483647
    fmt.Printf("%T %dB %v~%v\n", i8, unsafe.Sizeof(i8), math.MinInt8, math.MaxInt8)
    fmt.Printf("%T %dB %v~%v\n", i32, unsafe.Sizeof(i32), math.MinInt32, math.MaxInt32)
}

点击math,会跳转到官网。

image-20220412182839184

二进制、八进制、十六进制

func main() {
    a := 16
    fmt.Printf("%d \n", a) //16
    fmt.Printf("%b \n", a) //10000
    fmt.Printf("%x \n", a) //10

    b := 077
    fmt.Printf("%o \n", b) //77 八进制

    c := 0xff
    fmt.Printf("%x \n", c) //ff
    fmt.Printf("%X \n", c) //FF
    fmt.Printf("%d \n", c) //255
}

浮点类型

import (
    "fmt"
    "math"
)

func main() {
    fmt.Printf("%f\n", math.Pi)   //3.141593
    fmt.Printf("%.2f\n", math.Pi) //3.14
}

默认int

image-20220412183913893

布尔类型

两个值:truefalse

func main() {
    // a := false
    // var b = false
    // var c bool = false
    age := 16
    if age >= 18 {
        fmt.Println("成年了")
    } else {
        fmt.Println("未成年")
    }
}

字符串

一个golang字符串是一个任意字节的常量序列。

字符串字面量

使用双引号或者反引号来创建。

  • 双引号:可解析,支持转义,但是不能引用多行。
  • 反引号:创建原生的字符串字面量,可以多行,不支持转义,可以包含除了反引号的其他所有字符。
func main() {
    var str1 string = "hi"
    var html1 string = `
<html>
    <body>da</body>
</html>
    `
    fmt.Printf("str1: %v\n", str1)
    fmt.Printf("html1: %v\n", html1)
}

字符串连接

go中字符串不可变。

加号
func main() {
    s1:="hello"
    s2:="world"
    msg:=s1+s2
    fmt.Printf("msg: %v\n", msg) //msg: helloworld
}
Sprintf
func main() {
    s1 := "hello"
    s2 := "world"
    msg := fmt.Sprintf("%s, %s!", s1, s2)
    fmt.Printf("msg: %v\n", msg) //msg: hello, world!
}
strings.Join()
func main() {
    s1 := "hello"
    s2 := "world"
    msg := strings.Join([]string{s1, s2}, ",")
    fmt.Printf("msg: %v\n", msg) //msg: hello,world!
}

join会先根据字符串数组的内容,计算出拼接后的长度,然后申请内存,填入字符串。在已有数组的情况下效率比较高,没有的话,构造数组代价也不小。

buffer.WriteString()
func main() {
    var buffer bytes.Buffer
    s1 := "hello"
    s2 := "world"
    buffer.WriteString(s1)
    buffer.WriteString(",")
    buffer.WriteString(s2)
    fmt.Printf("buffer: %v\n", buffer) 
    //buffer: {[104 101 108 108 111 44 119 111 114 108 100] 0 0}

    fmt.Printf("buffer.String(): %v\n", buffer.String())
    //buffer.String(): hello,world
}
转义字符
\r \n \t之类的。
切片

顾前不顾后。前闭后开。

func main() {
    str := "hello world"
    fmt.Printf("str[3]: %v\n", str[3])     //str[3]: 108
    fmt.Printf("str[3:5]: %v\n", str[3:5]) //str[3:5]: lo
    // fmt.Printf("str[3:100]: %v\n", str[3:100]) wrong:out of range

    fmt.Printf("str[3:]: %v\n", str[3:]) //str[3:]: lo world
    fmt.Printf("str[:5]: %v\n", str[:5]) //str[:5]: hello
}
常用函数
import (
    "fmt"
    "strings"
)

func main() {
    str := "hello world"
    fmt.Printf("len(str): %v\n", len(str))
    //len(str): 11

    fmt.Printf("strings.Split(str, \" \"): %v\n", strings.Split(str, " "))
    // 分割为数组:strings.Split(str, " "): [hello world]

    fmt.Printf("strings.Contains(str, \"hell\"): %v\n", strings.Contains(str, "hell"))
    // 判断是否含有某个字符串:strings.Contains(str, "hell"): true

    fmt.Printf("strings.ToUpper(str): %v\n", strings.ToUpper(str))
    // 转大写:strings.ToUpper(str): HELLO WORLD

    fmt.Printf("strings.Index(str, \"l\"): %v\n", strings.Index(str, "l"))
    // 第一个出现匹配串的位置:strings.Index(str, "l"): 2

    fmt.Printf("strings.HasPrefix(str, \"hi\"): %v\n", strings.HasPrefix(str, "hi"))
    // 是否含有前缀:strings.HasPrefix(str, "hi"): false
}

格式化输出

image-20220413123720809

如:

type Website struct {
    Name string
    Owner string
}

func main() {
    var site = Website{Name: "orzlinux.cn",Owner: "hqingLau"}
    fmt.Printf("site: %v\n", site)  //site: {orzlinux.cn hqingLau}
    fmt.Printf("site: %+v\n", site) //site: {Name:orzlinux.cn Owner:hqingLau}
    fmt.Printf("site: %#v\n", site) //site: main.Website{Name:"orzlinux.cn", Owner:"hqingLau"}
    fmt.Printf("site: %T\n", site)  //site: main.Website
}

运算符

和其它语言相似。

+-*/%,==,大于,小于,大于等于,小于等于。

func main() {
    a := 100
    b := 30
    fmt.Printf("a+b: %v\n", a+b)  //130
    fmt.Printf("a-b: %v\n", a-b)  //70
    fmt.Printf("a*b: %v\n", a*b)  //3000
    fmt.Printf("a/b: %v\n", a/b)  //3
    fmt.Printf("a%%b: %v\n", a%b) //10
    a++
    fmt.Printf("a: %v\n", a) //101
    b--
    fmt.Printf("b: %v\n", b)         //29
    fmt.Printf("a>=b: %v\n", a >= b) //true
    fmt.Printf("a==b: %v\n", a == b) //false
    fmt.Printf("a<b: %v\n", a < b)   //false
}

逻辑运算:&&||。与或非。

位运算:&|^<<>>

赋值:&=|=等。

流程控制

if else

if a > b {
    fmt.Println("a is larger than b")
} else if a < b {
    fmt.Println("a is smaller than b")
} else {
    fmt.Println("a is equal to b")
}

else if 可以有零个或多个

遍历也可以写在if里面,如:

if who := "manager"; who == "manager" {
    fmt.Println("equal") //进入了
}
//fmt.Printf("who: %v\n", who) // 报错,who只能在if的作用域

牵扯到作用域的问题

demo1:

who := "manager"

if who = "boss"; who == "manager" {
    fmt.Println("equal") //没进入
}
fmt.Printf("who: %v\n", who) // who: boss

demo2:

who := "manager"

if who := "boss"; who == "boss" {
    fmt.Println("equal") // 进入了
}
fmt.Printf("who: %v\n", who) // who: manager

获取用户输入:

var name string
var age int
var email string

fmt.Println("输入name, age, email,空格分隔:")
fmt.Scan(&name, &age, &email)
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
fmt.Printf("email: %v\n", email)
// > go run main.go
// 输入name, age, email,空格分隔:
// hqinglau 23 hqinglau@gmail.com
// name: hqinglau
// age: 23
// email: hqinglau@gmail.com

判断奇偶数例子:

func main() {
    f2()
}

func f2() {
    var num int
    fmt.Println("请输入一个数字:")
    fmt.Scan(&num)
    fmt.Printf("num: %v\n", num)
    if num%2 == 0 {
        fmt.Println("偶数")
    } else {
        fmt.Println("奇数")
    }
}
// ---------------输入非法整数例子---

// 输入:str
// num: 0
// 偶数

// 输入:1.345
// num: 1
// 奇数

switch

go里面的case末尾不需要添加break

一个case后面可以跟多个:

func main() {
    who := "manager"
    switch who {
    case "manager", "boss":
        fmt.Println("leader")
    case "employee":
        fmt.Println("follower")
    default:
        fmt.Println("else")
    }
}

循环

go里循环只有for循环,去除了whiledo while等。

for i := 0; i < 10; i++ {
    fmt.Printf("i: %v\n", i)
}

x := [...]int{1, 3, 2, 3}
for _, v := range x {
    fmt.Printf("v: %v\n", v)
}

break,continue之类。

goto

goto被诟病很久了,golang里面还有goto,看到stackoverflow的一个解释如下:

在go标准库里面,goto经常被使用。当你真正知道自己在做什么,并且知道内建特性没有办法满足你的目的时,goto可能很方便。例如多层for嵌套要退出,不用goto就需要使用大量的bool语句来作为判断标志。

如果其他控制语句能达到你的目的,那就不要用goto。而且golang里面的goto加了一些限制,来防止错误:

  • A goto跳到的地方B,B处所有的变量必须在A处都有。

        goto L // BAD
        v := 3
    L:
    

    类似这样是不可以的,如果L下面代码用到了v,而v的声明都跳过了。

    image-20220413170700650

  • 在标签所在的block(就是大括号)外面的goto不能跳进这个标签。

    if n%2 == 1 {
        goto L1
    }
    for n > 0 {
        f()
        n--
    L1:
        f()
        n--
    }
    

跳出多重循环场景:判断一个多维数组里面是否有某个数,有就跳出循环,不再遍历:

func main() {
    var multiDimArray [100][100][100][100]int
    multiDimArray[50][60][70][80] = 1

    for i := 0; i < 100; i++ {
        for j := 0; j < 100; j++ {
            for k := 0; k < 100; k++ {
                for m := 0; m < 100; m++ {
                    if multiDimArray[i][j][k][m]==1 {
                        fmt.Println("find 1")
                        goto LABEL1
                    }
                }
            }
        }
    }
LABEL1:
    fmt.Println("DO STH ELSE...")
}

数组

声明

golang里面的数组一旦定义之后,长度不能改变。

var 数组名 [数组大小] 元素类型

如:

func main() {
    var a1 [2]int
    var a2 [3]string

    fmt.Printf("a1: %T\n", a1) //a1: [2]int
    fmt.Printf("a2: %T\n", a2) //a2: [3]string
}

声明之后没有初始化里面的元素就是默认值,如整型就是0,字符串就是空串。

初始化

var a1 = [2]int{1, 2}
var a2 = [...]int{1, 2} // 省略数组长度
var a3 = [3]int{1, 2} //1,2,0
var a4 = [4]string{0: "hi", 2: "you"} //{"hi", "", "you", ""}

...会根据初始化列表的长度默认大小;指定大小3但是只初始化了两个数,剩下的就默认0.

也可以根据index进行初始化。

访问、修改

直接通过下标访问修改。

var a4 = [4]string{0: "hi", 2: "you"} //{"hi", "", "you", ""}
a4[1] = "love"
a4[0] = "I" //a4: [4]string{"I", "love", "you", ""}

长度

var a1 = [2]int{1, 2}
fmt.Printf("len(a1): %v\n", len(a1)) //2

遍历

根据长度和index:

func main() {
    var a1 = [...]int{1, 2, 3, 4, 5}
    for i := 0; i < len(a1); i++ {
        fmt.Printf("a1[%d]: %v\n", i, a1[i])
    }
    // a1[0]: 1
    // a1[1]: 2
    // a1[2]: 3
    // a1[3]: 4
    // a1[4]: 5
}

range:

var a1 = [...]int{1, 2, 3, 4, 5}
for i, v := range a1 {
    fmt.Printf("a1[%d]: %v\n", i, v)
}

切片

slice。

数组一旦定义长度就定死了,切片可以理解为可变长度的数组,底层使用数组实现,增加了自动扩容功能。是一个拥有相同元素数据类型的可变长度的序列。

var 数组名 [] 元素类型

中括号里面不指定大小。

func main() {
    var slice1 []int
    fmt.Printf("slice1: %v\n", slice1)           //slice1: []
    fmt.Printf("len(slice1): %v\n", len(slice1)) //len(slice1): 0
}

初始化:

func main() {
    var slice1 []int = make([]int, 5)
    var slice2 = make([]int, 5)
    fmt.Printf("len(slice1): %v\n", len(slice1)) //len(slice1): 5
    fmt.Printf("len(slice2): %v\n", len(slice2)) //len(slice1): 5
}

demo

append

func test() {
    var a1 = []int{1, 2, 3}
    fmt.Printf("a1: %v\n", a1) // a1: [1 2 3]
    a1 = append(a1, 4)
    fmt.Printf("a1: %v\n", a1) //a1: [1 2 3 4]
}

func main() {
    test()
}

make

var a1 = make([]int, 2) // 已经声明了两个长度的切片
fmt.Printf("a1: %v\n", a1) // a1: [0 0]
a1 = append(a1, 4)
fmt.Printf("a1: %v\n", a1) // a1: [0 0 4]

长度和容量

func test() {
    var a1 = make([]int, 3)              // 默认长度和容量都是3
    fmt.Printf("len(a1): %v\n", len(a1)) //3
    fmt.Printf("cap(a1): %v\n", cap(a1)) //3
    a1 = append(a1, 4)
    fmt.Printf("len(a1): %v\n", len(a1)) //4
    fmt.Printf("cap(a1): %v\n", cap(a1)) //6
    a1 = append(a1, 5, 6, 7)
    fmt.Printf("len(a1): %v\n", len(a1)) //7
    fmt.Printf("cap(a1): %v\n", cap(a1)) //12
}

可以看到,达到容量之后是两倍扩容

初始化手段

数组或者数组的部分元素初始化:

func test() {
    var a1 = [...]int{1, 2, 3, 4, 5}   //切片也行
    slice1 := a1[:]                    // 全部元素
    slice2 := a1[:4]                   // 某一段
    fmt.Printf("slice1: %T\n", slice1) //[]int
    fmt.Printf("slice2: %T\n", slice2) //[]int
}

空切片

切片在未初始化之前为nil,长度和容量都是0。

func test() {
    var a1 []int
    fmt.Println(a1 == nil)               // true
    fmt.Printf("len(a1): %v\n", len(a1)) // len(a1): 0
    fmt.Printf("cap(a1): %v\n", cap(a1)) // cap(a1): 0
}

初始化空:

var s1 = []int{}
fmt.Println(s1 == nil) // false

遍历

参照数组遍历。

添加、删除、拷贝

a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要打散

参数三个点是可以传0到多个参数:

func test(args ...int) {
    for i, v := range args {
        fmt.Printf("%v: %v\n", i, v)
    }
}

func main() {
    test()
    test(1)
    test(1, 2, 3)
    test([]int{1, 2, 3}...) // 数组或者切片后面三个点,表示变成一个一个的元素,将其打散
}

具体详细注释版代码:

var s1 = []int{}
// 增加
s1 = append(s1, 1)
s1 = append(s1, 2)
s1 = append(s1, 3)
fmt.Printf("s1: %v\n", s1) // s1: [1 2 3]

// 删除索引为1的元素
// 用前后的切片组合
s1 = append(s1[:1], s1[2:]...)
fmt.Printf("s1: %v\n", s1) // s1: [1 3]

拷贝

切片
// 拷贝
// 只是引用指向了同一个内存地址
var s2 = s1
s2[0] = 2
fmt.Printf("s1: %v\n", s1) // s1: [2 3]
fmt.Printf("s2: %v\n", s2) // s2: [2 3]

// 仍然是同一个内存地址
var s3 = s1[:]
s3[0] = 100
fmt.Printf("s1: %v\n", s1) // s1: [100 3]
fmt.Printf("s3: %v\n", s3) // s3: [100 3]

// 真拷贝
var s4 = make([]int, len(s1))
copy(s4, s1)
s4[0] = 99
fmt.Printf("s1: %v\n", s1) // s1: [100 3]
fmt.Printf("s4: %v\n", s4) // s4: [99 3]
数组
var s1 = [2]int{1, 3}

// 拷贝
// ====数组的话变成了真拷贝=====
var s2 = s1
s2[0] = 2
fmt.Printf("s1: %v\n", s1) // s1: [1 3]
fmt.Printf("s2: %v\n", s2) // s2: [2 3]

// 同一个内存地址
var s3 = s1[:]
s3[0] = 100
fmt.Printf("s1: %v\n", s1) // s1: [100 3]
fmt.Printf("s3: %v\n", s3) // s3: [100 3]

// 真拷贝
var s4 = make([]int, len(s1))
copy(s4, s1[:])
s4[0] = 99
fmt.Printf("s1: %v\n", s1) // s1: [100 3]
fmt.Printf("s4: %v\n", s4) // s4: [99 3]

既然切片是数组连续片段的引用,那之后又增加元素的话呢?

func main() {
    var s1 = [2]int{1, 3}

    // 同一个内存地址
    var s3 = s1[:]
    s3[0] = 100
    fmt.Printf("s1: %#v\n", s1) // [2]int{100, 3}
    fmt.Printf("s3: %#v\n", s3) // []int{100, 3}

    s3 = append(s3, 5)
    fmt.Printf("s1: %#v\n", s1) // [2]int{100, 3}
    fmt.Printf("s3: %#v\n", s3) // []int{100, 3, 5}
}

再增加就是另一个隐形数组的了,和s1没关系。