go小技巧(易错点)集锦

news2024/11/28 13:53:45

目录

  • len的魔力
    • 评论区大佬解答
    • 答案详解
  • 结构体是否相等
    • 答案解析:
    • 结构体比较规则
    • 举例
  • 常量的编译
    • 我的答案
    • 标准答案
    • 内存四区概念:
  • new关键字
    • 答案
  • iota的魔力
    • 结果
    • 解析
      • 可跳过的值
      • 定义在一行
      • 中间插队
    • 小结
    • iota详解
    • iota 原理
    • iota 规则
      • 依赖 const
      • 按行计数
      • 多个iota
      • 空行处理
      • 跳值占位
      • 开头插队
      • 中间插队
      • 一行多个iota
      • 小结
  • nil赋值
    • 答案解析:
  • 接口
    • 答案解析
      • 空接口
      • 非空接口 iface
      • 答案
  • 结构体json
    • 答案
    • 解析
    • json.Marshal 函数的文档
  • 类型断言
    • 答案
    • 解析
  • channel
    • 答案
    • 解析
  • sync.WaitGroup
    • 答案
    • 解析
  • 空map取值
    • 答案
    • 注意
  • 可变参数函数
    • 答案
    • 解析
    • 可变参数
      • 与传切片的区别
  • 结构体嵌套接口
    • 答案
    • 解析
      • Go 没有继承,但可以通过内嵌类型模拟部分继承的功能。
      • 实例
      • 回到题目
  • 切片的第三个参数
    • 答案
    • 第三个参数
  • %+d
    • 答案
    • 解析
  • 匿名结构体嵌套
    • 答案
    • 解析
  • defer的执行顺序
    • 答案解析:
  • interface{}接口类型
    • 答案
    • 解析
  • 切片扩容
    • 答案
    • 解析
    • 图示
  • 当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil
    • 答案解析
  • String()方法
    • 答案解析
  • map 的 value 是不可寻址的
    • 答案解析
    • 解决方案
  • 一个select死锁问题
    • 结果
    • 解析
    • 相同问题
    • 更多类似的问题
    • 最典型的问题
  • 循环中改变切片长度是否会死循环
    • 答案解析
    • python会
  • defer再探
    • 答案解析
  • range数组和range指针
    • 答案解析
    • 结论
  • 切片传参和切片扩容
    • 答案解析
  • range赋值地址
    • 答案解析
    • 同理如下
  • range map
    • 答案解析
  • 多重赋值的坑
    • 答案解析
  • 如何判断map是否相等
    • 答案
    • DeepEqual部分源码
  • Go 1.15 中 var i interface{} = a 会有额外堆内存分配吗?
    • 答案解析
    • 基准测试
  • GO结构体字典值拷贝和值引用
    • 答案解析:
      • 结果
      • 分析

len的魔力

package main

const s = "Go101.org"

// len(s) == 9
// 1 << 9 == 512
// 512 / 128 == 4

var a byte = 1 << len(s) / 128
var b byte = 1 << len(s[:]) / 128

func main() {
	println(a, b)
}
-----------------------------------
>>> 4 0

评论区大佬解答

  1. len(s)若s为字符串常量或者简单的数组表达式,则len返回的为int型的常量,若s为不为上述情况(有函数计算、通道等),则len返回的为int型的变量

  2. 关于位移操作,如果常量位移表达式的左侧操作数是一个无类型常量,那么其结果是一个整数常量,否则就是和左侧操作数同一类型的常量(必须是整数类型 );如果一个非常量位移表达式的左侧的操作数是一个无类型常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型。

  3. “先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型" :对于var b byte = 1 << len(s[:]),1 << len(s[:])会被隐式的转换为var b byte = 1(表达式被1单独替换)的类型,1为无类型常量,因为b为byte,所以1会被隐式转换为byte,所以1 << len(s[:])为byte类型。

  4. s为常量表达式。对于var a byte = 1 << len(s) / 128,len(s)返回整型常量,所以1 << len(s)为常量表达式,且1为无类型常量,符合位移操作的第一种情况,所以为表达式结果为整型常量,故最后计算为4;

  5. 对于var b byte = 1 << len(s[:]) / 128,s[:]为函数计算,所以len(s[:])返回的为整型变量,所以1 << len(s[:]) / 128为非常量表达式,且1为无类型常量,符合位移操作的第二种情况,所以为表达式结果为byte,1<<9为512,转为byte类型,结果溢出,为0。

答案详解

  1. len 是一个内置函数,在官方标准库文档关于 len 函数 有这么一句:

    For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification’s “Length and capacity” section for details.(当参数是字符串字面量和简单 array 表达式,len 函数返回值是常量)

  2. 内置函数 len 和 cap 获取各种类型的实参并返回一个 int 类型结果。实现会保证结果总是一个 int 值。如果 s 是一个字符串常量,那么 len(s) 是一个常量 。如果 s 类型是一个数组或到数组的指针且表达式 s 不包含信道接收或(非常量的) 函数调用的话, 那么表达式 len(s) 和 cap(s) 是常量,这种情况下, s 是不求值的。否则的话, len 和 cap 的调用结果不是常量且 s 会被求值

  3. 第一句的 len(s) 是常量(因为 s 是字符串常量);而第二句的 len(s[:]) 不是常量。这是这两条语句的唯一区别:两个 len 的返回结果数值并无差异,都是 9,但一个是常量一个不是。

    var a byte = 1 << len(s) / 128
    var b byte = 1 << len(s[:]) / 128
    
  4. 关于位移操作,在位移表达式的右侧的操作数必须为整数类型,或者可以被 uint 类型的值所表示的无类型的常量。如果一个非常量位移表达式的左侧的操作数是一个无类型常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型。
    这里的关键在于常量位移表达式。根据上文的分析,1 << len(s) 是常量位移表达式,而 1 << len(s[:]) 不是。

  5. 如果常量位移表达式的左侧操作数是一个无类型常量,那么其结果是一个整数常量;否则就是和左侧操作数同一类型的常量(必须是整数类型 )

    因此对于 var a byte = 1 << len(s) / 128,因为 1 << len(s) 是一个常量位移表达式,因此它的结果也是一个整数常量,所以是 512,最后除以 128,最终结果就是 4。

    而对于 var b byte = 1 << len(s[:]) / 128,因为 1 << len(s[:]) 不是一个常量位移表达式,而做操作数是 1,一个无类型常量,根据规范定义它是 byte 类型(根据:如果一个非常量位移表达式的左侧的操作数是一个无类型常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型)。

    所以 var b byte = 1 << len(s[:]) / 128 中,根据规范定义,1 会隐式转换为 byte 类型,因此 1 << len(s[:]) 的结果也是 byte 类型,而 byte 类型最大只能表示 255,很显然 512 溢出了,结果为 0,因此最后 b 的结果也是 0。

结构体是否相等

下面代码是否可以编译通过?为什么?

package main

import "fmt"

