熟悉了C/C++,初学go语言的时候语法可能有点不适应。
对于这些数据类型格式化输出的方式可以查看下面文章:
Golang格式化输出
这里要说一下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]))
}
相比于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
注意:string是常量,不允许修改。
强制类型转换和C/C++相同
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}} //数组的元素是匿名的结构体
}
注意:如果数组不指定长度,也不进行推导,则它是切片类型。
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]
}
注意:如果传参时数组不指定长度,则它是切片类型。
上面我们说到如果数组不指定大小也不推导大小,则它会是切片类型,切片实际上是一个结构体类型,通过一个指针指向底层的数组,然后通过len和cap两个变量记录数组中数据的长度和数组的大小,有点类似于C++中的vector。
切片(slice)是对底层数组一个连续片段的引用,所以切片是一个引用类型。
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)
注意: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 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
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个位置
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个元素
提示:连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。
切片传参和截取子切片类似,就是子切片和母切片共享底层的内存空间,类似于浅拷贝。
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),可以通过传切片指针的方式解决这个问题:
这里的map就相当于C++中的unordered_map,底层都通过哈希表实现。
管道是无法扩容的。
channel支持for-range的方式进行遍历,请注意几个细节:
在读取的时候,可以顺便把读取成功与否一块读出来:
关于结构体类名以及成员变量,第一个字母是否大写,关乎到能否跨包访问,如果结构体类名首字母大写,则可以在其他包内使用该结构体,成员变量首字母大写,则可以在其他包内通过该结构体访问到该成员变量。
一般函数的定义方式为:
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")
}
go语言中没有构造函数和析构函数,因为gc能够自动帮我们回收不需要的内存空间,但为了和其他语言相符合,我们可以模拟实现一个构造函数。
构造函数的名字可以随便起:
这个和C语言相同,就是传值和传指针的区别。
这个也和C语言类似