本文以面向对象为基础,阐述一下go中的某些功能的使用。
高级数据类型
高级数据的关键字是type
。下面简单描述每种对象的声明:
数组
数据必须定义类型,所有元素都有默认值。
//第一种 先定义,而后赋值 []中的长度不可以省略,运行会出错
var arr [2]int
arr01 := []int{}
arr[0] = 2
//第二种
arr1 := [3]int{1, 2, 3}
//第三种 此处的[]中可以写长度值,也可以不写,也可以写...
//因为后面的{}里有值,go会自动计算出该数组的长度
arr2 := []int{4, 5, 6}
arr21 := [...]int{4, 5, 6}
//第四种 不按顺序来编写数组的值,而是按照索引
//下面数据的输出是[3 1]
//索引1位置的是1,索引0位置的是3
arr3 := []int{1: 1, 0: 3}
fmt.Println(arr, arr01, arr1, arr2, arr21, arr3)
基本方法:
- len():求数组长度,如len(arr21)。
- cap():求数组容量,如cap(arr21)。
- append():给数组追加元素(不会覆盖默认值)。arr21=append(arr21,22,23),值为[5 6 22 23]
Tips:关于len()和cap()的区别
在数组中,这两个是一样的。
在切片中,cap()是总容量,len()是可见元素的长度。所以cap()>=len()。
切片
切片,数组的一种,类似于java中的数组截断。但是java中的截断是生成一个新数组,这个是返回指定索引,还有可能通过切片扩容访问原数组。
数组中的所有方法都可以给切片使用。
var numbers4 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice5 := numbers4[4:6:8]
fmt.Println(len(slice5), cap(slice5))
fmt.Println("slice5-1:", slice5)
slice5 = slice5[:cap(slice5)]
fmt.Println("slice5-2:", slice5)
slice5 = append(slice5, 11, 12, 13)
fmt.Println("slice5-3:", slice5)
如下是输出内容:
2 4
slice5-1: [5 6]
slice5-2: [5 6 7 8]
slice5-3: [5 6 7 8 11 12 13]
关于切片的使用,有一种比numbers4[4:6:8]
简单的用法,numbers4[4:6]
。不同的是前者扩容,会找原数组下标为8的元素,并将原数组下标48的元素返回,而后者一直都是原数组下标46的元素。
字典
类似于在java中的map。其关键字也是map。
示例如下:
//定义一个string类型的键,int类型值的字典(不初始化则{}内为空即可)
enums := map[string]int{"Z": 100, "W": 44}
//给enum添加键值对,键为“X”,值为99
enums["X"] = 99
//获取键为“Z”的值
i := enums["Z"]
fmt.Println(enums, i)
//获取键为“X”的值,如果没有,返回该类型的默认值
//如果没有,则此时ok为false
p, ok := enums["X"]
fmt.Println(enums, p, ok)
//删除键值为“Z”的键值对
delete(enums, "Z")
fmt.Println(enums)
输出结果:
map[W:44 X:99 Z:100] 100
map[W:44 X:99 Z:100] 99 true
map[W:44 X:99]
关于p, ok := enums["X"]
这种写法,以后会很常见这种写法。因为对获取的元素是否存在的不确定性,所以ok变量是表示该元素是否存在的bool值。
通道
通道类型为chan,用符号<-
来表示。其数据格式类似于队列。示例如下:
//表示简历4个长度的通道
ints := make(chan int, 4)
ints <- 1
ints <- 2
i := <-ints //此处是1
ints <- 4
ints <- 7
ints <- 71
<-ints //此处是2
fmt.Println(i, <-ints) //此处是4
关于上面的通道ints,直接输出ints位地址,而<-ints表示ints中存储的值。而<-ints也可以和别的变量进行运算。
在i:=<-ints
这一行,会弹出一个长度的数据。当塞入的长度超出指定的长度时,会抛出错误。相当于队列满了,不能再塞入了。
fatal error: all goroutines are asleep - deadlock!
goroutines是go的函数模型,可以理解为一次性轻量级线程。在不使用协程时,主函数运行完了,此时没有其他函数去执行管道设值,程序就放弃等待了,然后出错了。
针对于上面的情况,
i1 := 1
//定义int型通道c1,默认长度为0
c1 := make(chan int)
go func() {
c1 <- 11
}()
fmt.Println("第一次:", i1)
//c1中的值赋值给i1
i1 = <-c1
fmt.Println("第二次:", i1)
输出的结果:
第一次: 1
第二次: 11
因为默认的通道为非缓冲通道,其长度为0,所以此时需要借助协程来操作。而第一个例子的通道指定了长度,叫做缓冲通道。
而go fun(){}
表示一个协程来运行其所包含的代码。
协程
理解协程,要和多线程进行比较。二者的共同点就是并发,但是协程是线程自己内部进行切换,由程序决定,而多线程是多个线程间切换,有切换上下文开销。相比较而言,协程开销小的多。
函数
在go中方法首字母大写等于public
(公开),小写等于protect
(同包)。没有其他访问范围限制。
对于JS中函数熟悉的人,对go中的函数理解也会很快的。
函数的基本形式为:
//第一种形式
func sum(a int, b int) int {
return a+b
}
//第二种形式
func sum1(a int,b int)(c int) {
c=a+b
return
}
对于函数,入参为变量名 变量类型。输出类型在最后。void类型的不写。如果入参的类型部分相同,则可以sum(a, b int)
这样写法。在go中,所有方法不管出入参数类型是否相同,方法名必须不同!
在JS中,函数可以用匿名变量代替,针对于上面的sum函数:
//第一种
var s0 = sum
s0(1, 2)
//第二种
var s1 = func(a, b int) int {
return a + b
}
s1(2, 3)
对于匿名函数,还有一种是在第二种后面直接加(),写上入参,但这种是一次性的,个人感觉有点类似于闭包。复用性不大,有兴趣的可以自己尝试。
go中的函数支持返回多个值,如下示例:
func sum2(a, b int) (int, bool) {
return a + b, true
}
结构体
go中的结构体类似于就java中的类。他可以封装属性、函数等。
示例,演示go中的继承:
type Animal struct {
name string
Age int
}
type Dog struct {
//继承部分
Animal
color string
name string
}
func main() {
animal := Animal{"hh", 33}
var dog Dog
dog.Animal.name="XX1"
dog.name="XX2"
fmt.Println(animal, dog)
}
输出如下:
{hh 33} {{XX1 0} XX2}
值得一提的是,在Dog类中,如果没有name属性,那么调用dog.name
属性会直接指向dog.Animal.name
属性。
匿名结构体
dog := struct {
name string
age int
color string
}{"小黄", 5, "#FF0000"}
fmt.Println(dog)
方法体
方法体,即属于某一结构专属的方法调用。
方法体是一种特殊的函数。所以上面函数部分的规则也同样适用于方法体。
func (animal *Animal) GrowUp() {
animal.Age++
}
接口
一个接口代表着某一种类型的行为。
接口的基本定义格式如下:
type Animal interface {
Grow()
Move(string) string
}
可以发现,接口内方法的定义和java是有相似之处的——都只有方法声明,没有方法体。在go的接口中,不支持属性。
接口的类型转换
func main() {
d := Dog{"Robert", 3, "Beijing"}
v := interface{}(&d)
animal := v.(Animal)
fmt.Println(animal)
}
type Animal interface {
Grow()
Move(string) string
}
type Dog struct {
name string
Age int
address string
}
func (dog *Dog) Grow() {
dog.Age++
}
func (dog *Dog) Move(address string) string {
oldAddr := dog.address
dog.address = address
return oldAddr
}
上面的&d
表示取地址操作。在下面会描写这部分的。而interface{}(&d)
返回的类型用 reflect.TypeOf 查看是*main.Dog
。需要说一下的是interface{}
是空接口,它相当于java中的Object。go中的任何类型都是它的实现类型。再紧随其后的v.(Animal)
是强制Dog类型转换为Animal类。
指针
对于java小伙伴来说,指针、地址等都只是概念性了解,不像C++中,直接操作地址、指针等。而C/C++的性能之所以高,指针功不可没。但是在C/C++中的回收控制不太好。所以下面从使用方面来了解一下指针与地址。
操作指针设计两个字符——&
和*
。简单的来说,&是取地址的,*是取值的。
基本类型
house := "wahaha"
// 对字符串取地址, ptr类型为*string
ptr := &house
// 打印ptr的类型,地址
fmt.Println(reflect.TypeOf(ptr), ptr)
//对ptr取值操作
fmt.Println(*ptr)
此时输出结果为
*string 0xc00004c1c0
wahaha
如想再深入了解,可以参见我整理的C++中的指针。
对象类型
还是以上面的Dog类的Grow方法。
func (dog *Dog) Grow() {
fmt.Println(&dog)
dog.Age++
}
此时打印输出的结果是 0xc000006028。
我们注意到了所有的方法中,前面的都是*Dog
,那么此时*Dog和Dog有什么区别呢?
此时的*Dog是Dog类的一个指针类型,简单点说,Dog类型的对象在程序中有很多,怎么确定是你这个d来调用方法呢?就要用到指针类型。上面的方法也叫指针方法。而如果是:
func (dog Dog) Grow() {
dog.Age++
}
此时就会将你的d复制一份,而复制的这一份与原来的d只有数值的相同而已,两者不共用地址,所以在func (dog Dog) Grow()
方法中,计算完后,函数的入参dog就没有再用了,也就是此时复制的这一份没有再用了,就会进行销毁,而且这个复制对象的生存与否与d没有任何关系,也不会影响d的数值变化。这个方法也叫值方法。
网友评论