Golang数据类型

基本数据类型

数据类型

  • bit计算机内部数据存储最小单位
  • byte 计算机中数据处理的基本单位
  • 计算机中以字节位单位存储和解释信息,规定一个字节由八个二进制位构成, 即一个字节等于8个比特(1Byte=8bit)

查看变量的数据类型

    var n = 100
    fmt.Printf("n 的数据类型是 %T", n)

查看变量的字节大小

    var n = 100
    fmt.Printf("n 的字节大小是 %d byte", unsafe.Sizeof(n))

整数类型

整型分为以下两个大类:

  • 按长度分为:int8、int16、int32、int64
  • 还有对应的无符号整型:uint8、uint16、uint32、uint64
类型 有无符号 占用存储空间 表数范围 备注
int 32byte/64byte 根据计算机架构决定
int8 1byte=8bit -128 ~ 127
int16 2byte=16bit -32768 ~ 32767
int32 4byte=32bit -2147483648 ~ 2147483647
int64 8byte=64bit -9223372036854775808 ~ 9223372036854775807
uint 32byte/64byte 根据计算机架构决定
uint8 1byte=8bit 0 ~ 255
uint16 1byte=16bit 0 ~ 65535
int32 4byte=32bit 0 ~ 4294967295
int64 8byte=64bit 0 ~ 18446744073709551615
uintptr 没有指定具体的bit大小 用于存放一个指针
  • uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

  • 其中,uint8 就是我们熟知的 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long 型。

  • Go 语言也有自动匹配特定平台整型长度的类型—— int 和 uint。

  • 逻辑对整型范围没有特殊需求时可以使用 int 和 uint。反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint。

  • 整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量使用占用空间小的数据类型。

byte与rune

byte与rune都属于别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
一个rune的类型值即可表示一个Unicode字符。一个Unicode代码点通常由"U+"和一个以十六进制表示法表示的整数表示,例如英文字母'A'的Unicode代码点为"U+0041"。
byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicode或utf-8字符

类型 描述 用途
byte 类似 uint8 常用来处理ascii字符
rune 类似 int32 常用来处理unicode或utf-8字符

浮点类型

Go语言支持两种浮点型数:float32 和 float64。这两种浮点型数据格式遵循 IEEE 754 标准:

  • float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。
  • float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。
  • 浮点型的存储分为三部分:符号位+指数位+尾数位,在存储过程中,尾数部分可能丢失,造成精度损失
  • golang的浮点型默认为float64类型
  • 通常情况下,应该使用float64,因为它比float32更精确
  • 0.123可以简写成.123,也支持科学计数法表示:5.1234e2 等价于512.34
类型 描述 精度
float32 IEEE-754 32位浮点型数 单精度
float64 IEEE-754 64位浮点型数 双精度

打印浮点数时,可以使用 fmt 包配合动词%f,代码如下:

    package main
    import (
            "fmt"
            "math"
    )
    func main() {
            fmt.Printf("%f\n", math.Pi)     //按默认宽度和精度输出整型。
            fmt.Printf("%.2f\n", math.Pi)   //按默认宽度,2 位精度输出(小数点后的位数)。
    }

复数类型

复数类型有两个:complex64和complex128。实际上,complex64类型的值会由两个float32类型的值分别表示复数的实数部分和虚数部分。而complex128类型的值会由两个float64类型的值表示复数的实数部分和虚数部分。
复数类型的值一般由浮点数表示的实数部分、加号"+"、浮点数表示的虚数部分以及小写字母"i"组成,比如3.9E+1 + 9.99E-2i。

类型 结构
complex32 float32实部+虚部
complex64 float64实部+虚部

字符类型

Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的,也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的。

  • 字符只能被单引号包裹,不能用双引号,双引号包裹的是字符串
  • 当我们直接输出type值时,就是输出了对应字符的ASCII码值
  • 当我们希望输出对应字符,需要使用格式化输出
  • Go语言的字符使用UTF-8编码,英文字母占一个字符,汉字占三个字符
  • 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值。
  • 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
  • 字符类型是可以运算的,相当于一个整数,因为它们都有对应的unicode码

但是如果我们保存的字符大于255,比如存储汉字,这时byte类型就无法保存,此时可以使用uint或int类型保存

