【Golang - 90天从新手到大师】Day02 - 基本语法

系列文章合集
Golang - 90天从新手到大师

变量和常量

变量声明

标准声明

var name type

批量声明

var (
    name1 type1
    name2 type2
  ...
)

声明时初始化

var name type = value

类型推导

var name = value

短变量声明

name := value

匿名声明

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,多用于占位

func foo() (int, string) {
    return 10, "name"
}
func main() {
    x, _ := foo()
    _, y := foo()
    fmt.Println("x=", x)
    fmt.Println("y=", y)
}

常量声明

const identifier [type] = value    //显式类型定义
const identifier = value    //隐式类型定义

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

iota

特殊常量,可以认为是一个可以被编译器修改的常量,代表了const声明块的行索引,可以被用作枚举值:

package main

import "fmt"

func main() {
    const (
            a = iota   //0
            b = iota   //1
            c          //2
            d = "ha"   //独立值,iota += 1
            e          //"ha"   iota += 1
            f = 100    //iota +=1
            g          //100  iota +=1
            h = iota   //7,恢复计数
            i          //8
    )
  const a1 = iota  //0
    fmt.Println(a,b,c,d,e,f,g,h,i)
}
 

结果为:0 1 2 ha ha 100 100 7 8
如果中断iota自增,则必须显式恢复;自增默认是int类型,可以自行进行显示指定类型。

指针

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针,指针操作只需要记住两个符号:&(取地址)和*(根据地址取值)即可。

空指针的值为nil

new

用于分配内存空间。

func new(Type) *Type —— new函数返回一个指向该类型内存地址的指针

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

func make(t Type, size ...IntegerType) Type
func main() {
    var b map[string]int              //声明map类型
    b = make(map[string]int, 10)      //初始化,不初始化会引发panic
    b["测试"] = 100                    //赋值
    fmt.Println(b)
}

数据类型

布尔型 bool

1.布尔类型变量的默认值为false。
2.Go 语言中不允许将整型强制转换为布尔型.
3.布尔型无法参与数值运算,也无法与其他类型进行转换.

整数型 int8、int16、int32、int64 有符号整型

uint8、uint16、uint32、uint64 无符号整型
int 32位操作系统上就是int32,64位操作系统上就是int64
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
uintptr 无符号整型,用于存放一个指针
rune 代表一个 UTF-8字符,实际上是uint8
byte 代表一个ASCII码字符,实际上是int32

浮点型 float32、float64 浮点型

complex64、complex128 复数,实部和虚部各占一半

字符串型 string Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型一样。

复合类型

1、指针类型(Pointer)

2、数组类型

3、结构化类型(struct)

4、Channel 类型

5、函数类型

6、切片类型

7、接口类型(interface)

8、Map 类型

字符串类型

字符串修改

字符串是不允许修改的,想要修改字符串,需要先将其转换成 []rune 或 []byte,修改后再转换为 string。无论哪种转换,都会重新分配内存,并复制字节数组。

字符串拼接

在Go中,字符串可以很方便的拼接:str := str1 + str2 + str3

字符串拼接在编译时都会被存放到一个切片中,拼接过程需要遍历两次切片,第一次遍历获取总长度,据此申请内存,第二次遍历会把字符串逐个拷贝过去。

为什么字符串不允许修改?

C++语言中的string,其本身拥有内存空间,修改string是支持的。但Go的实现中,string不包含内存空间,只有一个内存的指针,且通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了string不可修改的约定。

[]byte 转换成 string 一定会拷贝内存吗?

在临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存。例如以下场景:

使用m[string(b)]来查找map(map是string为key,临时把切片b转成string);

字符串拼接,如"<" + “string(b)” + “>”;

字符串比较:string(b) == “foo”

Slice 类型

Slice又称动态数组,依托数组实现,可以方便的进行扩容、传递等,实际使用中比数组更灵活

Slice基本操作

切片追加

Go语言的内建函数 append() 可以为切片动态添加元素。可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素。还可以使用 append() 来变相删除元素

func main(){
    var s []int
    s = append(s, 1)        // [1]
    s = append(s, 2, 3, 4)  // [1 2 3 4]
    s2 := []int{5, 6, 7}  
    s = append(s, s2...)    // [1 2 3 4 5 6 7]

  // 从切片中删除元素
  a := []int{30, 31, 32, 33, 34, 35, 36, 37}
  // 要删除索引为2的元素
  a = append(a[:2], a[3:]...)
  fmt.Println(a) //[30 31 33 34 35 36 37]
}