func main() {

	sn1 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}

	sn2 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}

	if sn1 == sn2 {
		fmt.Println("sn1 == sn2")
	}

	sm1 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	sm2 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	if sm1 == sm2 {
		fmt.Println("sm1 == sm2")
	}
}

答案解析:

编译不通过。

结构体比较规则

  1. 只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。
  2. 结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice,则结构体不能用==比较。

举例

sn1 := struct {
	age  int
	name string
}{age: 11, name: "qq"}

sn3:= struct {
    name string
    age  int
}{age:11, name:"qq"}

sn3与sn1就不是相同的结构体了,不能用等于号比较。但可以使用reflect.DeepEqual进行比较

if reflect.DeepEqual(sm1, sm2) {
		fmt.Println("sm1 == sm2")
} else {
		fmt.Println("sm1 != sm2")
}

常量的编译

下面代码有什么问题?

package main

const cl = 100

var bl = 123

func main()  {
    println(&bl,bl)
    println(&cl,cl)
}

我的答案

这个还是比较简单的,因为常量在预处理的时候直接替换到代码中,相当于在某处直接写了一个常量值。
在这里插入图片描述

标准答案

  1. 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。

内存四区概念:

  1. 数据类型本质:固定内存大小的别名

  2. 数据类型的作用:编译器预算对象(变量)分配的内存空间大小。

  3. 内存四区:

栈区(Stack):
空间较小,要求数据读写性能高,数据存放时间较短暂。由编译器自动分配和释放,存放函数的参数值、函数的调用流程方法地址、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区)

堆区(heap):
空间充裕,数据存放时间较久。一般由开发者分配及释放(但是Golang中会根据变量的逃逸现象来选择是否分配到栈上或堆上),启动Golang的GC由GC清除机制自动回收。

全局区-静态全局变量区:
全局变量的开辟是在程序在main之前就已经放在内存中。而且对外完全可见。即作用域在全部代码中,任何同包代码均可随时使用,在变量会搞混淆,而且在局部函数中如果同名称变量使用:=赋值会出现编译错误。全局变量最终在进程退出时,由操作系统回收。

全局区-常量区:
常量区也归属于全局区,常量为存放数值字面值单位,即不可修改。或者说的有的常量是直接挂钩字面值的。

const cl = 10
cl是字面量10的对等符号。

所以在golang中,常量是无法取出地址的,因为字面量符号并没有地址而言。

new关键字

下面这段代码能否通过编译?

func main() {
	list := new([]int)
	list = append(list, 1)
	fmt.Println(list)
}

答案

过于简单,new()的返回值是[]int类型的指针且为nil,对nil不可操作

iota的魔力

下面这段代码能否编译通过?如果可以,输出什么?

const (
	x = iota
	_
	y
	z = "zz"
	k 
	p = iota
)

func main()  {
	fmt.Println(x,y,z,k,p)
}

结果

在这里插入图片描述

解析

iota是golang语言的常量计数器,只能在常量的表达式中使用。
在这里插入图片描述
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。

可跳过的值

可以使用下划线跳过不想要的值。

const (
    OutMute AudioOutput = iota 	  // 0
    OutMono                       // 1
    OutStereo                     // 2
    _
    _
    OutSurround                   // 5
)

定义在一行

const (
    Apple, Banana = iota + 1, iota + 2
    Cherimoya, Durian
    Elderberry, Fig
)
-----------------------------------
// Apple: 1
// Banana: 2
// Cherimoya: 2
// Durian: 3
// Elderberry: 3
// Fig: 4

iota 在下一行增长,而不是立即取得它的引用。

中间插队

const (
    i = iota
    j = 3.14
    k = iota
    l
)

那么打印出来的结果是 i=0,j=3.14,k=2,l=3

小结

  1. iota初始值为0,出现一次const自增1
  2. 省略const语句时,代表与上一句相同
const (
	x = iota
	_
	y
	z = "zz"
	k 
	p = iota
)

写完整是:

const (
	x = iota  // 0
	_ = iota  // 1 _代表不需要这个值
	y = iota  // 2
	z = "zz"  // zz iota=3
	k = "zz"  // zz iota=4
	p = iota  // 5
)

值得注意的是:iota是在一个const递增的

package main

import "fmt"

const a = iota

const b = iota

func main() {
	fmt.Println(a,b)
}
-----------------------------
0 0

iota详解

iota 的主要使用场景用于枚举。Go 语言的设计原则追求极尽简化,所以没有枚举类型,没有 enum关键字。

Go 语言通常使用常量定义代替枚举类型,于是 iota 常常用于其中,用于简化代码。

package main

const (
  B  = 1 << (10 * iota) // 1 << (10*0)
  KB                    // 1 << (10*1)
  MB                    // 1 << (10*2)
  GB                    // 1 << (10*3)
  TB                    // 1 << (10*4)
  PB                    // 1 << (10*5)
  EB                    // 1 << (10*6)
  ZB                    // 7 << (10*5)
)


func main() {
  println(B, KB, MB, GB, TB)
}

const (
   B  = 1
   KB = 1024
   MB = 1048576
   GB = 1073741824
   ...
 )

不使用 iota 的代码,对于代码洁癖者来说,简直就是一坨,不可接受。

而 Go 语言的发明者,恰恰具有代码洁癖,而且还是深度洁癖。Go 语言设计初衷之一:追求简洁优雅。

iota 原理

iota 源码在 Go 语言代码库中,只有一句定义语句,

const iota = 0 // Untyped int.

iota 是一个预声明的标识符,它的值是 0。 在 const 常量声明中,作为当前 const 代码块中的整数序数。

package main

const (
  FirstItem = iota
  SecondItem
  ThirdItem
)

func main() {
  println(FirstItem)
  println(SecondItem)
  println(ThirdItem)
}

编译上述代码:

# 使用 -N -l 编译参数用于禁止内联和优化,防止编译器优化和简化代码,弄乱次序。这样便于阅读汇编代码。
go tool compile -N -l main.go

# 导出汇编代码:
go tool objdump main.o
TEXT %22%22.main(SB) gofile../Users/wangzebin/test/test/main.go
...
main.go:10    MOVQ $0x0, 0(SP)  // 对应源码 println(FirstItem)
main.go:10    CALL 0x33b [1:5]R_CALL:runtime.printint
...
main.go:11    MOVQ $0x1, 0(SP)  // 对应源码 println(SecondItem)
main.go:11    CALL 0x357 [1:5]R_CALL:runtime.printint
...
main.go:11    MOVQ $0x2, 0(SP)  // 对应源码 println(ThirdItem)
main.go:11    CALL 0x373 [1:5]R_CALL:runtime.printint
...

编译之后,对应的常量 FirstItem、SecondItem 和 ThirdItem,分别替换为$0x0、$0x1 和 $0x2。

这说明:Go代码中定义的常量,在编译时期就会被替换为对应的常量(所以根本不会有地址)。当然 iota,也不可避免地在编译时期,按照一定的规则,被替换为对应的常量。

所以,Go 语言源码库中是不会有 iota 源码了,它的魔法在编译时期就已经施展完毕。也就是说,解释 iota 的代码包含在 go 这个命令和其调用的组件中。