字符类型本质探讨

字符型存储到计算机中,需要将字符对应的码值(整数)找出来
存储:字符 --> 码值 --> 二进制 --> 存储
读取: 二进制 -->码值 --> 字符 --> 读取

字符和码值的对应关系是通过字符编码表决定的(是规定好的)
Go语言的编码都统一成了UTF-8。非常的方便,很统一,再也没有编码乱码的困扰了

布尔类型

布尔型数据在 Go 语言中以 bool 类型进行声明,布尔型数据只有 true(真)和 false(假)两个值。

  • bool类型占1个字节
  • bool类型适用于逻辑运算,一般用于流程控制
  • Go 语言中不允许将整型强制转换为布尔型, cannot convert n (type bool) to type int
  • 布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串类型

字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本

  • 字符串一旦赋值了,就不能修改了:在Go中字符串是不可变的
  • 字符串的两种标识形式
  • 双引号,会识别转义字符
    var str = "abc\nabc" //输出时会换行
  • 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
    var str string = `abc\nabc` //输出时原样输出,不会转义
  • 字符串拼接方式"+"
    var str string = "hello " + "world"
    str += "!"
  • 当一行字符串太长时,需要使用到多行字符串,可以使用如下处理
    //正确写法
    str := "hello" + 
            " world!"
    fmt.Println(str)

    //错误写法
    str := "hello "
            + "world!"
    fmt.Println(str)

基本数据类型默认值与数据类型转换

基本数据类型默认值

在Golang中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在Golang中,默认值也叫做零值。
基本数据类型默认值如下:

数据类型 默认值
整型 0
浮点型 0
字符串 ""
布尔类型 false

数据类型转换

Golang和Java/C不同,Golang在不同类型的变量之间赋值时需要显式转换。也就是Golang中数据类型不能自动转换。

基本语法:
表达式var_type(var_name) 将值v转换为类型var_type
var_type:就是数据类型,比如int32, int64, float32等等
var_name:就是需要转换的变量

    var num int = 42
    var float float64 = float64(num)
    var ui uint8 = uint8(float)
    fmt.Println(num, float, ui)

注意事项

  • Go中,数据类型的转换可以是从表示范围小-->表示范围大,也可以 范围大—>范围小
  • 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
  • 在转换中,比如将int64转成int8,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。
  • 数据的转换必须显式转换,不能自动转换
  • 定义一个int8类型的整数(var num int8 = 0),如果一直自加1,这个变量的值会是(0...127 -128 -127... 0 ...127)循环往复下去,而不会超过类型最大值的范围
  • 其他基本类型转string类型

在程序开发中,我们经常需要将数值型转成string类型,或者将string类型转成数值型。

  • 方式1:
   func Sprintf(format string, a ...interface{}) string

Sprintf根据format参数生成格式化的字符串并返回该字符串。
示例

   package main
   import "fmt"
   
   func main() {
       var num1 int = 99;
       var num2 float64 = 23.456
       var isTrue bool = true 
       var char byte = 'A'
   
       var str string
   
       str = fmt.Sprintf("%d", num1)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   
       str = fmt.Sprintf("%f", num2)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   
       str = fmt.Sprintf("%t", isTrue)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   
       str = fmt.Sprintf("%d", char)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   }

输出结果为

 str类型为 string str = "99"
 str类型为 string str = "23.456000"
 str类型为 string str = "true"
 str类型为 string str = "65"
  • 方式2:使用strconv包的函数
   package main
   import (
      "fmt"
      "strconv"
   )
   
   func main() {
       var num1 int = 99;
       var num2 float64 = 23.456
       var isTrue bool = true 
       var str string
       str = strconv.FormatInt(int64(num1), 10)
       str = strconv.Itoa(num1)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   
       str = strconv.FormatFloat(num2, 'f', 10, 64)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   
       str = strconv.FormatBool(isTrue)
       fmt.Printf("str类型为 %T str = %q\n",str, str)
   }

