[GO语言基础] Go的引用数据类型, 指针、数组、结构体和管道(九)

引用数据类型

指针

获取变量的地址,用&,比如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) 则是地址拷贝。

管道

接口

猜你喜欢

转载自blog.csdn.net/weixin_54707168/article/details/114005911