如果你要阅读它的源码,准确的说,阅读处理 iota 关键字的源码,需要到 Go 工具源码库中寻找,而不是 Go 核心源码库。

iota 规则

使用 iota,虽然可以书写简洁优雅的代码,但对于不熟悉规则的人来讲,又带来的很多不必要的麻烦和误解。

对于引入 iota,到底好是不好,每个人都有自己的评价。实际上,有些不常用的写法,甚至有些卖弄编写技巧的的写法,并不是设计者的初衷。

大多数情况下,我们还是使用最简单最明确的写法,iota 只是提供了一种选择而已。一个工具使用的好坏,取决于使用它的人,而不是工具本身。

以下是 iota 编译规则:

依赖 const

iota 依赖于 const 关键字,每次新的 const 关键字出现时,都会让 iota 初始化为0。

const a = iota // a=0
const (
  b = iota     // b=0
  c            // c=1
)

按行计数

iota 按行递增加 1。

const (
  a = iota     // a=0
  b            // b=1
  c            // c=2
)

多个iota

同一 const 块出现多个 iota,只会按照行数计数,不会重新计数。

const (
   a = iota     // a=0
   b = iota     // b=1
   c = iota     // c=2
 )

空行处理

空行在编译时期首先会被删除,所以空行不计数。

const (
   a = iota     // a=0


   b            // b=1
   c            // c=2
 )

跳值占位

占位 “_”,它不是空行,会进行计数,起到跳值作用。

const (
  a = iota     // a=0
  _            // _=1
  c            // c=2
)

开头插队

开头插队会进行计数。

const (
    i = 3.14 // i=3.14
    j = iota // j=1
    k = iota // k=2
    l        // l=3
)

中间插队

中间插队会进行计数。

const (
    i = iota // i=0
    j = 3.14 // j=3.14
    k = iota // k=2
    l        // l=3
)

一行多个iota

一行多个iota,分别计数。

const (
    i, j = iota, iota // i=0,j=0
    k, l              // k=1,l=1
)

小结

对于20221016中文网给出的每日一题来说,这样的iota用法完成就是一坨大便,就如同大家痛恨的某cpp书中a++--++之类的代码,如果用Go的思想去看,你的代码写成这样只说明了两点:1. 你的代码出问题了 2. 你人出问题了。

nil赋值

下面赋值正确的是:

A. var x = nil
B. var x interface{} = nil
C. var x string = nil
D. var x error = nil

答案解析:

参考答案及解析:BD。
这道题考的知识点是 nil。nil 只能赋值给指针、chan、func、interface、map 或 slice 类型的变量。强调下 D 选项的 error 类型,它是一种内置接口类型,看它的源码就知道,所以 D 是对的。

type error interface {
	Error() string
}

接口

以下代码打印出来什么内容

package main

import (
	"fmt"
)

type People interface {
	Show()
}

type Student struct{}

func (stu *Student) Show() {

}

func live() People {
	var stu *Student
	return stu
}

func main() {
	if live() == nil {
		fmt.Println("AAAAAAA")
	} else {
		fmt.Println("BBBBBBB")
	}
}

在这里插入图片描述

答案解析

interface 在使用的过程中,共有两种表现形式:一种为空接口(empty interface),
另一种为非空接口(non-empty interface)

空接口

空接口 eface 结构,由两个属性构成,一个是类型信息 _type,一个是数据信息。

type eface struct {      // 空接口
    _type *_type         // 类型信息
    data  unsafe.Pointer // 指向数据的指针
}

非空接口 iface

face 结构中最重要的是 itab 结构(结构如下),每一个 itab 都占 32 字节的空间。

type iface struct {
  tab  *itab
  data unsafe.Pointer
}

itab 里面包含了 interface 的一些关键信息,比如 method 的具体实现。

type itab struct {
  inter  *interfacetype   // 接口自身的元信息
  _type  *_type           // 具体类型的元信息
  hash   int32            // _type 里也有一个同样的 hash,此处多放一个是为了方便运行接口断言
  _      [4]byte
  fun    [1]uintptr       // 函数指针,指向具体类型所实现的方法
}

答案

所以,People 拥有一个 Show 方法,属于非空接口,People 的内部定义是一个iface结构体。

type People interface {
    Show()  
}

stu 是一个指向 nil 的空指针,但是最后return stu 会触发匿名变量 People = stu 值拷贝动作,所以最后live()放回给上层的是一个People insterface{}类型,也就是一个iface struct{}类型。 stu 为 nil,只是iface中的 data 为 nil 而已。 但是iface struct{}本身并不为 nil.

func live() People {
    var stu *Student
    return stu      
}

结构体json

以下代码输出什么

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

func main() {
	t := struct {
		time.Time
		N int
	}{
		time.Date(2020, 12, 20, 0, 0, 0, 0, time.UTC),
		5,
	}

	m, _ := json.Marshal(t)
	fmt.Printf("%s", m)
}

答案

"2020-12-20T00:00:00Z"

解析

package main

import (
	"encoding/json"
  "fmt"
)

type Person struct {
  Name  string `json:"name"`
  Hobby string `json:"hobby"`
}

func main() {
  person := Person{name: "polarisxu", hobby: "Golang"}
  m, _ := json.Marshal(person)
  fmt.Printf("%s", m)
}

要想输出 {“name”:“polarisxu”,“hobby”:"Golang”},一般我们会这么做:将 Person 的字段导出,同时设置上 tag。

但如果不想导出 Person 的字段可以通过实现 Marshaler 来做到。

func (p Person) MarshalJSON() ([]byte, error) {
	return []byte(`{"name":"`+p.name+`","hobby":"`+p.hobby+`"}`), nil
}

time.Time 是一个没有导出任何字段的结构体类型,因此它肯定实现了 Marshaler 接口。

type Time struct {
    // contains filtered or unexported fields
}

// MarshalJSON implements the json.Marshaler interface.
// The time is a quoted string in RFC 3339 format, with sub-second precision added if present.
func (t Time) MarshalJSON() ([]byte, error) {
	if y := t.Year(); y < 0 || y >= 10000 {
		// RFC 3339 is clear that years are 4 digits exactly.
		// See golang.org/issue/4556#c15 for more discussion.
		return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
	}

	b := make([]byte, 0, len(RFC3339Nano)+2)
	b = append(b, '"')
	b = t.AppendFormat(b, RFC3339Nano)
	b = append(b, '"')
	return b, nil
}

正是因为内嵌,t 的方法集包括了 time.Time 的方法集,所以,t 自动实现了 Marshaler 接口。

其实这道题的情况,在日常工作中还真有可能遇到。所以,当你内嵌某个类型时,特别这个类型不是你自己定义的,需要留意这种情况。

一般解决这个问题的方法有两种:1)不内嵌;2)重新实现 MarshalJSON 方法。

然而这道题无法重新实现 MarshalJSON 方法,因为结构体类型是匿名的。只能通过不内嵌来得到正确的结果。