输出结果为

   str类型为 string str = "99"
   str类型为 string str = "23.4560000000"
   str类型为 string str = "23.4560000000"
   str类型为 string str = "true"
  • string类型转其他基本类型
  • 方式1:使用strconv包的函数
   package main
   import (
       "fmt"
       "strconv"
   )
   
   func main() {
       var str string = "true"
       var str1 string = "123456"
       var str2 string = "123.456"
   
       var isTrue bool
       var num int64
       var num2 float64
   
       isTrue, _ = strconv.ParseBool(str)
       fmt.Printf("str类型为 %T str = %v\n",isTrue, isTrue)
   
       num, _ = strconv.ParseInt(str1, 10, 64)
       fmt.Printf("str类型为 %T str = %v\n",num, num)
   
       num2, _ = strconv.ParseFloat(str2, 64)
       fmt.Printf("str类型为 %T str = %v\n",num2, num2) 
   }

数据结果为:

   str类型为 bool str = true
   str类型为 int64 str = 123456
   str类型为 float64 str = 123.456

注意:在将string类型转成其它基本数据类型时,要确保string类型能够转成有效的数据。比如,我们可以把”123“转成数字123,但是不能把”hello“转成一个整数,如果这样做,Golang直接将其转成0,其它类型也是一样的道理,float => 0, bool => false

引用数据类型

指针

获取变量的地址,用&,比如var num int,获取num的地址:&num

    var a int = 10
    fmt.Println("a 的地址=" + &a)

指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值,比如:var ptr int = &num
获取指针类型所指向的值,使用:
,比如,var ptr int,使用ptr获取ptr指向的值

    var a int = 10                  //变量
    var ptr *int = &a               //指针变量
    fmt.Printf("ptr 的地址=%v \n", &ptr)
    fmt.Printf("ptr的值是 a 的地址=%v \n", &a)
    fmt.Printf("ptr 所指向地址的值=%v \n", *ptr)

值类型,都有对应的指针类型,形式为 *数据类型, 值类型包括:基本数据类型数组结构体struct

    var a int = 10                  //变量
    var ptr *int = &a               //指针变量
    fmt.Printf("ptr 的地址=%v \n", &ptr)

    var b float64 = .123
    var piont *float64 = &b
    fmt.Printf("piont 的地址=%v \n", &piont)

值类型与引用类型

区分

  • 值类型:基本数据类型(int系列、float系列、bool、string)、数组和结构体
  • 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型

使用特点

  • 值类型:变量直接存储值,内存通常中分配
  • 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常上分配,当没有任何变量应用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。

数组

数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。

  • 定义与赋值

var 数组名 [数组大小]数据类型

    var intArr [3]int //int占8个字节
	//当我们定义完数组后,其实数组的各个元素有默认值 0
	//赋值
	intArr[0] = 10
	intArr[1] = 20
	intArr[2] = 30

    //四种初始化数组的方式
	var numArr01 [3]int = [3]int{1, 2, 3}

	var numArr02 = [3]int{5, 6, 7}

	var numArr03 = [...]int{8, 9, 10}  //这里的 [...] 是规定的写法,不确定大小

	var numArr04 = [...]int{1: 800, 0: 900, 2:999} //下标赋值

    //类型推导
    strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"}

  • 数组在内存布局
  1. 数组的地址可以通过数组名来获取 &intArr
  2. 数组的第一个元素的地址,就是数组的首地址
  3. 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8 int32->4.
  • 数组遍历
  1. 常规 for循环
  2. for-range结构遍历:这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。
    heroes  := [...]string{"宋江", "吴用", "卢俊义"}
	for index, value := range heroes {
		fmt.Printf("index=%v value=%v\n", index , value)
		fmt.Printf("heroes[%d]=%v\n", index, heroes[index])
	}

	for _, v := range heroes {
		fmt.Printf("元素的值=%v\n", v)
	}

      1. 第一个返回值 index是数组的下标
      2. 第二个value是在该下标位置的值
      3. 他们都是仅在 for循环内部可见的局部变量
      4. 遍历数组元素的时 候,如果不想使用下标index,可以直接把下标index标为下划线_
      5. index和value的名称不是固定的,即程序员可以自行指定.一般命名为index和value

  • 注意事项
  1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
  2. var arr []int 声明一个数组没有定义长度,arr 就是一个 slice 切片
  3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
  4. 数组创建后,如果没有赋值,有默认值(零值)
    数值类型数组:默认值为 0
    字符串数组:默认值为 ""
    bool 数组: 默认值为 false
  1. 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
  2. 数组的下标是从 0 开始的
  3. 数组下标必须在指定范围内使用,否则报 panic:数组越界
  4. Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
  5. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
  6. 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度

