Go语言学习笔记-数据类型

stormbuf · · 598 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

记录一下 GO 里面的一些数据类型。

数组

类型 [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 是一个元素类型为 Tslice

一个 slice 会指向一个序列的值,并且包含了长度信息。

说人话,slice 其实类似于 Java 里的 ArrayList ,就是一个动态数组。

不同点在于,Java 里的 ArrayList 自己内部维护一个数组, slice 即可以内部维护一个数组,也可以在现有的数组上创造 slice

每次扩容 slice 会指向一个扩容后的数组。

nil slice

slice 的零值是 nil

一个 nilslice 的长度和容量是 0

构造 slice


/*
	 * 切片不存储任何数据,它只描述底层数组的一部分。 更改切片的元素会修改其基础数组的相应元素。 共享相同底层数组的其他切片将看到这些更改。
	 * 切片就像是对数组的引用。
     */
     //数组
	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
复制代码

slice [n:m] 用法

  • slice[n:m] 表示从 nm-1slice 元素含两端

  • slice[n:n]空的,即 nil

  • slice[n:n+1]一个元素

  • slice[:m] 表示从 0m-1slice 元素含两端

  • slice[n:] 表示从 nlen(slice)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 映射键到值。

map 在使用之前必须创建;值为 nilmap 是空的,并且不能赋值

map 变量声明

//map[键的类型]值的类型
var m map[string]int
复制代码

map 构造

/*
 *  就是键值对儿,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")
复制代码

iota

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

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
复制代码

类型判断:type-switch

接口变量的类型也可以使用一种特殊形式的 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。

如果 recvstruct_type 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()

如果 recv 一个指针,Go 会自动解引用。

如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:

func (_ struct_type) methodName(parameter_list) (return_value_list) { ... }
复制代码

recv 就像是面向对象语言中的 thisself,但是 Go 中并没有这两个关键字。

可以使用 thisself 作为 receiver 的名字。

使用结构体

v1.X = 4				//设置结构变量的值
fmt.Println("第 2 行:",v1.X)	//读取结构变量的值

p := &v1		//p指向 v1的内存地址
p.X = 1e9		//为p的X变量 重新赋值
fmt.Println("第 3 行:",v1)	//看下v1的新值
复制代码

container

Go 提供的容器数据类型,在container包中。

heap

堆结构

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

双向链表

标准库里的定义如下:

// 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{} // 删除某个元素
复制代码

ring

双向循环链表

双向链表和双向循环链表的结构示意图:

标准库里的定义如下:

// 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下定义一个lib2T1的别名,

这样第三方的引用就可以不用修改,也可以正常使用,

只需要兼容一段时间,再彻底的去掉旧的package里的类型兼容,

这样就可以渐进式的重构我们的代码,而不是一刀切。

类型定义

类型定义就是基于原本的类型创建了一个新类型,新类型和原本的类型是两个类型了。

这个特性多用于,你想给类型 a 添加新方法,但是类型 a 非本地类型,就无法添加。

这时,你可以用类型定义将其定义为类型 b ,这样就能添加新方法了。


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:stormbuf

查看原文:Go语言学习笔记-数据类型

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

598 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传