Go语言的指针类型

声明指针:

var ptr *int
var p_array [5]*int

指针数组

const max = 3

func main() {
    number := [max]int{5, 6, 7}
    var ptrs [max]*int //指针数组
    //将number数组的值的地址赋给ptrs
    for i, x := range number {
        ptrs[i] = &x
    }
    for i, x := range ptrs {
        fmt.Printf("指针数组:索引:%d 值:%d 值的内存地址:%d\n", i, *x, x)
    }
}
//输出如下
指针数组:索引:0 值:7 值的内存地址:824634204304
指针数组:索引:1 值:7 值的内存地址:824634204304
指针数组:索引:2 值:7 值的内存地址:824634204304

上述写法中,之所以值和地址都一样,是因为这种写法中,每次往指针数组里存的都是局部变量x的地址和值,到最后一次循环的时候x=7,所以最终结果如输出结果一样。与range一起使用来对指针数组赋值时尤其要注意这点。

正确写法应为:

const max = 3

func main() {
    number := [max]int{5, 6, 7}
    var ptrs [max]*int //指针数组
    //将number数组的值的地址赋给ptrs
    for i := 0; i < max; i++ {
        ptrs[i] = &number[i]
    }
    for i, x := range ptrs {
        fmt.Printf("指针数组:索引:%d 值:%d 值的内存地址:%d\n", i,*x, x)
    }
}

指向指针的指针

声明方式

var ptr **int

函数的指针形参

func myfun(ptr *int) [return type]{/* code */}

Go语言的结构体

声明结构体

type mystruct struct{
	member1 type
	member2 type
	...
	member3 type
}

初始化结构体

s1:=mystruct{val1,val2,val3...}

key value形式初始化
s2:=mystruct{member1:val1,member2:val2...}
忽略的成员字段为零值
s3:=mystruct{member1:val1}

访问结构体的成员

通过句号运算符来访问

s1:=mystruct{1,2,3}
s1.member2=6
fmt.Println(s1.member1)//输出1

结构体的指针

var sp *mystruct
sp1:=&s1
//指针访问结构体成员也是句号运算符:
//s1.member1和sp.member1等价 这点和C++有所区别

结构体成员的可见性(public和private)

首字母大写的是public成员,小写的是private成员

这里的可见性是包与包之间的可见性,包A中有个结构体S,包B中想要访问S的成员的话,必须S中的成员变量名是首字母大写的

当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON。

type Person struct {
   Name string      //Name字段首字母大写
   age int               //age字段首字母小写
}

func main() {
  person:=Person{"小明",18}
  if result,err:=json.Marshal(&person);err==nil{  //json.Marshal 将对象转换为json字符串
    fmt.Println(string(result))
  }
}

输出:

{"Name":"小明"}    //只有Name,没有age

那这样 JSON 字符串以后就只能是大写了么? 当然不是,可以使用 tag 标记要返回的字段名。

type Person  struct{
     Name  string   `json:"name"`   //标记json名字为name   
     Age    int     `json:"age"`
     Time int64    `json:"-"`        // 标记忽略该字段

}

func main(){
  person:=Person{"小明",18, time.Now().Unix()}
  if result,err:=json.Marshal(&person);err==nil{
   fmt.Println(string(result))
  }
}

控制台输出:

{"name":"小明","age":18}

结构体嵌套,实现继承关系

type s1 struct{
	radius float32
}
type s2 struct{
	s1 //匿名父类
	height float32
}

func main(){
	mystruct:=s2{s1{radius:1.23},5.666}
	fmt.Println(mystruct.radius,mystruct.height)
}
//output 1.23 5.666

在子结构体中使用匿名父类,实现继承关系,子结构体可访问父类中的所有成员,父类如同在子结构体中展开了一般。子结构体不仅继承了父类的所有成员,也继承了父类的所有方法(相当于子类也实现了这些方法一样),子结构体的对象都可以访问父类的方法和成员。

s2{s1{radius:1.23},5.666}这里的父类s1的初始化很像C++中子类构造函数指定如何初始化父类一样:

type s1 struct{
	radius float32
}
type s2 struct{
	s1
	height float32
}

func main(){
	my_struct:=s2{s1:s1{123},height:456} //这个是指定字段初始化
    my_struct2:=s2{s1{123},456} //这个是按照s2声明的成员顺序初始化
}

如果父类与子结构体有相同的字段,则访问父类中的同名字段时,需要加上父类的类名,而子结构体的则直接访问:

type s1 struct{
	same string
}
type s2 struct{
	s1
	same string
}
func main(){
	mystruct:=s2{}
	mystruct.s1.same="hello"
	mystruct.same="world"
	fmt.Println(mystruct.s1.same,mystruct.same) //output hello world
}


Go语言的范围(range)