多维数组

二维数组

  • 使用方式
  1. 先声明/定义,再赋值
    语法: var 数组名 [大小][大小]类型
    //定义/声明二维数组
	var arr [2][3]int
	//赋初值
	arr[1][2] = 1
	arr[2][1] = 2
	arr[2][3] = 3
  1. 直接初始化
    var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}

    二维数组在声明/定义时也对应有四种写法[和一维数组类似]
    var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
    var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}
    var 数组名 = [大小][大小]类型{{初值..},{初值..}}
    var 数组名 = [...][大小]类型{{初值..},{初值..}}

  1. 二维数组在内存的存在形式
    var arr [2][3]int //以这个为例来分析arr2在内存的布局!!
	arr[1][1] = 10

    fmt.Println(arr) //[[0 0 0] [0 10 0]]
	fmt.Printf("arr[0]的地址%p\n", &arr[0]) //arr[0]的地址0xc000018090
	fmt.Printf("arr[1]的地址%p\n", &arr[1]) //arr[1]的地址0xc0000180a8
    //0xc000018090和 0xc0000180a8 相差 3x8: 3个int元素 (1个int 8byte)

	fmt.Printf("arr[0][0]的地址%p\n", &arr[0][0]) //arr[0][0]的地址0xc000018090
    //&arr2[0] == &arr[0][0] 

	fmt.Printf("arr[1][0]的地址%p\n", &arr[1][0]) //arr[1][0]的地址0xc0000180a8
    &arr[1] == &arr[1][0]

    总结
    1. 二维数组内存形式存储的是指针
    2. 二维数组第一组存储的第一组第一个元素的地址,第二组存储的是第二组第一个元素的地址,依次类推
    3. 二维数组两组地址相差的是一组元素所占的字节
  1. 二维数组的遍历
  2. 双层 for 循环完成遍历
    var arr  = [2][3]int{{1,2,3}, {4,5,6}}

	for i := 0; i < len(arr); i++ {
		for j := 0; j < len(arr[i]); j++ {
			fmt.Printf("%v\t", arr[i][j])
		}
	}
  1. for-range 方式完成遍历
    var arr  = [2][3]int{{1,2,3}, {4,5,6}}
	for i, v := range arr {
		for j, v2 := range v {
			fmt.Printf("arr[%v][%v]=%v \t",i, j, v2)
		}
	}

切片

  • 定义
  1. 切片的英文是 slice
  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
  4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
  5. 切片定义的基本语法:
    //var 切片名 []类型
    var a [] int
  • 切片的内存形式
  1. slice 的确是一个引用类型
  2. slice 从底层来说,其实就是一个数据结构(struct 结构体)
    type slice struct {
        ptr *[2]int //截取数组开始位置的地址
        len int //截取的长度
        cap  //容量
    }
  • 切片的使用
  1. 定义一个切片,然后让切片去引用一个已经创建好的数组
    var slice = arr[startIndex:endIndex]
    说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。

    var slice = arr[0:end] 可以简写 var slice = arr[:end]
    var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
    var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
  1. 通过 make 来创建切片.
    基本语法:var 切片名 []type = make([]type, len, [cap])
    参数说明: type: 就是数据类型 len : 大小 cap :指定切片容量,可选(如果你分配了 cap, 则 cap>=len)

    1. 通过 make 方式创建切片可以指定切片的大小和容量
    2. 如果没有给切片的各个元素赋值,那么就会使用默认值[int , float=> 0   string =>""  bool =>false]
    3. 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素.
  1. 定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
    var strSlice []string = []string{"tom", "jack", "mary"}

方式 1 和方式 2 的区别

