• Go语言语法入门


    熟悉了C/C++,初学go语言的时候语法可能有点不适应。


    一、基本数据类型

    对于这些数据类型格式化输出的方式可以查看下面文章:
    Golang格式化输出
    在这里插入图片描述

    1.1. rune

    这里要说一下rune,它一般用来表示中文的ASCII码,go默认的编码是utf-8编码,utf-8中一个中文占三个字节。但是go中的字符串底层实际上和C/C++是一样的,都是一个byte数组,所以存储一个中文就需要3个byte,比如:
    在这里插入图片描述

    package main
    
    import "fmt"
    
    func main() {
    
    	var s string = "你"
    	fmt.Println(len(s)) //3
    	for i := 0; i < len(s); i++ {
    		fmt.Println(s[i]) //打印它们的ASCII码值
    	}
    
    	for i := 0; i < len(s); i++ {
    		fmt.Println(string(s[i]))
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    可以看到对于中文的string,是无法通过下标的方式来打印的,因为3个byte才能显示一个中文,按照下标一个byte一个byte的方式打印肯定不行嘛。

    所以为了解决这种问题,可以将中文的ASCII码(3个byte表示)和英文的ASCII码(1个byte表示)同时转成rune类型(4个byte),这样我们打印的时候,就相当于打印一个个rune。

    package main
    import "fmt"
    func main() {
    	str1 := "你好"
    	str2 := []rune(str1)
    	fmt.Println(len(str2)) //2
    	for i := 0; i < len(str2); i++ {
    		fmt.Println(string(str2[i]))
    	}
    	str3 := "hello"
    	str4 := []rune(str3)
    	fmt.Println(len(str4)) //5
    	for i := 0; i < len(str4); i++ {
    		fmt.Println(string(str4[i]))
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    当然go语言在设计之初也考虑到了这种问题,所以range遍历可以自动帮我们进行转换:

    package main
    
    import "fmt"
    
    func main() {
    	var a rune = '你'
    	fmt.Printf("rune的类型是%T\n", a)
    
    	str1 := "你好"
    	for i, v := range str1 { 
    	//这里的i,v的意思是,获取str1中的下标和字符,它会根据ASCII自动转换成rune
    		fmt.Println(i)
    		fmt.Println(string(v))
    		fmt.Printf("v的类型是%T\n", v)
    	}
    
    	str3 := "hello"
    	for i, v := range str3 {
    		fmt.Println(i)
    		fmt.Println(string(v))
    		fmt.Printf("v的类型是%T\n", v)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    到此,再看下面一段程序,很容易就能看懂了:

    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println(string(97))
    	fmt.Println(string(20320))
    	temp := []rune{20320, 22909, 32, 19990, 30028}
    	fmt.Println(string(temp))
    
    	var str string = "hello world"
    	fmt.Println("byte=", []byte(str))
    	fmt.Println("byte=", []rune(str))
    	fmt.Println(str[:2])
    	fmt.Println(string([]rune(str)[:2]))
    
    	var str2 string = "你好 世界"
    	fmt.Println(len(str2))
    	str3 := []rune(str2)
    	fmt.Println(len(str3))
    
    	fmt.Println("byte=", []byte(str2))
    	fmt.Println("byte=", []rune(str2))
    	fmt.Println(str2[:3])
    	fmt.Println(string([]rune(str2)[:2]))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

    1.2. 变量定义的方式

    相比于C/C++,go语言在变量定义方面友好很多,方式如下:

    // 定义一个名称为 “variableName” ,类型为 "type" 的变量
    var variableName type
    
    // 定义并初始化初始化 “variableName” 的变量为 “value” 值,类型是 “type”
    var variableName type = value
    
    // 定义三个类型都是 “type” 的三个变量
    var vname1, vname2, vname3 type
    /*
    定义并初始化三个类型都是 "type" 的三个变量 , 并且它们分别初始化相应的值
    vname1 为 v1 , vname2 为 v2 , vname3 为 v3
    */
    var vname1, vname2, vname3 type= v1, v2, v3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    批量声明变量:

    var (
        a int
        b string
        c []float32
        d float64
        ...
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果还嫌麻烦,对于变量的类型,我们也是可以直接忽略的:

    var vname1, vname2, vname3 = v1, v2, v3
    
    vname1, vname2, vname3 := v1, v2, v3
    
    • 1
    • 2
    • 3

    := 这个符号直接取代了 var 和 type , 这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用 var 方式来定义全局变量。

    _(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。 在这个例子中,我们将值 32赋予 b ,并同时丢弃 31:

    _, b := 31, 32
    
    • 1

    Go对于声明但没有使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:我们定义并初始化了了 i 但未使用。
    在这里插入图片描述
    定义常量:

    const constantName = value
    
    // 如果需要,也可以明确指定常量的类型:
    const Pi float32 = 3.1415926
    
    const Pi = 3.1415926
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.3. 字符串操作

    注意:string是常量,不允许修改。
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    1.4. 强制类型转换

    强制类型转换和C/C++相同

    在这里插入图片描述

    1.5. 数组

    package main
    
    import "fmt"
    
    func main() {
    	var arr1 [5]int = [5]int{}      //指定数组的长度和类型,这里的_可以替换成数组名
    	var arr2 = [5]int{}             //不指定长度和类型
    	var arr3 = [5]int{3, 2}         //不指定长度和类型,给前两个元素进行初始化,其他元素默认都是0
    	var arr4 = [5]int{2: 10, 4: 30} //给第二个和第四个元素初始化,其他元素默认0
    	var arr5 = [...]int{1, 2, 3, 4} //不指定长度,根据数组内容进行推导
    	var arr6 = [...]struct {
    		name string
    		age  int
    	}{{"tom", 18}, {"jim", 20}} //数组的元素是匿名的结构体
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意:如果数组不指定长度,也不进行推导,则它是切片类型。
    在这里插入图片描述

    1.5.1. 遍历数组

    在这里插入图片描述

    1.5.2. 数组传参

    在这里插入图片描述

    package main
    import "fmt"
    
    //调用f1函数只会拷贝数组
    func f1(arr [5]int) {
    	arr[0] += 1
    }
    
    //f2传入数组的指针,可以修改外面的数组
    func f2(arr *[5]int) {
    	//由于go语言会省略掉指针解引用的操作,所以
    	//这样写也可以 arr[0] += 1
    	(*arr)[0] += 1
    
    	//go语言的for循环没有C++那种引用类型
    	//for循环中,i是arr的下标,n是arr[i]的拷贝,所以修改n不会修改arr[i]
    	//如果想修改数组中的内容,只能使用arr[i]的方式
    	for i, n := range arr {
    		arr[i] = n + 1
    	}
    }
    func main() {
    
    	var arr1 [5]int = [5]int{}
    	f1(arr1)
    	fmt.Println(arr1)//[0 0 0 0 0]
    	f2(&arr1)
    	fmt.Println(arr1)//[2 1 1 1 1]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    注意:如果传参时数组不指定长度,则它是切片类型。


    二、复合数据类型

    在这里插入图片描述

    2.1. 自定义类型

    在这里插入图片描述

    2.2. 切片slice

    上面我们说到如果数组不指定大小也不推导大小,则它会是切片类型,切片实际上是一个结构体类型,通过一个指针指向底层的数组,然后通过len和cap两个变量记录数组中数据的长度和数组的大小,有点类似于C++中的vector。

    切片(slice)是对底层数组一个连续片段的引用,所以切片是一个引用类型。
    在这里插入图片描述

    2.2.1. 初始化切片

    make与new类似,但make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。

    注意,初始化切片的时候不能够在[]中赋值,否则就变成数组了。
    在这里插入图片描述

    // 声明字符串切片
    // 声明一个字符串切片,切片中拥有多个字符串
    var strList []string
    
    // 声明整型切片
    // 声明一个整型切片,切片中拥有多个整型数值
    var numList []int
    
    // 声明一个空切片
    // 将 numListEmpty 声明为一个整型切片
    // 本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的,但是此时的 numListEmpty 已经被分配了内存,只是还没有元素
    var numListEmpty = []int{}
    
    // 输出3个切片
    // 切片均没有任何元素,3 个切片输出元素内容均为空
    fmt.Println(strList, numList, numListEmpty)
    
    // 输出3个切片大小
    // 没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片
    fmt.Println(len(strList), len(numList), len(numListEmpty))
    
    // 切片判定空的结果
    //声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true
    // numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false
    fmt.Println(strList == nil)
    fmt.Println(numList == nil)
    fmt.Println(numListEmpty == nil)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.2.2. append向切片中追加元素

    注意:append会返回新的切片,也就是说并不会改变原来的切片,所以一般需要将返回的切片赋值给原来的切片。
    在这里插入图片描述

    var a []int
    a = append(a, 1) // 追加1个元素
    a = append(a, 1, 2, 3) // 追加多个元素
    a = append(a, []int{1,2,3}...) // 追加一个切片
    
    • 1
    • 2
    • 3
    • 4

    在开头追加:

    var a = []int{1,2,3}
    a = append([]int{0}, a...) // 在开头添加1个元素
    a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
    
    • 1
    • 2
    • 3

    因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

    var a []int
    a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
    a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
    
    • 1
    • 2
    • 3

    每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

    注意:在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

    在这里插入图片描述

    2.2.3. copy():切片复制(切片拷贝)

    Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

    // 其中 srcSlice 为数据来源切片
    // destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice)
    // 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致
    // copy() 函数的返回值表示实际发生复制的元素个数。
    copy( destSlice, srcSlice []T) int
    
    • 1
    • 2
    • 3
    • 4
    • 5
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{5, 4, 3}
    copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
    copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
    
    • 1
    • 2
    • 3
    • 4

    2.2.4. 切片的删除

    go语言没有提供删除切片的功能,但是我们可以通过截取子切片的方式,删除原来切片中的一些元素。

    在这里插入图片描述

    删除开头的位置:

    a = []int{1, 2, 3}
    a = append(a[:0], a[1:]...) // 删除开头1个元素
    a = append(a[:0], a[N:]...) // 删除开头N个元素
    
    • 1
    • 2
    • 3

    删除中间的位置:

    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个元素
    
    • 1
    • 2
    • 3
    • 4
    • 5

    删除结尾的位置:

    a = []int{1, 2, 3}
    a = a[:len(a)-1] // 删除尾部1个元素
    a = a[:len(a)-N] // 删除尾部N个元素
    
    • 1
    • 2
    • 3

    提示:连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

    2.2.5. 切片传参

    切片传参和截取子切片类似,就是子切片和母切片共享底层的内存空间,类似于浅拷贝。

    package main
    import "fmt"
    
    //arr相当于是母切片的拷贝,它们共同指向同一块内存空间
    //但是当arr需要扩容的时候,就会脱离母切片
    func update_slice(arr []int) {
    	//由于arr和a指向同一块空间,所以会修改a这一块空间
    	arr[0] = 100
    	fmt.Printf("arr指针的地址%p\n", arr)
    	arr = append(arr, 1)
    	arr = append(arr, 1)
    	arr = append(arr, 1)
    	arr = append(arr, 1)
    	arr = append(arr, 1)
    	fmt.Printf("扩容后arr指针的地址%p\n", arr)
    }
    func main() {
    	a := make([]int, 3, 5)
    	fmt.Printf("a指针的地址%p\n", a)
    	update_slice(a)
    	fmt.Println(a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    这种传参的方式会导致函数体内可以修改母切片底层数组的值,但是却没法给母切片后面追加数据(因为没办法改变母切片的len和cap),可以通过传切片指针的方式解决这个问题:
    在这里插入图片描述

    2.3. map

    这里的map就相当于C++中的unordered_map,底层都通过哈希表实现。
    在这里插入图片描述

    2.3.1. map初始化

    在这里插入图片描述

    2.3.2. 添加和删除

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    2.4. 管道channel

    在这里插入图片描述

    2.4.1. 初始化

    管道是无法扩容的。
    在这里插入图片描述

    2.4.2. 放入和取出元素

    在这里插入图片描述

    在这里插入图片描述

    channel支持for-range的方式进行遍历,请注意几个细节:

    1. 在遍历的时候,如果channel没有关闭,则会出现deadlock的错误。
    2. 在遍历的时候,如果channel已经关闭,则会正常遍历数据,遍历完后会退出遍历。
    3. 遍历管道相当于从管道之中读取数据,也就是说,如果遍历完成,管道将会为空。
    4. 管道关闭以后不能够再打开,如果想接着使用管道,可以再创建一个。
    5. 当管道长度满了以后,如果没有人取走数据,则无法继续往管道中写,会报死锁错误(因为需要阻塞住,等管道中的数据被读走才能继续写)。
    6. 当管道空了以后,如果不关闭管道,继续读会报死锁错误(因为管道空了以后,继续读会被阻塞住)。如果关闭管道,为空时继续读则会读取默认值(比如int类型的管道,读取0)。
    7. 管道关闭以后,可以继续从管道中读取数据,但是不能写入数据。
      在这里插入图片描述
      在这里插入图片描述

    在读取的时候,可以顺便把读取成功与否一块读出来:
    在这里插入图片描述

    2.5.结构体类型和成员函数

    关于结构体类名以及成员变量,第一个字母是否大写,关乎到能否跨包访问,如果结构体类名首字母大写,则可以在其他包内使用该结构体,成员变量首字母大写,则可以在其他包内通过该结构体访问到该成员变量。
    在这里插入图片描述

    2.5.1. 成员函数(方法)

    一般函数的定义方式为:

    func 函数名(变量名 变量类型)返回值类型{
    	//函数体
    }
    
    • 1
    • 2
    • 3

    而成员方法,则只需要在func和函数名中间加上结构体的名字和类型即可

    func (对象名 结构体)函数名(变量名 变量类型)返回值类型{
    	//函数体
    }
    
    • 1
    • 2
    • 3

    因为要使用结构体的成员变量,所以需要加上对象名,如果不使用成员变量则可以不加对象名,但要指定结构体。

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    type User struct {
    	id         int
    	score      float64
    	name, addr string
    	time       time.Time
    }
    
    //这里需要访问User变量中的name成员
    func (u User) hello(man string) (int, int) {
    	fmt.Println("hi " + man + ",my name if " + u.name)
    	return 1, 2
    }
    
    //这里不需要访问User,所以可以不写,或者用_代替
    func (User) think(man string) {
    	fmt.Println("hi " + man + ",do you know my name?")
    }
    func main() {
    
    	user2 := User{1, 1.1, "tom", "北京", time.Now()}
    	_, a := user2.hello("jack")
    	fmt.Println(a)
    	user2.think("jack")
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述

    2.5.2. 匿名结构体

    在这里插入图片描述

    2.5.3. 结构体中含有匿名成员

    在这里插入图片描述

    2.5.4. 结构体指针

    在这里插入图片描述

    2.5.5. 构造函数

    go语言中没有构造函数和析构函数,因为gc能够自动帮我们回收不需要的内存空间,但为了和其他语言相符合,我们可以模拟实现一个构造函数。

    构造函数的名字可以随便起:
    在这里插入图片描述

    在这里插入图片描述

    2.5.6. 方法接收指针

    这个和C语言相同,就是传值和传指针的区别。
    在这里插入图片描述

    2.5.7. 结构体嵌套

    这个也和C语言类似
    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    source /etc/profile卡住不动
    GetX导航学习笔记
    【网络安全 --- MySQL数据库】网络安全MySQL数据库应该掌握的知识,还不收藏开始学习。
    记录一下在Pycharm中虚拟环境的创建
    Python实战项目5-Git远程仓库/分支合并/冲突解决
    【网络编程】多路转接之select、poll、epoll
    软件测试的发展趋势
    【OpenCV DNN】Flask 视频监控目标检测教程 09
    某知乎x-zse-96参数解析2022/08/09首发
    IBM研究员:人工智能+量子计算可解决抗生素耐药性挑战
  • 原文地址:https://blog.csdn.net/qq_52670477/article/details/126382661