字符串切片

切片扩容

切片追加时,由于每个切片会指向一个底层数组,若这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 append() 函数调用时,所以我们通常都需要用原变量接收 append 函数的返回值。

func main() {
    //append()添加元素和切片扩容
    var numSlice []int
    for i := 0; i < 10; i++ {
        numSlice = append(numSlice, i)
        fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    }
}
输出:
[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1]  len:2  cap:2  ptr:0xc0000a8040
[0 1 2]  len:3  cap:4  ptr:0xc0000b2020
[0 1 2 3]  len:4  cap:4  ptr:0xc0000b2020
[0 1 2 3 4]  len:5  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5]  len:6  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9]  len:10  cap:16  ptr:0xc0000b8000

可以看出:切片容量以 1、2、4、8、16 的规则再扩容,每次扩容后是扩容前的两倍。

slice扩容源码

从源码中可得:

如果新的长度(newlen)大于两倍旧容量(doublecap),最终容量(newcap)就是新的长度(newlen)

否则,如果旧容量小于256,最终容量就是两倍旧容量

否则,扩容计算方式变为 旧容量 +(旧容量 + 768)/4,也就是扩充(25%+192)的容量,不断扩容直到最终容量大于新长度

最后,若newcap溢出后,最终容量 就设为 新的长度

切片拷贝

浅拷贝

只拷贝了对象的全部内容,但不拷贝对象的引用的内容

func main() {
    s1 := make([]int, 3) //[0 0 0]
    s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
    s2[0] = 100
    fmt.Println(s1) //[100 0 0]
    fmt.Println(s2) //[100 0 0]
}
深拷贝

使用 Go 语言内建的 copy() 函数可以轻松做到对切片的深拷贝

func main() {
    // copy()复制切片
    a := []int{1, 2, 3, 4, 5}
    c := make([]int, 5, 5)
    copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1 2 3 4 5]
    c[0] = 1000
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1000 2 3 4 5]
}

Map 类型

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用

使用 make()函数分配内存,语法为:

make ( map[KeyType]ValueType,  cap)

判断某个键是否存在,语法为:

value,ok := map[key]

map的遍历

Go 中使用 for range 遍历map

使用 delete() 函数删除键值对,语法为:

delete(map, key)

数据结构

hmap数据结构

了解map的大致用法后,看一下 map 的底层结构能更好的理解 map,Go 语言中 map 使用哈希表作为底层实现,map 类型的变量本质上是一个指针,指向 hamp 结构体。其数据结构如下:

源码文件:runtime/map.go line:117、134

type hmap struct {

    count     int     //当前元素个数
    flags     uint8    //当前状态
    B         uint8     //记录桶的数目是2^B
    noverflow uint16     //记录使用的溢出桶数量
    hash0     uint32     //hash种子

    buckets    unsafe.Pointer     //数组指针,记录桶的位置
    oldbuckets unsafe.Pointer    //扩容时,记录旧桶的位置
    nevacuate  uintptr        //渐进式扩容阶段,记录下一个迁移的旧桶编号

    extra *mapextra    //指向mapextra结构体,里面记录溢出桶相关信息

}
type mapextra struct {

  overflow *[]*bmap  //记录已经使用的溢出桶的地址
  oldoverflow *[]*bmap  //旧桶使用的溢出桶的地址
  nextOverflow *bmap  //下一个空闲溢出桶的地址
}

一个哈希表可以有多个 bucket (哈希桶),而每个 bucket 能保存若干个键值对。

注意:当B大于4时,会额外创建 2^(B-4) 个桶用作溢出桶 (这些溢出桶和常规桶 在内存中是连续的)

bucket数据结构

源码文件:runtime/map.go  line:64、151

const bucketCnt  = abi.MapBucketCount     //=8 

type bmap struct {

    tophash [bucketCnt]uint8  //存储哈希值的高8位
   
    //以下属性在编译期间生成;源码位于 src/cmd/compile/internal/reflectdata/reflect.go:MapBucketType   line:91

    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr  //用于对齐内存
    overflow uintptr  //溢出桶的地址
}

显而易见,每个桶中定义了有8个哈希值的容量,即每个桶的容量为8,超过8个就要用到溢出桶。