方式1是直接引用数组,这个数组是事先存在的,程序员是可见的。
方式2是通过make未创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是看不见的。

  • 切片的遍历
  1. for 循环常规方式遍历
    var arr [5]int = [...]int{10, 20, 30, 40, 50}
	slice := arr[1:4]
	for i := 0; i < len(slice); i++ {
		fmt.Printf("slice[%v]=%v ", i, slice[i])
	}
  1. for-range 结构遍历切片
    var arr [5]int = [...]int{10, 20, 30, 40, 50}
	slice := arr[1:4]
	for i, v := range slice {
		fmt.Printf("i=%v v=%v \n", i, v)
	}
  • 注意事项
  1. 切片初始化时 var slice = arr[startIndex:endIndex]
    说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。
  2. 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.
  3. cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
  4. 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make一个空间供切片来使用
  5. 切片可以继续切片
  6. 用 append 内置函数,可以对切片进行动态追加
    var slice []int = []int{100, 200, 300}
	//通过append直接给slice3追加具体的元素
	slice = append(slice, 400, 500, 600)
	//通过append将切片slice追加给slice
	slice = append(slice, slice...) 

    切片 append 操作的底层原理分析:
    1. 切片 append 操作的本质就是对数组扩容
    2. go 底层会创建一下新的数组 newArr(安装扩容后大小)
    3. 将 slice 原来包含的元素拷贝到新的数组 newArr
    4. slice 重新引用到 newArr
    5. 注意 newArr 是在底层来维护的,程序员不可见.

  1. 切片的拷贝:切片使用 copy 内置函数完成拷贝
    var slice1 []int = []int{1, 2, 3, 4, 5}
	var slice2 = make([]int, 10)
	copy(slice2, slice1)
	fmt.Println("slice1=", slice1)// 1, 2, 3, 4, 5
	fmt.Println("slice2=", slice2) // 1, 2, 3, 4, 5, 0 , 0 ,0,0,0

    对上面代码的说明:
    1. copy(para1, para2) 参数的数据类型是切片
    2. 按照上面的代码来看, slice4 和 slice5 的数据空间是独立,相互不影响,也就是说 slice4[0]= 999,slice5[0] 仍然是 1
  1. 切片是引用类型,所以在传递时,遵守引用传递机制。
    var arr [5]int = [...]int{10, 20, 30, 40, 50}
	slice1 := arr[1:4] // 20, 30, 40
    slice2 := slice1[1:2] //[30]
	slice2[0] = 100  // 因为arr , slice1 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化
  • string 和 slice
  1. string 底层是一个 byte 数组,因此 string 也可以进行切片处理
  2. string 的内存形式
    type slice struct {
        ptr *[4]byte //截取数组开始位置的地址
        len int //截取的长度
    }
  1. string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
  2. 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
    str := "hello@world"
    arr1 := []byte(str) 
	arr1[0] = 'z'
	str = string(arr1)

    //我们转成[]byte后,可以处理英文和数字,但是不能处理中文
	// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
	// 解决方法是 将  string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字

    arr1 := []rune(str) 
	arr1[0] = '北'
	str = string(arr1)

map