json.Marshal 函数的文档

Marshal traverses the value v recursively. If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method and encodes the result as a JSON string. The nil pointer exception is not strictly necessary but mimics a similar, necessary exception in the behavior of UnmarshalJSON.

如果值实现了 json.Marshaler 接口并且不是 nil 指针,则 Marshal 函数会调用其 MarshalJSON 方法以生成 JSON。如果不存在 MarshalJSON 方法,但该值实现 encoding.TextMarshaler 接口,则 Marshal 函数调用其 MarshalText 方法并将结果编码为 JSON 字符串。

可见,json.Marshal 函数优先调用 MarshalJSON,然后是 MarshalText,如果都没有,才会走正常的类型编码逻辑。

类型断言

下面这段代码能否编译通过?如果可以,输出什么?

func GetValue() int {
	return 1
}

func main() {
	i := GetValue()
	switch i.(type) {
	case int:
		println("int")
	case string:
		println("string")
	case interface{}:
		println("interface")
	default:
		println("unknown")
	}
}

答案

编译失败。

解析

类型断言,类型断言的语法形如:i.(type),其中 i 是接口,type 是固定关键字,需要注意的是,只有接口类型才可以使用类型断言。

channel

执行下面的代码会发生什么

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 1000)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()
	go func() {
		for {
			a, ok := <-ch
			if !ok {
				fmt.Println("close")
				return
			}
			fmt.Println("a: ", a)
		}
	}()
	close(ch)
	fmt.Println("ok")
	time.Sleep(time.Second * 100)
}

答案

panic: send on closed channel

解析

  1. 给一个 nil channel 发送数据,造成永远阻塞
  2. 从一个 nil channel 接收数据,造成永远阻塞
  3. 给一个已经关闭的 channel 发送数据,引起 panic
  4. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  5. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

本题中,因为 main 在开辟完两个 goroutine 之后,立刻关闭了 ch, 结果就是 panic

sync.WaitGroup

以下代码有什么问题

package main

import (
	"sync"
)

const N = 10

var wg = &sync.WaitGroup{}

func main() {
	for i := 0; i < N; i++ {
		go func(i int) {
			wg.Add(1)
			println(i)
			defer wg.Done()
		}(i)
	}
	wg.Wait()
}

答案

输出结果不唯一,代码存在风险, 所有 go 语句未必都能执行到。

解析

go语句执行太快了,导致 wg.Add(1) 还没有执行 main 函数就执行完毕了。wg.Add 的位置放错了。

空map取值

下面这段代码输出什么?

type person struct {  
    name string
}

func main() {  
    var m map[person]int
    p := person{"mike"}
    fmt.Println(m[p])
}

答案

  1. m 是一个 map,值是 nil。
  2. 从 nil map 中取值不会报错,而是返回相应的零值,这里值是 int 类型,因此返回 0。

注意

  1. nil map 可以取值,但不能直接赋值

可变参数函数

下面这段代码输出什么?

func hello(num ...int) {  
    num[0] = 18
}

func main() {  
    i := []int{5, 6, 7}
    hello(i...)
    fmt.Println(i[0])
}

答案

18

解析

可变参数函数会将参数变成切片

func hello(num ...int) {
	fmt.Println(num)
	fmt.Printf("%T \n",num)
}

func main() {
	hello(1,2,3,4,5)
	fmt.Println()
}

在这里插入图片描述
值拷贝的形式传入,底层数组是一样的

func hello(num ...int) {
	fmt.Println(num)
	fmt.Printf("%T \n",num)
	println(&num)
	num[0]=18
}

func main() {
	a:=[]int{1,2,3,4,5}
	println(&a)
	hello(a...)
	println(&a)
	fmt.Println(a)
}

在这里插入图片描述
可变参数是切片,切片是引用,所以func内赋值会带出来。

可变参数

func sum(vals ...int) int
----------------------------------
fmt.Println(sum())           // "0"
fmt.Println(sum(3))          // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
  1. 在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。
  2. 如果原始参数已经是切片类型,只需在最后一个参数后加上省略符。

下面的代码功能与上个例子中最后一条语句相同。

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

与传切片的区别

虽然在可变参数函数内部,…int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。

func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

结构体嵌套接口

以下代码能否通过编译?

package main

import (
	"fmt"
)

type worker interface {
	work()
}

type person struct {
	name string
	worker
}

func main() {
	var w worker = person{}
	fmt.Println(w)
}

答案

能通过编译

解析

Go 没有继承,但可以通过内嵌类型模拟部分继承的功能。

  1. 接口也是类型,自然也将它作为嵌入类型。
  2. 这个类型默认就实现了这个接口
  3. 实例化 person 时,没有给 worker 指定值,因此 person 中的 worker 是 nil,调用它的话会报错
type person struct {
	name string
	worker worker
}

实例

sort包中:

  1. 这是用于排序的
type Interface interface {
	// Len is the number of elements in the collection.
	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)
}
  1. 这是用于倒序的
type reverse struct {
	// This embedded Interface permits Reverse to use the methods of
	// another Interface implementation.
	Interface
}
  1. 使用
func Reverse(data Interface) Interface {
	return &reverse{data}
}

实例化 reverse 时,直接通过传递的 Interface 实例赋值给 reverse 的内嵌接口,然后 reverse 类型可以有选择的重新实现内嵌的 Interface 的方法。

回到题目

通过实例化的 w 调用 work 方法会报错,需要给 person 中的 worker 实例化,也就是需要一个实现了 worker 接口的类型实例。

type student struct{
    name string
}

func (s student) work() {
    fmt.Println("I am ", s.name, ", I am learning")
}

然后这样实例化 person:


var w worker = person{worker: student{"polarisxu"}}

切片的第三个参数

package main

import (  
    "fmt"
)

func main() {  
    a := [5]int{1, 2, 3, 4, 5}
    t := a[3:4:4]
    fmt.Println(t[0])
}

答案

4

第三个参数

  1. 截取操作符还可以有第三个参数,形如 [i,j,k],第三个参数 k 用来限制新切片的容量,但不能超过原数组(切片)的底层数组大小。
  2. 截取获得的切片的长度和容量分别是:j-i、k-i。

%+d

func main() {  
    i := -5
    j := +5
    fmt.Printf("%+d %+d", i, j)
}

答案

参考答案及解析:A。

解析

%d表示输出十进制数字,+表示输出数值的符号。这里不表示取反。

匿名结构体嵌套

下面这段代码输出什么?

type People struct{}

func (p *People) ShowA() {
	fmt.Println("showA")
	p.ShowB()
}
func (p *People) ShowB() {
	fmt.Println("showB")
}

type Teacher struct {
	People
}

func (t *Teacher) ShowB() {
	fmt.Println("teacher showB")
}

func main() {
	t := Teacher{}
	t.ShowB()
}

答案

teacher showB。