关键字range在go中是用来迭代数组、切片、channel和map元素(可迭代对象)的,对于数组和切片而言,得到是索引和元素的值,而对于map而言,得到的是键值对

//example1
slice:=[]int{1,2,3,4,5}
sum:=0
for idx,_:=range slice{
	sum+=slice[idx]
}
fmt.Println(sum) //output 15

//example2
m := map[string]string{"a": "hello", "b": "world"}
for k, v := range m {
    fmt.Printf("key: %s value: %s ", k, v)
} //output key: a value: hello key: b value: world

range还可用于迭代字符串,返回每个字符的下标和该字符对应unicode的值(也即一个rune类型的值)

str := "你好"
for idx, val := range str {
fmt.Println(idx, val)
}
// output 
//0 20320
//3 22909
for idx:=range str{
    fmt.Println(idx)
}//output 0 3 返回的是每个字符的索引,由于你好两个字是三个字节为单位,所以返回的是0,3
//对于数组和切片而言,这种写法都是返回的下标索引

通过range实现范围for也可以不需要使用range的返回值:

slice:=[]int{1,2,3,4,5}
count:=0
for range slice{
	count++
}
fmt.Println(count) //输出5


Go语言的map类型

map是无序的键值对存储容器,底层使用hash_table实现,与C++的map有区别,与python的字典相似,遍历map时并不知道键值对谁先谁后。

声明方式:

var my_map map[key_type] value_type//这种方式不初始化的话声明了一个nil的map,无法进行键值对的存储
my_map:=make(map[key_type]value_type)//这种方式不初始化虽然是空map,但是可以进行键值对的插入

通过make创建map时,已经完成了初始化,是一个空map;此外,下面这种方式也是初始化了:

another_map:=map[int]string{1:"hhh",2:"www"}
my_map:=map[int]string{}//空map,但初始化了,可以存储kv
my_map[1]="helloworld"//此时和C++的字典使用方法一样,已存在的key则会赋予新的值,不存在的key则生成一个新的kv对,存入map中

但单纯只是声明的话,得到的是一个nil,无法进行存储:

var map1 map[int]string // nil
map1[1]="helloworld" //runtime error

可以通过下面的方法取判断map中是否存在某个key,如果不存在,则通过下标访问符得到的是个空值:

my_map := make(map[int]string)
my_map[1] = "helloworld" //插入kv对
fmt.Println(my_map)
v1 := my_map[2] //因为key 2不存在,v1是一个value类型的空值=""
if v1 == "" {
fmt.Println("empty key 2")
}
fmt.Println(my_map)
_, exists := my_map[3] //_是键值对的value,这里不使用,exists是一个bool类型,map中有该key则为true,否则为false
if exists {
fmt.Println("key 3 exists")
} else {
fmt.Println("key 3 doesn't exists")
}
fmt.Println(my_map)
//output:
//map[1:helloworld]
//empty key 2
//map[1:helloworld]
//key 3 doesn't exists
//map[1:helloworld]

在C++中,map通过中括号的访问会新增kv对或者修改key对应的value值,而在上述的代码中,两次通过[]获取map中的元素,都没有增加map中新的kv对,这与C++是不同的。C++的结果如下:

 #include<map>
 #include<iostream>
 #include<string>
 int main(){
         std::map<int,std::string> my_map;
         std::cout<<my_map[1];
         std::cout<<my_map.size()<<std::endl;
         if(my_map[1]=="")
                 std::cout<<"key 1 == empty"<<std::endl;
}//output
//1
//key 1 == empty

对map的range for循环

my_map:=map[string]string{"hello":"world","nihao":"dajiahao"}
for k,v:=range my_map{ //kv分别为键和值
	/* code */
}
for k:=range my_map{  //k 是key
	/* code */
}

delete函数

delete函数用于删除map中的元素,参数为map和想要删除的key

my_map1:=map[string]string{"hello":"world","nihao":"dajiahao"}
delete(my_map1,"hello")
fmt.Println(my_map1)
//output
//map[nihao:dajiahoa]


Go语言类型转换

类型强制转换(显式)和C++的写法很像

type(expression)
//example
var i1 int32=5
var i2 int32=10
f1:=float32(i1)/float32(i2)  //0.5

go不支持类型隐式转换

var f1 float32=0.1
var f2 float64=0.2
res:=f1/f2 //wrong msg: invalid operation: mismatched types float64 and float32
//除非 res:=f1/float32(f2)才能过编译


Go语言接口类型

接口和直观上的理解一样,很多内置的函数,或者自己编写的函数,可能存在对某个类调用同名的方法(形参、返回类型都一样),就可以实现多态。此时,为一个类型实现接口的方法,在后续别的地方使用时,就可以实现回调。

接口类型是声明了一系列共性方法的一种类型,声明方式为:

type my_interface interface{
	func1(params) [return type]
	func2(params) [return type]
}