map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合

  • 基本语法
    var 变量名 map[keytype]valuetype
    
    keytype:
    golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组 通常 key 为 int 、string
    注意: slice, map, function 不可以,因为这几个没法用 == 来判断

    valuetype:
    valuetype 的类型和 key 基本一样
  • 声明和使用
  1. 声明:
    var a map[string]string
    var a map[string]int
    var a map[int]string
    var a map[string]map[string]string
    注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。
  1. 使用:
    方式一:
        var a map[string]string
        //在使用map前,需要先make , make的作用就是给map分配数据空间
        a = make(map[string]string, 10)
        a["no1"] = "宋江" 
        a["no2"] = "吴用" 
        a["no1"] = "武松" 
        a["no3"] = "吴用" 
    
    方式二:
        cities := make(map[string]string)
        cities["no1"] = "北京"
        cities["no2"] = "天津"
        cities["no3"] = "上海"
    
    方式三:
        heroes := map[string]string{
            "hero1" : "宋江",
            "hero2" : "卢俊义",
            "hero3" : "吴用",
        }
    
  • map 的增删改查操作
    cities := make(map[string]string)
    //增
	cities["no1"] = "北京" //如果 key 还没有,就是增加,如果 key 存在就是修改。
	cities["no2"] = "天津"
	cities["no3"] = "上海"

	//删
	delete(cities, "no1")
	//当delete指定的key不存在时,删除不会操作,也不会报错
	delete(cities, "no4")

    //改
    //因为 no3这个key已经存在,因此下面的这句话就是修改
	cities["no3"] = "西安" 

	//查
	val, ok := cities["no2"]
	if ok {
		fmt.Printf("有no1 key 值为%v\n", val)
	} else {
		fmt.Printf("没有no1 key\n")
	}

	//如果希望一次性删除所有的key
	//1. 遍历所有的key,逐一删除 [遍历]
	//2. 直接make一个新的空间
	cities = make(map[string]string)
  • map 遍历
    只能使用for-range遍历
    cities := make(map[string]string)
	cities["no1"] = "北京"
	cities["no2"] = "天津"
	cities["no3"] = "上海"
	
	for k, v := range cities {
		fmt.Printf("k=%v v=%v\n", k, v)
	}
  • map 切片
    //1. 声明一个map切片
	var monsters []map[string]string
	monsters = make([]map[string]string, 2)

    //2. 增加第一个妖怪的信息
	if monsters[0] == nil {
		monsters[0] = make(map[string]string, 2)
		monsters[0]["name"] = "牛魔王"
		monsters[0]["age"] = "500"
	}

    //3. 切片的append函数,可以动态的增加monster
	newMonster := map[string]string{
		"name" : "新的妖怪~火云邪神",
		"age" : "200",
	}
	monsters = append(monsters, newMonster)
  • map 排序
    map1 := make(map[int]int, 10)
	map1[10] = 100
	map1[1] = 13
	map1[4] = 56
	map1[8] = 90

    //如果按照map的key的顺序进行排序输出
    //1. 先将map的key 放入到 切片中
	var keys []int
	for k, _ := range map1 {
		keys = append(keys, k)
	}

	//2. 对切片排序 
	sort.Ints(keys)
	fmt.Println(keys)

    //3. 遍历切片,然后按照key来输出map的值
	for _, k := range keys{
		fmt.Printf("map1[%v]=%v \n", k, map1[k])
	}
  • 使用细节
  1. map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来
  2. map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
  3. map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),

结构体

  • Golang 语言面向对象编程说明
  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
  4. Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。
  • 结构体和结构体变量(实例)的区别和联系
  1. 结构体是自定义的数据类型,代表一类事物.
    type Cat struct {
        Name string 
        Age int 
        Color string 
        Hobby string
        Scores [3]int
    }
  1. 结构体变量(实例)是具体的,实际的,代表一个具体变量
    var cat1 Cat
	
	cat1.Name = "小白"
	cat1.Age = 3
	cat1.Color = "白色"
	cat1.Hobby = "吃<・)))><<"
  • 结构体变量(实例)在内存的布局
    var cat1 Cat  // var a int
	
	cat1.Name = "小白"
	cat1.Age = 3
	cat1.Color = "白色"
	cat1.Hobby = "吃<・)))><<"
	fmt.Printf("cat1的地址=%p\n", &cat1)

	fmt.Printf("cat1.Name的地址=%p\n", &cat1.Name)
	fmt.Printf("cat1.Age的地址=%p\n", &cat1.Age)
	fmt.Printf("cat1.Color的地址=%p\n", &cat1.Color)
	fmt.Printf("cat1.Hobby的地址=%p\n", &cat1.Hobby)

    结果:
    cat1的地址=0xc00007e000    //824634236928
    cat1.Name的地址=0xc00007e000 //824634236928 //string占位 16byte
    cat1.Age的地址=0xc00007e010 //824634236944 //int占位 8byte
    cat1.Color的地址=0xc00007e018 //824634236952
    cat1.Hobby的地址=0xc00007e028 //824634236968

    总结:
    &cat1 == &cat1.Name 
    &cat1.Name + 16 = &cat1.Age
    &cat1.Age + 8 = &cat1.Color
  • 声明结构体
    基本语法
    type 结构体名称 struct {
        字段1 type //结构体字段 = 属性 = field 
        字段2 type
    }
    举例:
    type Student struct { //结构体和字段名大写代表public,小写表示private
        Name string
        Age int
        Score float32
    }

    字段 :字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。
    字段细节说明
    1) 字段声明语法同变量,示例:字段名 字段类型
    2) 字段的类型可以为:基本类型、数组或引用类型
    3) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
        布尔类型是 false ,数值是 0 ,字符串是 ""。
        数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
        指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
    4) 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
  • 创建结构体变量和访问结构体字段
  1. 直接声明
    var person Person
    var person Person = Person{}

    举例:
    p2 := Person{"mary", 20}
    var person *Person = new (Person)
    (*p3).Name = "smith"  //(*p3).Name = "smith" 也可以这样写 p3.Name = "smith"
    /*
    * 原因: go的设计者 为了程序员使用方便,底层会对 p3.Name = "smith" 进行处理
	* 会给 p3 加上 取值运算 (*p3).Name = "smith"
    */
    var person *Person = &Person{}
    //var person *Person = &Person{"mary", 60} 
    (*person).Name = "scott"  //person.Name = "scott"
	(*person).Age = 88  //person.Age = 88
    
  • 注意事项
  1. 结构体的所有字段在内存中是连续的
    假如有两个 Point类型,这个两个Point类型的本身地址也是连续的,但是他们指向的地址不一定是连续
  2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
  3. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
  4. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
    package main 
    import "fmt"
    import "encoding/json"

    type Monster struct{
        Name string `json:"name"` // `json:"name"` 就是 struct tag
        Age int `json:"age"`
        Skill string `json:"skill"`
    }
    func main() {
        //1. 创建一个Monster变量
        monster := Monster{"牛魔王", 500, "芭蕉扇~"}

        //2. 将monster变量序列化为 json格式字串
        //   json.Marshal 函数中使用反射,这个讲解反射时,我会详细介绍
        jsonStr, err := json.Marshal(monster)
        if err != nil {
            fmt.Println("json 处理错误 ", err)
        }
        fmt.Println("jsonStr", string(jsonStr))
    }