除此之外,bucket 还有一些属性会在编译时动态生成,因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导。

map扩容

触发扩容的条件有二个:

负载因子 > 6.5时,也即平均每个 bucket 存储的键值对达到6.5个。

noverflow >= 2^15(B > 15)或 noverflow >= 2^B(B <= 15) 时,也即溢出桶数量 超过 常规桶数量 或 32768 时。

条件不同触发的扩容方法也不同:

增量扩容

当负载因子过大时,就新建一个 bucket,新的 bucket 长度是原来的2倍,然后旧 bucket 数据分流到两个新的 bucket 中。

等量扩容

当溢出桶过多,但负载因子又没有超过阈值时,就进行等量扩容。(即条件2)

所谓等量扩容,实际上并不是扩大容量,而是创建和旧桶一样多的新桶,然后搬迁键值对,把松散的键值对重新排列一次,以使 bucket 的使用率更高,进而保证更快的存取。

struct 类型

结构体的定义
使用 type 和 struct 关键字来定义结构体,语法如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

在 Go 1.9 版本新增了类型别名的新功能,语法如下:

type 类型别名 = 类型名
结构体实例化

package main

import (
    "fmt"
)

type A struct {
    a string
    b int8
}

func main() {
    //普通实例化
    var a1 A  //没初始化前,成员变量都是零值
    a1.a = "a"
    a1.b = 1

    //匿名结构体
    var user struct {
        Name string
        Age  int
    }
    user.Name = "小王子"
    user.Age = 18
    fmt.Printf("%#v\n", user)    //struct { Name string; Age int }{Name:"小王子", Age:18}

    //指针类型结构体
    var a2 = new(A)
    fmt.Printf("%T\n", a2) //*main.A

    //取结构体的地址实例化
    var a3 = &A{}          //相当于进行了 new
    a3.a = "a3"            //底层是(*a3).a = "a3",是 Go 语言的语法糖
    fmt.Printf("%T\n", a3) //*main.A
}

构造函数
  Go 语言的结构体并没有构造函数,但我们可以通过初始化来简便的实现构造函数。

func newA(a string, b int8) *A {
    return &A{
        a: a,
        b: b,
    }
}
//调用构造函数
func main() {
  a1 := NewA("a",1)
  fmt.Println(a1) 
}

方法
  在 Go 语言中,结构体就像是类的简化形式。但结构体内不允许定义函数,也没有类的概念,怎么定义方法呢?通过绑定类型来实现方法。

Go 语言的方法,是作用在接收者(receiver)上的一个函数,是一种特殊的函数。举个例子:

type Person struct{
     name string
     age int
}

func (this *Person) GetName() {
    fmt.Printf(this.Name)
}

func main(){
    person := Person{
       Name: "xiaofan",
      Age:  18,
   }
   person.GetName()  //执行Person绑定的方法GetName
   return
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。方法的调用看起来就像 C++ 中调用类的成员函数一样 —— 对象.方法。

结构体的“继承”
  Go 语言中可以通过(组合)结构体嵌套来实现“继承”。

package main

import (
    "fmt"
)

type Person struct { //结构体大写开头 —— 公有
    Name string //属性大写开头 —— 公有属性
    age  int    //属性小写开头 —— 私有属性
}

func (p *Person) GetName() { //方法大写开头 —— 共有
    fmt.Printf("%s\n", p.Name)
}

type Student struct {
    Person     //这样Student类就可以继承Person类了
    score  int //student类自己的属性
}

func (s *Student) getscore() { //方法小写开头 —— 私有
    fmt.Println(s.score)
}

func main() {
    stu := Student{}
    stu.Name = "小饭" //继承父类的属性
    stu.score = 100
    stu.GetName() //继承父类的方法
    stu.getscore()
}

Go 语言的公有是可挎包访问,在同一包内 私有属性同样可访问。

结构体标签(Tag)
  Go的struct声明允许字段附带 Tag 来对字段做一些标记。

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

key1:"value1" key2:"value2"

举个例子:

//Student 学生
type Student struct {
    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name   string //私有不能被json包访问
}

func main() {
    s1 := Student{
        ID:     1,
        Gender: "男",
        name:   "学生",
    }
    data, err := json.Marshal(s1)
    if err != nil {
        fmt.Println("json marshal failed!")
        return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

Tag 常见用法主要是JSON数据解析、ORM映射等。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

isevena、

谢谢您的肯定

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值