Go语言语法入门

本文介绍了Go语言的基本数据类型(如rune、变量定义、字符串操作、强制类型转换、数组与切片),以及复合数据类型(如自定义类型、切片、map、管道和结构体)的使用和区别,帮助C++开发者快速上手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]))
	}
}

在这里插入图片描述

可以看到对于中文的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]))
	}
}

在这里插入图片描述

当然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)
	}
}

在这里插入图片描述

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

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. 变量定义的方式

相比于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

批量声明变量:

var (
    a int
    b string
    c []float32
    d float64
    ...
)

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

var vname1, vname2, vname3 = v1, v2, v3

vname1, vname2, vname3 := v1, v2, v3

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

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

_, b := 31, 32

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

const constantName = value

// 如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926

const Pi = 3.1415926

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.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]
}

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


二、复合数据类型

在这里插入图片描述

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)

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}...) // 追加一个切片

在开头追加:

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

因为 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个位置插入切片

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

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

在这里插入图片描述

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

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

// 其中 srcSlice 为数据来源切片
// destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice)
// 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致
// copy() 函数的返回值表示实际发生复制的元素个数。
copy( destSlice, srcSlice []T) int
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个位置

2.2.4. 切片的删除

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

在这里插入图片描述

删除开头的位置:

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

删除中间的位置:

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个元素

删除结尾的位置:

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

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

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)
}

在这里插入图片描述

这种传参的方式会导致函数体内可以修改母切片底层数组的值,但是却没法给母切片后面追加数据(因为没办法改变母切片的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 函数名(变量名 变量类型)返回值类型{
	//函数体
}

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

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

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

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")

}

在这里插入图片描述

2.5.2. 匿名结构体

在这里插入图片描述

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

在这里插入图片描述

2.5.4. 结构体指针

在这里插入图片描述

2.5.5. 构造函数

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

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

在这里插入图片描述

2.5.6. 方法接收指针

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

2.5.7. 结构体嵌套

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值