任何其他类型实现了上述的这些方法(func_i),则是实现了这个接口:

func(ms mystruct) func1(){   //自定义结构体类型mystruct
	/* code */    
}

func(ms newStruct) func1(){ //自定义结构体类型newStruct
	/* code */
}
//其实和之前函数教程中写某个类的方法的方式是一样的,本质上都是一样的

上述的代码中,两个自定义的类都实现了接口中的func1函数,通过接口对象来调用这个函数的过程如下

var inter my_interface
inter=new(mystruct)
inter.func1()  //调用mystruct实现的函数
inter=new(newStruct)
inter.func1()  //调用newStruct实现的函数

这种方式有点像基类指针指向派生类,而接口中的函数相当于虚函数一样,然后通过接口对象(基类)访问自定义的类(派生类)实现的方法(虚函数)。但使用这种方法的前提是,自定义的类(派生类)必须把接口中所有声明的函数都实现了,不然无法过编译(以这里的例子而言,还需实现func2函数)。

接口类型的变量可以保存任何实现了该接口的类型的值,自定义的类型实现接口中的方法可以是值也可以是指针作为接收者,而没有硬性要求必须全为值或全为指针接收者,但只要有一个方法是指针接收者实现的,则在赋给接口变量的时候,就必须取址:

type MyInterface interface {
	read()
	write()
}

func (m MyStruct) read() {  //这里是值接收者
	fmt.Println(m.radius)
}
func (m *MyStruct) write() { //这里是指针接收者
	fmt.Println(m.radius)
}
func main() {
	ms := &MyStruct{1.2, 2.4}  //ms是指针
	var inter MyInterface
	inter = ms //因为有指针接收者实现的方法,则赋给接口对象的必须是个指针
	inter.read()
	inter.write()
}

接口本身也是一个值,它可以看作一个包含具体值和类型的元组(value,type),这里的value指的是赋给接口的对象的值,而type即为该对象的类型,可用于做类型断言(下面的笔记有写)。接口对象调用接口中的方法实际上调用的就是底层对象value的方法,如上main函数中的inter。

如果将变量A通过值赋给接口对象,那么接口对象底层的具体值是个A的副本;而如果是通过指针赋给接口对象,则接口底层对象是A的指针,此时接口对底层具体值的修改会影响变量A本身。

假设类型A使用指针接收者实现了接口,如果赋给接口对象inter的是一个A的nil指针,这时候接口对象并不为nil,只是底层的value是个nil指针,类型type是*A。当然这是如果inter调用了方法,可能出现panic,可以通过如下方式进行保护:

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}
func main(){
	var tp *T //这里的tp是类型T的nil指针,
	var inter I
	inter=tp //接受了类型T的nil指针
	inter.M() //函数M进入if语句块,保证程序不panic
}

nil的接口:var inter MyInterface这时inter为一个nil接口,既没有保存底层具体值value,也没有类型type信息,不能对其进行方法调用,例如此时的inter.func1()是错误的。

由上所述,这种实现接口方法的方式和为自定义类添加方法的方式是一样的,所以通过类对象自己访问方法也是可以的,这时候就不需要所有的接口方法都实现:

ms:=mystruct{}
ms.func1() //这样,即使没有实现func2,照样可以运行

若实现接口方法不是通过类型指针实现的,则通过接口访问类型方法的方式为:

func(ms mystruct) func1(){
	/* code */
}
var inter my_interface=mstruct{}

若用到了类型指针,则访问方式为:

func(ms *mystruct) func1(){
	/* code */
}
var inter my_interface=&mstruct{}

两者的区别为,用到指针了,则将类型对象赋给接口对象时需要取址&。

接口对象可以接受值或者指针赋值,用指针赋值永远不会错,推荐写代码时都取址,避免多余记忆点。

接口还可以组合:

type outer_inter1 interface {
	read()
}

type outer_inter2 interface {
	write()
}

type my_interface interface {
	outer_inter1
	outer_inter2
}

func (m mystruct) read() {
	fmt.Println("mystruct running read function from interface")
}

func (m mystruct) write() {
	fmt.Println("mystruct running write function from interface")
}

func main(){
	var inter my_interface
	inter = &mystruct{}
	inter.read()
	inter.write()
}
//output
//mystruct running read function from interface
//mystruct running write function from interface

组合的接口看起来难理解,可以直接把它看作多个接口的展开,一个组合的接口就等于是声明了其中组分接口的每个方法(因此,上面的代码中同时实现了read和write方法)。

在结构体中,可以通过使用匿名结构体进行继承,继承父类的所有方法和成员,因此,如果父类实现了接口,则相应的子结构体也实现了接口