解析

  1. 在嵌套结构体中,People 称为内部类型,Teacher 称为外部类型;
  2. 通过嵌套,内部类型的属性、方法,可以为外部类型所有,就好像是外部类型自己的一样。
  3. 此外,外部类型还可以定义自己的属性和方法,甚至可以定义与内部相同的方法,这样内部类型的方法就会被“屏蔽”。
  4. 这个例子中的 ShowB() 就是同名方法。

defer的执行顺序

下面代码段输出什么?

type Person struct {
	age int
}

func main() {
	person := &Person{28}

	// 1. 
	defer fmt.Println(person.age)

	// 2.
	defer func(p *Person) {
		fmt.Println(p.age)
	}(person)  

	// 3.
	defer func() {
		fmt.Println(person.age)
	}()

	person.age = 29
}

答案解析:

参考答案及解析:29 29 28。变量 person 是一个指针变量 。

1.person.age 此时是将 28 当做 defer 函数的参数,会把 28 缓存在栈中,等到最后执行该 defer 语句的时候取出,即输出 28;

2.defer 缓存的是结构体 Person{28} 的地址,最终 Person{28} 的 age 被重新赋值为 29,所以 defer 语句最后执行的时候,依靠缓存的地址取出的 age 便是 29,即输出 29;

3.很简单,闭包引用,输出 29;

又由于 defer 的执行顺序为先进后出,即 3 2 1,所以输出 29 29 28。

interface{}接口类型

A、B、C、D 哪些选项有语法错误?

type S struct {
}

func f(x interface{}) {
}

func g(x *interface{}) {
}

func main() {
	s := S{}
	p := &s
	f(s) //A
	g(s) //B
	f(p) //C
	g(p) //D
}

答案

BD。

解析

  1. 函数参数为 interface{} 时可以接收任何类型的参数,包括用户自定义类型等,即使是接收指针类型也用 interface{},而不是使用 *interface{}。

  2. 永远不要使用一个指针指向一个接口类型,因为它已经是一个指针。

切片扩容

func main() {
	s1 := []int{1, 2, 3}
	s2 := s1[1:]
	s2[1] = 4
	fmt.Println(s1)
	s2 = append(s2, 5, 6, 7)
	fmt.Println(s1)
}

答案

[1 2 4]
[1 2 4]

解析

  1. golang 中切片底层的数据结构是数组
  2. 当使用 s1[1:] 获得切片 s2,和 s1 共享同一个底层数组,这会导致 s2[1] = 4 语句影响 s1。
  3. append 操作会导致底层数组扩容,生成新的数组,因此追加数据后的 s2 不会影响 s1。

图示

s1 := []int{1, 2, 3}
s2 := s1[1:]

在这里插入图片描述

s2[1] = 4

在这里插入图片描述

s2 = append(s2, 5, 6, 7)

在这里插入图片描述

当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil

下面这段代码输出什么?为什么?

type People interface {
	Show()
}

type Student struct{}

func (stu *Student) Show() {

}

func main() {

	var s *Student
	if s == nil {
		fmt.Println("s is nil")
	} else {
		fmt.Println("s is not nil")
	}
	var p People = s
	if p == nil {
		fmt.Println("p is nil")
	} else {
		fmt.Println("p is not nil")
	}
}

答案解析

参考答案及解析:s is nil 和 p is not nil。

  1. 分配给变量 p 的值明明是 nil,然而 p 却不是 nil。当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil。上面的代码,给变量 p 赋值之后,p 的动态值是 nil,但是动态类型却是 *Student,是一个 nil 指针,所以相等条件不成立。

String()方法

下面这段代码输出什么?

type Direction int

const (
	North Direction = iota
	East
	South
	West
)

func (d Direction) String() string {
	return [...]string{"North", "East", "South", "West"}[d]
}

func main() {
	fmt.Println(South)
}

答案解析

参考答案及解析:South。

  1. 根据 iota 的用法推断出 South 的值是 2;

  2. 如果类型定义了 String() 方法,当使用 fmt.Printf()、fmt.Print() 和 fmt.Println() 会自动使用 String() 方法,实现字符串的打印。

  3. println(South)依旧会打印2.

  4. 当改为指针接收者时不会调用String()

    func (d *Direction) String() string {
    	return [...]string{"North", "East", "South", "West"}[*d]
    }
    

    在这里插入图片描述

  5. 原因:当对象是指针时,不用管接收者是指针接收者还是非指针接收者;当对象不是指针时,无法调用指针接收者的方法。

map 的 value 是不可寻址的

下面代码输出什么?

type Math struct {
	x, y int
}

var m = map[string]Math{
	"foo": Math{2, 3},
}

func main() {
	m["foo"].x = 4
	fmt.Println(m["foo"].x)
}

答案解析

  1. 编译报错 cannot assign to struct field m[“foo”].x in map。
  2. 错误原因:对于类似 X = Y的赋值操作,必须知道 X 的地址,才能够将 Y 的值赋给 X,但 go 中的 map 的 value 本身是不可寻址的。
  3. 究其原因,因为Go的map是通过散列表来实现的,说得更具体一点,就是通过数组和链表组合实现的。并且Go的map也可以做到动态扩容,当进行扩容之后,map的value那块空间地址就会产生变化,所以无法对map的value进行寻址。

解决方案

  1. 使用临时变量

    type Math struct {
    	x, y int
    }
    
    var m = map[string]Math{
    	"foo": Math{2, 3},
    }
    
    func main() {
    	tmp := m["foo"]
    	tmp.x = 4
    	m["foo"] = tmp
    	fmt.Println(m["foo"].x)
    }
    
  2. 修改数据结构

    type Math struct {
    	x, y int
    }
    
    var m = map[string]*Math{
    	"foo": &Math{2, 3},
    }
    
    func main() {
    	m["foo"].x = 4
    	fmt.Println(m["foo"].x)
    	fmt.Printf("%#v", m["foo"])   // %#v 格式化输出详细信息
    }
    

一个select死锁问题

以下代码的输出结果:

func main() {
	var wg sync.WaitGroup
	foo := make(chan int)
	bar := make(chan int)
	wg.Add(1)
	go func() {
		defer wg.Done()
		select {
		case foo <- <-bar:
		default:
			println("default")
		}
	}()
	wg.Wait()
}

结果

在这里插入图片描述

解析

  1. 对于 select 语句,在进入该语句时,会按源码的顺序对每一个 case 子句进行求值:这个求值只针对发送或接收操作的额外表达式。将代码改为:

    func main() {
    	var wg sync.WaitGroup
    	foo := make(chan int)
    	bar := make(chan int)
    	wg.Add(1)
    	go func() {
    		defer wg.Done()
    		select {
    		case val:=<-bar:
    			foo<-val
    		// case foo <- <-bar:
    		default:
    			println("default")
    		}
    	}()
    	wg.Wait()
    }
    

    此时,正常输出:default

  2. 所以上述语句的理解就是:getVal()、<-input 和 getch() 等类似的操作会执行,导致死锁

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	ch := make(chan int)
    	go func() {
    		select {
    		case ch <- getVal(1):
    			fmt.Println("in first case")
    		case ch <- getVal(2):
    			fmt.Println("in second case")
    		default:
    			fmt.Println("default")
    		}
    	}()
    
    	fmt.Println("The val:", <-ch)
    }
    
    func getVal(i int) int {
    	fmt.Println("getVal, i=", i)
    	return i
    }
    

    无论 select 最终选择了哪个 case,getVal() 都会按照源码顺序执行:getVal(1) 和 getVal(2),必然输出:getVal, i= 1 | getVal, i= 2

