类型 [n]T
是一个有 n 个类型为 T 的值的数组。
表达式:
var a [10]int 复制代码
定义变量 a 是一个有十个整数的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。
var a [2]string a[0] = "Hello" a[1] = "World" b := [6]int{2, 3, 5, 7, 11, 13} //数组 声明并赋值 复制代码
切片即 slice
[]T
是一个元素类型为 T 的 slice 。
一个 slice
会指向一个序列的值,并且包含了长度信息。
说人话, slice
其实类似于 Java
里的 ArrayList
,就是一个动态数组。
不同点在于, Java
里的 ArrayList
自己内部维护一个数组, slice
即可以内部维护一个数组,也可以在现有的数组上创造 slice
。
每次扩容 slice
会指向一个扩容后的数组。
slice
的零值是 nil
。
一个 nil
的 slice
的长度和容量是 0 。
/* * 切片不存储任何数据,它只描述底层数组的一部分。 更改切片的元素会修改其基础数组的相应元素。 共享相同底层数组的其他切片将看到这些更改。 * 切片就像是对数组的引用。 */ //数组 names := [5]string{ "0_John", "1_Paul", "2_George", "3_Ringo", } //构造一个对 names 数组的切片,[n:m] 说明引用哪部分 slice := names[0:] //slice 由函数 make 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组: a := make([]int, 5) // len(a)=5 //为了指定容量,可传递第三个参数到 `make`: b := make([]int, 0, 5) // len(b)=0, cap(b)=5 b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4 复制代码
[n:m]
用法 slice[n:m]
表示从 n
到 m-1
的 slice 元素
, 含两端 。
slice[n:n]
是 空的 ,即 nil
。
slice[n:n+1]
有 一个元素 。
slice[:m]
表示从 0
到 m-1
的 slice 元素
, 含两端 。
slice[n:]
表示从 n
到 len(slice)
的 slice 元素
, 含两端 。
/* * 将新元素附加到一个切片上是很常见的,因此Go提供了一个内置的append函数。 func append(s T,vs.T)T的第一个参数s是一个类型T的切片,其余的是T值附加到片上。 * append的结果是一个包含原始切片的所有元素和新提供的值的切片。 如果s的后备数组太小,不足以容纳所有给定的值,那么就会分配更大的数组。 返回的切片将指向新分配的数组。 */ slice = append(slice, 2, 3, 4) /*slice1为另一个切片, *后面必须加 ... , *这样才能将该切片指向的数组里的元素放入 append */ slice = append(slice,slice1...) 复制代码
map
映射键到值。
map
在使用之前必须创建;值为 nil
的 map
是空的,并且 不能赋值 。
//map[键的类型]值的类型 var m map[string]int 复制代码
/* * 就是键值对儿,map的零值为nil。 nil的map没有键,也不能添加键。 make函数会 返回 指定类型的map,初始化并准备好使用。 make(map[键的类型]值的类型) */ m = make(map[string]int) var ma = map[string]int{ "Bell Labs":1, "Google": 2, } //如果顶级类型只是一个类型名,可以省略它。 //如下:省略了Ver(结构体)。 var mapa = map[string]Ver{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408}, } 复制代码
//设置键值对,没有key就新增 key:value 对, //key 已存在就覆盖 value 值 m[键]= 值 m["Bell Labs"] = 1 // 取值 v 为 key 对应的 value ,ok为该key是否存在的 bool 值,存在为 true, //否则 false。 key 不存在时 v 为其类型对应的零值。 v, ok := m["Answer"] //删除 key:value delete(m, "Answer") 复制代码
Go 的枚举用法
//1、iota常量 自动生成器,每隔一行,自动累加1 //2、iota给常量赋值使用 const ( a = iota //0 b = iota //1 c = iota //2 ) //3、iota遇到const,重置为0 const d = iota //0 //4、可以只写一个iota const ( e = iota //0 f //1 g //2 ) //5,如果是在同一行,值都一样 const ( h = iota //0 i,j,k = iota,iota,iota //1,1,1 l=iota //2 ) 复制代码
感觉这是 Go 的一个 bug 吧,要是别人传个数值型的进来就 gg 了。
error 是一个接口。
标准库里的定义如下:
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string } 复制代码
只要实现了这个接口都是实现了 error 。
很多时候都要在函数的多返回值里接收error,判断是否有错误。
在 Go 里面 error 基本就是个字符串,用惯了 Java 的异常机制,还真不习惯这个。
GO 里的指针不像 c/c++ 没有指针运算。
指针的用法: *变量=变量的值 &变量=变量的内存地址
i, j := 42, 2701 p := &i // point to i 指向i fmt.Println(*p) // read i through the pointer 通过指针读取i fmt.Println(&p) // p的内存地址 fmt.Println(&i) // i的内存地址 *p = 21 // set i through the pointer 通过指针设置i fmt.Println(i) // see the new value of i 看下i的新值 p = &j // point to j 指向j *p = *p / 37 // divide j through the pointer 通过指针进行除法运算 fmt.Println(j) // see the new value of j 看下j的新值 复制代码
函数也是值。它们可以像其他值一样传递。函数值可以用作函数参数和返回值。
//compute 函数 的参数 是一个函数, //给这个函数 取了个变量名fn,fn函数的类型为 float64, //compute函数的返回值也是float64。 func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) } 复制代码
//这里是定义了一个函数hypot,两个参数 //和 一个返回值 全为float64类型。 x*x+y*y,再开方 hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(5, 12)) // 5*5+12*12,开方 结果为13 fmt.Println(compute(hypot)) //把hypot这个函数 作为参数 传给函数compute 。3*3+4*4=25,开方,结果为5。 fmt.Println(compute(math.Pow)) //内置函数 Pow(x, y float64) 计算x的y次方,3的4次方 结果为81。 复制代码
//函数的返回值是一个匿名函数,返回一个函数类型 func(int) int func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } /* * 返回值为一个匿名函数,返回一个函数类型, 通过f来调用返回的匿名函数,f来调用闭包函数。 * 它不关心这些捕获了的变量和常量是否已超出作用域, 只要闭包还在使用它,这些变量就还会存在。 */ f:=adder() fmt.Println(f(1)) //结果1 fmt.Println(f(2)) //结果3 fmt.Println(f(3)) //结果6 复制代码
参考文档 第11章:接口(interface)与反射(reflection)
Go 的接口和 Java 类似,也是声明了一些方法,用来约束实现该接口的类(结构体)的行为。
Go 的接口不需要显示继承,只要结构体实现了接口声明的方法,就是实现了该接口。
Go 中接口可以通过组合的方式将其他接口声明的方法嵌套进接口当中。
type Namer interface { Method1(param_list) return_type Method2(param_list) return_type ... } //嵌套接口 type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } type Interface interface { sort.Interface Push(x interface{}) Pop() interface{} } // 等于 type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) Push(x interface{}) Pop() interface{} } 复制代码
上面的 Namer
是一个 接口类型 。
(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namer,ai是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
接口也是一种类型,所以该类型的变量可以被赋值该接口的实现。
package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { sq1 := new(Square) sq1.side = 5 // var areaIntf Shaper // areaIntf = sq1 // shorter,without separate declaration: // areaIntf := Shaper(sq1) // or even: areaIntf := sq1 fmt.Printf("The square has area: %f/n", areaIntf.Area()) } 复制代码
接口变量里的类型可能任何类型的值,如果需要在运行时判断出究竟是什么类型,可以用类型断言。
/* * T 是你需要断言的类型, *当 ok 为 true ,v 就是转换到 T 类型的值, *当 ok 为false ,v 就是 T 类型的零值。 */ if v, ok := varI.(T); ok { // checked type assertion Process(v) return } // varI is not of type T 复制代码
接口变量的类型也可以使用一种特殊形式的 swtich
来检测: type-swtich
switch t := areaIntf.(type) { case *Square: fmt.Printf("Type Square %T with value %v/n", t, t) case *Circle: fmt.Printf("Type Circle %T with value %v/n", t, t) case nil: fmt.Printf("nil value: nothing to check?/n") default: fmt.Printf("Unexpected type %T/n", t) } 复制代码
type-switch
不允许有 fallthrough
。
空接口不声明任何方法。类似于 Java 中的 Object 类,Go 中任何类型都实现了空接口。
用法也和 Java 中的 Object 类似,毕竟现在 Go 里面没有泛型,希望官方早点加入这个特性。
参考文档: 第10章:结构(struct)与方法(method)
Go 里面没有类的概念,自然也没有继承的概念。
所幸还有接口,所以可以用结构体+接口+方法+组合,实现oop。
/* * 结构的定义 */ type Vertex struct { X int Y int } //带标签(tag)的结构体 type TagType struct { // tags field1 bool "An important answer" field2 string "The name of the thing" field3 int "How much there are" } 复制代码
标签( tag
): 它是一个附属于字段的字符串,可以是文档或其他的重要标记。
标签的内容不可以在一般的编程中使用,只有包 reflect
能获取它。
它可以在运行时自省类型、属性和方法。
比如:在一个变量上调用 reflect.TypeOf()
可以获取变量的正确类型。
如果变量是一个 结构体类型 ,就可以通过 Field
来 索引结构体的字段 ,然后就可以使用 Tag
属性。
结构体可以包含一个或多个 匿名(或内嵌)字段 。
即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。
匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体 。
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。
Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
示例1:
package main import "fmt" type innerS struct { in1 int in2 int } type outerS struct { b int c float32 int // anonymous field innerS //anonymous field } func main() { outer := new(outerS) outer.b = 6 outer.c = 7.5 outer.int = 60 outer.in1 = 5 outer.in2 = 10 fmt.Printf("outer.b is: %d/n", outer.b) fmt.Printf("outer.c is: %f/n", outer.c) fmt.Printf("outer.int is: %d/n", outer.int) fmt.Printf("outer.in1 is: %d/n", outer.in1) fmt.Printf("outer.in2 is: %d/n", outer.in2) // 使用结构体字面量 outer2 := outerS{6, 7.5, 60, innerS{5, 10}} fmt.Printf("outer2 is:", outer2) } 复制代码
输出:
outer.b is: 6 outer.c is: 7.500000 outer.int is: 60 outer.in1 is: 5 outer.in2 is: 10 outer2 is:{6 7.5 60 {5 10}} 复制代码
示例2:
package main import "fmt" type A struct { ax, ay int } type B struct { A bx, by float32 } func main() { b := B{A{1, 2}, 3.0, 4.0} fmt.Println(b.ax, b.ay, b.bx, b.by) fmt.Println(b.A) } 复制代码
输出:
1 2 3 4 {1 2} 复制代码
通过类型 outer.int
的名字来获取存储在匿名字段中的数据,
于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。
一般 Go 里有函数和方法两种说法,本质上是一样的,只是方法是独属于一个结构体的函数,只能通过结构体的变量用 .
去调用。
定义如下:
func (recv struct_type) methodName(parameter_list) (return_value_list) { ... } 复制代码
在方法名之前, func
关键字之后的括号中指定 receiver。
如果 recv
是 struct_type
的实例, Method1
是它的方法名,那么方法调用遵循传统的 object.name
选择器符号: recv.Method1()
。
如果 recv
一个 指针 ,Go 会自动解引用。
如果方法不需要使用 recv
的值,可以用 _
替换它,比如:
func (_ struct_type) methodName(parameter_list) (return_value_list) { ... } 复制代码
recv
就像是面向对象语言中的 this
或 self
,但是 Go 中并没有这两个关键字。
可以使用 this 或 self 作为 receiver
的名字。
v1.X = 4 //设置结构变量的值 fmt.Println("第 2 行:",v1.X) //读取结构变量的值 p := &v1 //p指向 v1的内存地址 p.X = 1e9 //为p的X变量 重新赋值 fmt.Println("第 3 行:",v1) //看下v1的新值 复制代码
Go 提供的容器数据类型,在container包中。
堆结构
Go 中只定义了接口,没有给出实现。
type Interface interface { sort.Interface Push(x interface{}) // add x as element Len() Pop() interface{} // remove and return element Len() - 1. } 复制代码
如果想使用堆,只要实现以下五个方法就能实现该接口。
Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) Push(x interface{}) // add x as element Len() Pop() interface{} // remove and return element Len() - 1. 复制代码
双向链表
标准库里的定义如下:
// List represents a doubly linked list. // The zero value for List is an empty list ready to use. type List struct { root Element // sentinel list element, only &root, root.prev, and root.next are used len int // current list length excluding (this) sentinel element } 复制代码
// Element is an element of a linked list. type Element struct { // Next and previous pointers in the doubly-linked list of elements. // To simplify the implementation, internally a list l is implemented // as a ring, such that &l.root is both the next element of the last // list element (l.Back()) and the previous element of the first list // element (l.Front()). next, prev *Element // The list to which this element belongs. list *List // The value stored with this element. Value interface{} } 复制代码
list对应的方法有:
type Element func (e *Element) Next() *Element func (e *Element) Prev() *Element type List func New() *List func (l *List) Back() *Element // 最后一个元素 func (l *List) Front() *Element // 第一个元素 func (l *List) Init() *List // 链表初始化 func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在某个元素后插入 func (l *List) InsertBefore(v interface{}, mark *Element) *Element // 在某个元素前插入 func (l *List) Len() int // 在链表长度 func (l *List) MoveAfter(e, mark *Element) // 把e元素移动到mark之后 func (l *List) MoveBefore(e, mark *Element) // 把e元素移动到mark之前 func (l *List) MoveToBack(e *Element) // 把e元素移动到队列最后 func (l *List) MoveToFront(e *Element) // 把e元素移动到队列最头部 func (l *List) PushBack(v interface{}) *Element // 在队列最后插入元素 func (l *List) PushBackList(other *List) // 在队列最后插入接上新队列 func (l *List) PushFront(v interface{}) *Element // 在队列头部插入元素 func (l *List) PushFrontList(other *List) // 在队列头部插入接上新队列 func (l *List) Remove(e *Element) interface{} // 删除某个元素 复制代码
双向循环链表
双向链表和双向循环链表的结构示意图:
标准库里的定义如下:
// A Ring is an element of a circular list, or ring. // Rings do not have a beginning or end; a pointer to any ring element // serves as reference to the entire ring. Empty rings are represented // as nil Ring pointers. The zero value for a Ring is a one-element // ring with a nil Value. // type Ring struct { next, prev *Ring Value interface{} // for use by client; untouched by this library } 复制代码
环的结构有点特殊,环的尾部就是头部,所以每个元素实际上就可以代表自身的这个环。 它不需要像list一样保持list和element两个结构,只需要保持一个结构就行。
ring提供的方法有:
type Ring func New(n int) *Ring // 初始化环 func (r *Ring) Do(f func(interface{})) // 循环环进行操作 func (r *Ring) Len() int // 环长度 func (r *Ring) Link(s *Ring) *Ring // 连接两个环 func (r *Ring) Move(n int) *Ring // 指针从当前元素开始向后移动或者向前(n可以为负数) func (r *Ring) Next() *Ring // 当前元素的下个元素 func (r *Ring) Prev() *Ring // 当前元素的上个元素 func (r *Ring) Unlink(n int) *Ring // 从当前元素开始,删除n个元素 复制代码
参考资料 飞雪无情的博客
type D = int // 类型别名 type I int // 类型声明 复制代码
类型别名有 =
号,类型声明没有。
类型别名其实就是给原本的类型起多一个名字。 原本的类型该怎么用,该类型基本就怎么用(除非用类型别名改变了可导出性)。
这个特性的主要目的是用于已经定义的类型,在package之间的移动时的兼容。 比如我们有一个导出的类型 flysnow.org/lib/T1
,现在要迁移到另外一个 package flysnow.org/lib2/T1
中。
没有 type alias
的时候我们这么做,会导致其他第三方引用旧的package路径的代码,要统一修改,不然无法使用。
有了 type alias
就不一样了,类型 T1
的实现我们可以迁移到 lib2
下,
同时我们在原来的 lib
下定义一个 lib2
下 T1
的别名,
这样第三方的引用就可以不用修改,也可以正常使用,
只需要兼容一段时间,再彻底的去掉旧的package里的类型兼容,
这样就可以渐进式的重构我们的代码,而不是一刀切。
类型定义就是基于原本的类型创建了一个新类型,新类型和原本的类型是两个类型了。
这个特性多用于,你想给类型 a
添加新方法,但是类型 a
非本地类型,就无法添加。
这时,你可以用类型定义将其定义为类型 b
,这样就能添加新方法了。