方法

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。

  • 声明
    func (recevier type) methodName(参数列表) (返回值列表){
        方法体
        return 返回值
    }

    说明:
    1. 参数列表:表示数据类型调用传递给方法的参数
    2. recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
    3. receiver type : type 可以是结构体,也可以其它的自定义类型
    4. receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
    5. 返回值列表:表示返回的值,可以多个
    6. 方法主体:表示为了实现某一功能代码块
    7. return 语句不是必须的。

  • 调用
    type A struct {
        Num int
    }
    func (a A) test() {
        fmt.Println(a.Num)
    }
    func main() {
        var a A
        a.test() //调用方法
    }

    说明
    1. func (a A) test() {} 表示 A 结构体有一方法,方法名为 test
    2. (a A) 体现 test 方法是和 A 类型绑定的
    3. test 方法只能通过 A 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
    4.  func (a A) test() {}... a 表示哪个 A 变量调用,这个 a 就是它的副本, 这点和函数传参非常相似。
  • 方法的调用和传参机制原理
    方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)。
  • 注意事项
  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法
  4. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
  5. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出

方法和函数区别

  1. 调用方式不一样
    函数的调用方式: 函数名(实参列表)
    方法的调用方式: 变量.方法名(实参列表)
  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  3. 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
    type Person struct {
        Name string
    } 

    //函数
    func test01(p Person) {
        fmt.Println(p.Name)
    }
    func test02(p *Person) {
        fmt.Println(p.Name)
    }

    //方法
    func (p Person) test03() {
        p.Name = "jack"
        fmt.Println("test03() =", p.Name) // jack
    }
    func (p *Person) test04() {
        p.Name = "mary"
        fmt.Println("test03() =", p.Name) // mary
    }

    func main() {
        p := Person{"tom"}
        test01(p)
        test02(&p)

        p.test03()
        fmt.Println("main() p.name=", p.Name) // tom
        
        (&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝
        fmt.Println("main() p.name=", p.Name) // tom

        (&p).test04()
        fmt.Println("main() p.name=", p.Name) // mary
        p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝
        fmt.Println("main() p.name=", p.Name) // mary
    }

    总结:
    1. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
    2. 如果是和值类型,比如(p Person) , 则是值拷贝; 如果和指针类型,比如是 (p *Person) 则是地址拷贝。

管道

接口

posted @ 2020-09-02 19:01  养诚  阅读(224)  评论(0编辑  收藏  举报