相同问题

为什么每次都是输出一半数据,然后死锁?

package main

import (
	"fmt"
	"time"
)

func talk(msg string, sleep int) <-chan string {
	ch := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- fmt.Sprintf("%s %d", msg, i)
			time.Sleep(time.Duration(sleep) * time.Millisecond)
		}
	}()
	return ch
}

func fanIn(input1, input2 <-chan string) <-chan string {
	ch := make(chan string)
	go func() {
		for {
			select {
			case ch <- <-input1:
			case ch <- <-input2:
			}
		}
	}()
	return ch
}

func main() {
	ch := fanIn(talk("A", 10), talk("B", 1000))
	for i := 0; i < 10; i++ {
		fmt.Printf("%q\n", <-ch)
	}
}
  1. 每次进入以下 select 语句时:<-input1 和 <-input2 都会执行,相应的值是:A x 和 B x(其中 x 是 0-5)。但每次 select 只会选择其中一个 case 执行,所以 <-input1 和 <-input2 的结果,必然有一个被丢弃了,也就是不会被写入 ch 中。因此,一共只会输出 5 次,另外 5 次结果丢掉了。
  2. 而 main 中循环 10 次,只获得 5 次结果,所以输出 5 次后,报死锁。

更多类似的问题

// ch 是一个 chan int;
// getVal() 返回 int
// input 是 chan int
// getch() 返回 chan int
select {
  case ch <- getVal():
  case ch <- <-input:
  case getch() <- 1:
  case <- getch():
}

最典型的问题

有内存泄露(传递给 time.After 的时间参数越大,泄露会越厉害)

PS: 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

func main()  {
    ch := make(chan int, 10)

    go func() {
        var i = 1
        for {
            i++
            ch <- i
        }
    }()

    for {
        select {
        case x := <- ch:
            println(x)
        case <- time.After(30 * time.Second):
            println(time.Now().Unix())
        }
    }
}
  1. 每次执行select都会执行一次time.After(30*time.Second)获取一个chan。

循环中改变切片长度是否会死循环

下面这段代码能否正常结束?

func main() {
	v := []int{1, 2, 3}
	for i := range v {
		v = append(v, i)
	}
}

答案解析

参考答案及解析:不会出现死循环,能正常结束。

循环次数在循环开始前就已经确定,循环内改变切片的长度,不影响循环次数。

python会

a = [1, 2, 3]

for i in a:
    print(i)
    a.append(i)

print(a)

在这里插入图片描述

defer再探

下面这段代码输出什么?

func f(n int) (r int) {
	defer func() {
		r += n
		recover()
	}()

	var f func()

	defer f()
	f = func() {
		r += 2
	}
	return n + 1
}

func main() {
	fmt.Println(f(3))
}

答案解析

参考答案及解析:7。

func f(n int) (r int) {
	// 入栈
	defer func() {
		r += n
		recover()
	}()
	
	// f为nil
	var f func()
	
	// 入栈
	defer f()
	
	// f赋值为function
	f = func() {
		r += 2
	}
	// r = n+1
	// return r
	return n + 1
	
	1. r = 3+1 = 4
	2. 执行f()即,nil(),引发panic
	3. 被异常捕获,执行r+=3 =7
	4. return r
}

func main() {
	fmt.Println(f(3))
}

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
在这里插入图片描述

range数组和range指针

下面这段代码输出什么?