type s1 struct{
}
type s2 struct{
	s1
}
type myinter interface{
	helloworld()
}
func(s s1) helloworld(){
	fmt.Println("helloworld")
}
func main(){
	var inter myinter
	myinter=new(s2)
	myinter.helloworld() //output helloworld
}

interface{}叫做空接口,所有类型都实现了空接口,所以空接口实例化的对象可以承载任何对象类型。因此,使用interface{}作为函数的形参,可以接收所有的类型对象,然后再对接收的对象进行类型断言,实现相应的功能。这个特性可用于实现go语言的多态。

声明一个空接口:var inter interface{}

func universe(inter interface{}) string {
	if _, ok := inter.(string); ok {
		return "oh yea"
	} else {
		return "not good"
	}

}
func main(){
	b := "hhh"
	c := 132
	fmt.Println(universe(b))
	fmt.Println(universe(c))
}
//output 
//oh yea 
//not good


Go语言的断言(type assertion)

接口除了像上一节中一样,还有一种方式也是接口,但更像是一种容器一样:

var inter interface{}
inter=123 //可以是任何一种类型
fmt.Println(inter) // output 123

通过接口,可以进行断言,进行接口对象所承载的类型的判断,

语法:val,ok:=inter.(type),val是inter承载类型对象的值,ok为bool值,表示是否是type类型。(安全断言)

不安全断言:val:=inter.(type),这种情况下,如果断言失败,程序会panic

var inter interface{}
inter=123
if val,ok:=inter.(int);ok{  //在本例中,val=123 ok=true
	fmt.Println(val) 
}  
//output 123

interface{}叫做空接口,所有类型都实现了空接口,所以空接口实例化的对象可以承载任何对象类型,如下所示。这种接口变量可以形成接口数组/切片:

var inter_slice []interface{}
inter_slice=append(inter_slice,123,"nihao",[]int{1,2,3},map[int]string{1:"hello",2:"world"})
fmt.Println(inter_slice)
for _, element := range inter_slice {
    if _, ok := element.(int); ok {
        fmt.Println("find int element")
    }
}//output
//[123 nihao [1 2 3] map[1:hello 2:world]]
//find int element

像上一节中接口类型的用法,也可以用来作类型判断,有种判断基类指针指向的是什么派生类类型的味道:

type my_inter interface{
	read()
}

func(m mystruct) read(){
	fmt.Println("helloworld")
}

func main(){
	var inter my_inter=mystruct{}
	if _,ok:=inter.(mystruct){
		fmt.Println("it's mystruct")
	}
}//output it's mystruct

搭配type switch,可用于判断类型,inter.(type)只能搭switch形成type switch语句:

var inter my_interface
inter = &mystruct{}
switch inter.(type) {
case *mystruct:
fmt.Println("it's mystruct")
}//output it's mystruct


Go语言rune类型

rune类型其实就是int32,源码中是int32的别名,常用来处理unicode,因为unicode一般有三个字节组成一个字符,所以一个rune就可以对应一个unicode,而这个rune其实就是该字符对应的编码值:

str := "nihao你好"
fmt.Println(len(str))
fmt.Println(len([]rune(str)))
for _, val := range []rune(str) {
fmt.Println(val)
}
//output
11  //字符串对应的字节数 11
7   //rune切片的长度 7=nihao(5)+你好(2)
110
105
104
97
111
20320 //"你"的编码值
22909 //"好"的编码值

从例子中可以看到,其实rune就是每个字符的对应的编码值,因为其本质就是一个int32,然后可以再根据这个编码值进行编码,得到相应的字符:

output := make([]byte, 20)
utf8.EncodeRune(output, 20320)
fmt.Println(string(output))
//output 你


Go语言map类型

var m map[int]string
m2:=map[int]string{1:"hello"}

只要实现了=号运算符的类型,都可以作为map的键类型,例如integers, floating point and complex numbers, strings, pointers, interfaces (as long as the dynamic type supports equality), structs and arrays。

而切片不能作为键类型,因为切片是没有实现=号运算符的。

和切片一样,map拥有底层数据结构的引用,如果将map传参给函数,其实是按照引用进行了传递,在函数内的修改会导致传递的map发生变化:

func changeMap(m map[int]string) {
	for k := range m {
		m[k] = "changed"
	}
}
func main(){
	m1 := map[int]string{2: "hello"}
	fmt.Println(m1)
	changeMap(m1)
	fmt.Println(m1)
}
//output
map[2:hello]
map[2:changed]

由于map是引用传递,所以所有涉及到赋值、参数传递的行为都和切片的类似,例如将map1赋值给map2,然后对map2进行修改,map1的值也相应发生了变化:

m1 := map[int]string{2: "hello"}
fmt.Println(m1)
m2 := m1
fmt.Println(m2)
m2[2] = "world"
fmt.Print(m1, m2)
//output
map[2:hello]
map[2:hello]
map[2:world] map[2:world]