func main() {
	var a = [5]int{1, 2, 3, 4, 5}
	var r [5]int

	for i, v := range a {
		if i == 0 {
			a[1] = 12
			a[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r = ", r)
	fmt.Println("a = ", a)
}

答案解析

r =  [1 2 3 4 5]
a =  [1 12 13 4 5]

range 表达式是副本参与循环,就是说例子中参与循环的是 a 的副本,而不是真正的 a

如果想要 r 和 a 一样输出,修复办法:

func main() {
	var a = [5]int{1, 2, 3, 4, 5}
	var r [5]int

	for i, v := range &a {
		if i == 0 {
			a[1] = 12
			a[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r = ", r)
	fmt.Println("a = ", a)
}

使用 *[5]int 作为 range 表达式,其副本依旧是一个指向原数组 a 的指针,因此后续所有循环中均是 &a 指向的原数组亲自参与的,因此 v 能从 &a 指向的原数组中取出 a 修改后的值。

结论

GO中无论做什么都是把原值copy一份,原值是指针就copy指针,原值是值类型就copy值类型。

切片传参和切片扩容

下面这段代码输出什么?

func change(s ...int) {
	s = append(s,3)
}

func main() {
	slice := make([]int,5,5)
	slice[0] = 1
	slice[1] = 2
	change(slice...)
	fmt.Println(slice)
	change(slice[0:2]...)
	fmt.Println(slice)
}

答案解析

[1 2 0 0 0]
[1 2 3 0 0]

知识点:可变函数、append()操作。

Go 提供的语法糖…,可以将 slice 传进可变函数,不会创建新的切片。第一次调用 change() 时,append() 操作使切片底层数组发生了扩容,原 slice 的底层数组不会改变; 第二次调用change() 函数时,使用了操作符[i,j]获得一个新的切片,假定为 slice1,
它的底层数组和原切片底层数组是重合的,不过 slice1 的长度、容量分别是 2、5,所以在 change() 函数中对 slice1 底层数组的修改会影响到原切片。

range赋值地址

下面这段代码输出结果正确吗?

type Foo struct {
	bar string
}
func main() {
	s1 := []Foo{
		{"A"},
		{"B"},
		{"C"},
	}
	s2 := make([]*Foo, len(s1))
	for i, value := range s1 {
		s2[i] = &value
	}
	fmt.Println(s1[0], s1[1], s1[2])
	fmt.Println(s2[0], s2[1], s2[2])
}
输出:
{A} {B} {C}
&{A} &{B} &{C}

答案解析

参考答案及解析:s2 的输出结果错误。

s2 的输出是 &{C} &{C} &{C},在前面题目我们提到过,for range 使用短变量声明(:=)的形式迭代变量时,变量 i、value 在每次循环体中都会被重用,而不是重新声明。所以 s2 每次填充的都是临时变量 value 的地址,而在最后一次循环中,value 被赋值为{c}。因此,s2 输出的时候显示出了三个 &{c}。

可行的解决办法如下:

for i := range s1 {
	s2[i] = &s1[i]
}

同理如下

func main() {
	a := []int{1,2,3,4,5}
	b:=make([]*int,len(a))

	for i,val:=range a{
		b[i]=&val  // 赋值地址:b[i]是val的地址
	}
	fmt.Println(a)
	fmt.Println(b,*b[0])
}

在这里插入图片描述
不是地址赋值可以正常使用:

func main() {
	a := []int{1,2,3,4,5}
	b:=make([]int,len(a))

	for i,val:=range a{
		b[i]=val
	}
	fmt.Println(a)
	fmt.Println(b)
}

在这里插入图片描述

range map

下面代码里的 counter 的输出值?

func main() {
	var m = map[string]int{
		"A": 21,
		"B": 22,
		"C": 23,
	}
	counter := 0
	for k, v := range m {
		if counter == 0 {
			delete(m, "A")
		}
		counter++
		fmt.Println(k, v)
	}
	fmt.Println("counter is ", counter)
}

答案解析

for range map 是无序的,如果第一次循环到 A,则输出 3;否则输出 2,并且对m的修改会生效,但不影响循环次数.

多重赋值的坑

下面代码输出正确的是?

func main() {
	i := 1
	s := []string{"A", "B", "C"}
	i, s[i-1] = 2, "Z"
	fmt.Printf("s: %v \n", s)
}

答案解析

  1. 多重赋值分为两个步骤,有先后顺序:
    1. 计算等号左边的索引表达式和取址表达式,接着计算等号右边的表达式;
    2. 赋值;
  2. 所以,会先计算 s[i-1],等号右边是两个表达式是常量,所以赋值运算等同于 i, s[0] = 2, “Z”。

如何判断map是否相等

如何确认两个 map 是否相等?

答案

map 深度相等的条件:

  1. 都为 nil
  2. 非空、长度相等,指向同一个 map 实体对象
  3. 相应的 key 指向的 value “深度”相等

直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。

package main

import "fmt"

func main() {
	var m map[string]int
	var n map[string]int

	fmt.Println(m == nil)
	fmt.Println(n == nil)

	// 不能通过编译
	//fmt.Println(m == n)
}

或者直接利用反射:

package main

import( 
    "fmt"
    "relflect"
)
func main() {
    var m map[string]int
    var n map[string]int

    fmt.Println(reflect.DeepEqual(m,n))
}

DeepEqual部分源码

DeepEqual对于map是否相等也是从以上三点判断的。

case Map:
		if v1.IsNil() != v2.IsNil() {
			return false
		}
		if v1.Len() != v2.Len() {
			return false
		}
		if v1.Pointer() == v2.Pointer() {
			return true
		}
		for _, k := range v1.MapKeys() {
			val1 := v1.MapIndex(k)
			val2 := v2.MapIndex(k)
			if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(val1, val2, visited) {
				return false
			}
		}
		return true

Go 1.15 中 var i interface{} = a 会有额外堆内存分配吗?

var a  int = 3
// 以下有额外内存分配吗?
var i interface{} = a

答案解析

在 Go 中,接口被实现为一对指针:指向有关类型信息的指针和指向值的指针。可以简单的表示为:

type iface struct {
	// 类型
    tab  *itab
    // 值
    data unsafe.Pointer
}

其中 tab 是指向类型信息的指针;data 是指向值的指针。因此,一般来说接口意味着必须在堆中动态分配该值。

然而,Go 1.15 发行说明在 runtime 部分中提到了一个有趣的改进:

  • Converting a small integer value into an interface value no longer causes allocation.

意思是说,将小整数转换为接口值不再需要进行内存分配。小整数是指 0 到 255 之间的数。

基准测试

函数中进行了 100 次 int 到 interface 的转换.

package smallint

func Convert(val int) []interface{} {
    var slice = make([]interface{}, 100)
    for i := 0; i < 100; i++ {
        slice[i] = val
    }

    return slice
}
---------------------------
package smallint_test

import (
    "testing"
    "test/smallint"
)

func BenchmarkConvert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        result := smallint.Convert(12)
        _ = result
    }
}
  1. go1.14 和 go1.15
$ go version
go version go1.14.7 darwin/amd64
$ go test -bench . -benchmem ./...
goos: darwin
goarch: amd64
pkg: test/smallint
BenchmarkConvert-8      569830       1966 ns/op     2592 B/op      101 allocs/op
PASS
ok   test/smallint 1.647s
$ go version
go version go1.15 darwin/amd64
$ go test -bench . -benchmem ./...
goos: darwin
goarch: amd64
pkg: test/smallint
BenchmarkConvert-8     1859451        655 ns/op     1792 B/op        1 allocs/op
PASS
ok   test/smallint 2.178s
  1. go 1.15,但把12改为256
$ go test -bench . -benchmem ./...
goos: darwin
goarch: amd64
pkg: test/smallint
BenchmarkConvert-8      551546       2049 ns/op     2592 B/op      101 allocs/op
PASS
ok   test/smallint 1.502s

证明了上面提到的优化点。

Go 中定义了一个特殊的静态数组,该数组由 256 个整数组成(0 到 255)。当必须分配内存以将整数存储在堆上,并将其转换为接口的一部分时,它首先检查是否它可以只返回指向数组中适当元素的指针。这种经常使用的值的静态分配,是一种很常见的优化手段,例如,Python 对小整数执行类似的操作。

GO结构体字典值拷贝和值引用

以下代码能否编译?

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]Student

func main() {

	list = make(map[string]Student)

	student := Student{"Aceld"}

	list["student"] = student
	list["student"].Name = "LDB"

	fmt.Println(list["student"])
}

答案解析:

结果

编译失败,cannot assign to struct field list[“student”].Name in map

分析

map[string]Student 的 value 是一个 Student 结构值,所以当list[“student”] = student,是一个值拷贝过程。而list[“student”]则是一个值引用。那么值引用的特点是只读。所以对list[“student”].Name = "LDB"的修改是不允许的。

将 map 的类型的 value 由 Student 值,改成 Student 指针。

实际上每次修改的都是指针所指向的 Student 空间,指针本身是常指针,不能修改,只读属性,但是指向的 Student 是可以随便修改的,而且这里并不需要值拷贝。只是一个指针的赋值。

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]*Student

func main() {

	list = make(map[string]*Student)

	student := Student{"Aceld"}

	list["student"] = &student
	list["student"].Name = "LDB"

	fmt.Println(list["student"])
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/512823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云数据库技术沙龙|多云多源下的数据复制技术解读-NineData

摘要&#xff1a;随着数据智能时代的到来&#xff0c;多云多源架构下的数据管理是企业必备的基础设施&#xff0c;我们认为数据存取、数据集成与分发、数据安全与数据质量是基础&#xff0c;也是走向多云多源架构的起点。本议题介绍云原生的多云多源数据管理NineData&#xff0…

PlSql存储过程基础

目录儿 常用指令1. 什么是PLSQL语言2. PLSQL程序结构2.1 第一个程序 HelloWord:2.2 执行程序2.2.1 在工具中执行2.2.2 在sqlplus客户端中执行(命令行) 3. 变量3.1 普通变量3.2 引用型变量3.3 记录型变量 4. 流程控制4.1 条件分支4.2 循环 5. 游标5.1 定义5.2 语法5.3 游标的属性…

或许你需要这套uni-app打包android与ios流程

1、hbuilder每个账户的每日云打包有上限次数限制&#xff0c;超出次数要么换账户要么换成本地打包(uni-app提供了足够多云端的打包次数) 2、android打包&#xff0c;也就是apk包 优先搞明白两个需求&#xff1a; 、android包名是否为默认值&#xff0c;如果是默认值&#xf…

基于轻量化深度学习网络的工业环境小目标缺陷检测

源自&#xff1a;控制与决策 作者&#xff1a;叶卓勋 刘妹琴 张森林 摘 要 工业环境下表面缺陷检测是质量管理的重要一环, 具有重要的研究价值.通用检测网络(如YOLOv4)已被证实在多种数据集检测方面是有效的, 但是在工业环境的缺陷检测仍需要解决两个问题: 一是缺陷实例在…

【接口测试】

【接口测试】 1、分层测试理论&#xff1a; UI测试&#xff08;测试成本最高&#xff0c;发现BUG时间最晚&#xff09; 接口测试 单元测试 2、协议分析工具 1&#xff09;网络监听 TcpDump Wireshark 2&#xff09;协议客户端工具 curl、postman 3&#xff09;代理Pr…

phpstorm+phpstusy配置xdebug

真心觉得配置好麻烦呀 phpstorm版本&#xff1a; php版本用的7.3.4nts phpstudy版本&#xff1a;8.1.3 先下载xdebug,我用的是php_xdebug-3.1.6-7.3-vc15-nts-x86_64,phpstudy自带的那个xdebug我配置完不起作用 把下完的xdebug放到php下etc目录里&#xff0c;并改名为php_x…

C语言小项目之三子棋

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C语言学习分享⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多C语言知识   &#x1f51d;&#x1f51d; 三子棋 1. 前言&#x1f4d5;2. 思…

【英】考虑多能负荷不确定性的区域综合能源系统鲁棒规划(MatlabPython代码)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

寅家科技完成近亿元B1轮融资,加速高阶智能驾驶布局

近日&#xff0c;寅家科技宣布完成近亿元人民币B1轮融资&#xff0c;本轮融资由东方富海、深创投、深圳高新投联合投资&#xff0c;所募资金主要用于公司高阶智能驾驶技术产品的研发迭代&#xff0c;以及智能驾驶产品量产、海外市场开拓&#xff0c;从而进一步提升核心产品的市…

Solow模型推导模拟

Solow模型推导模拟 文章目录 Solow模型推导模拟[toc]1 Solow模型推导2 Solow模型模拟 1 Solow模型推导 在存在资本折旧、技术进步和人口增长条件下&#xff0c;有效劳动人均资本为 k ˙ ( t ) K ˙ ( t ) A ( t ) L ( t ) − K ( t ) [ A ( t ) L ( t ) ] 2 [ A ( t ) L ˙ …

腾讯云网站备案流程步骤、备案审核通过时间详细说明

腾讯云网站备案流程先填写基础信息、主体信息和网站信息&#xff0c;然后提交备案后等待腾讯云初审&#xff0c;初审通过后进行短信核验&#xff0c;最后等待各省管局审核&#xff0c;前面腾讯云初审时间1到2天左右&#xff0c;最长时间是等待管局审核时间&#xff0c;网站备案…

css新手引导实现方式总结

新手引导功能一般都是用一个半透明的黑色进行遮罩&#xff0c;蒙层上方对界面进行高亮&#xff0c;旁边配以弹窗进行讲解&#xff0c;样式如下 但是由于交互不同&#xff0c;实现方案也不一样&#xff0c;下面就针对不用的交互&#xff0c;总结了不同的实现方法&#xff08;下文…

路由和寻址的区别

如果说传输层协议&#xff0c;除了 TCP/UDP&#xff0c;我们还可以有其他选择&#xff0c;比如 Google 开发的 QUIC 协议&#xff0c;帮助在传输层支持 HTTP 3.0 传输。但是在网络层&#xff0c;IP 协议几乎一统天下。IP 协议目前主要有两个版本 IPv4 和 IPv6。 根据 Google 统…

开发者一定要知道的 API 管理五大趋势

API First 大势所趋&#xff0c;APIaaS&#xff08;API 作为服务&#xff09;也将受到越来越多企业和组织的选择&#xff0c;研发团队对 API 研发管理工具的要求也水涨船高。 API 在今天的数字化世界中扮演着至关重要的角色&#xff0c;随着云计算、物联网、微服务尤其是 AI 等…

Uniapp 引入uView-ui 插件

前言 最近在写uniapp&#xff0c;用了一下原生的ui框架感觉不是很好看&#xff0c;打算去插件市场上面找一下好看的UI插件。学习不是闭门造车&#xff0c;自己重复造轮子。能自己解决和会不会用别人的方法是两回事。我自己也能写UI界面逻辑&#xff0c;但是这样太费时间了。最…

线宽变大,损耗变小;线宽无限大,损耗无限小?

一博高速先生成员&#xff1a;黄刚 作为高速信号传输的重要的指标之一&#xff0c;损耗&#xff0c;无论是对硬件工程师&#xff0c;设计工程师还是我们SI工程师来说&#xff0c;都会是非常的关注。而对于像背板传输这种长距离的走线系统或是像芯片测试板要求损耗极小的情况&a…

港联证券|港交所内地路演人气爆棚 新一轮上市改革加速推进

5月10日&#xff0c;香港交易所在深圳举行未来科技峰会&#xff0c;拉开了港股特专科技上市新规内地路演推广的序幕。 今年3月底&#xff0c;港交所正式推出特专科技公司上市新规&#xff0c;《主板上市规则》新增18C章&#xff0c;并于3月31日起接收特专科技公司上市申请&…

JavaWeb《后端内容:2. MVC-ServletContext-IOC-事务管理-过滤器Filter》

目录 1. 准备和回顾 2. MVC-reflect 3. MVC-dispatcherServlet 3.1 思路部分 3.2 Debug部分 3.3 基于controller进行优化 4. Servlet-api 4.1 回顾 4.2 Init方法 1. 获取ServletConfig config getServletConfig(); 2. 获取初始化参数值&#xff1a;config.getInitP…

静态库(lib),动态库(dll)

我之前有一篇文章讲过静态链接库和动态链接库&#xff0c;大家可以点击链接去看一下&#xff1a;静态链接库&#xff0c;动态链接库【滴水逆向三期48笔记】&#xff0c;由于不是系统学习Windows编程&#xff0c;感觉那时候就是囫囵吞枣&#xff0c;今天又学习了一遍&#xff0c…

基于Keil生成外部Nor Flash下载算法,并使用J-Flash直接烧录(以W25Q64为例)

需要的软件&#xff1a; Keil STM32CubeMX J-Flash 参考文档&#xff1a; 方法1&#xff1a;在Keil中点击Help→uVision Help&#xff0c;然后再搜索框中输入FLM&#xff0c;点击列出主题&#xff0c;可以看到生成下载算法的大致步骤&#xff1a; 方法2&#xff1a;在ARM Ke…