Go编程之路(~未完结)

74 阅读18分钟

1.1什么是Go语言

1.高性能、高并发

2.语法简单、学习曲线平缓

3.丰富的标准库

4.完善的工具链

5.静态链接

6.快速编译

7.跨平台

8.垃圾回收

//实现一个简单的http服务来体现Go语言的简洁
package main
​
import (
    "net/http"
)
​
func main() {
    //将当前目录中的文件作为静态文件提供
    http.Handle("/", http.FileServer(http.Dir(".")))
    http.ListenAndServe(":8080", nil) //监听来自8080端口的请求并启动服务
​
}
​

image.png

1.2哪些公司在使用Go语言

image.png 1.3字节跳动为什么全面拥抱Go语言

1.最初使用的Python,由于性能问题换成了Go

2.C++不太适合在线Web 业务

3.早期团队非Java背景

4.性能比较好

5.部署简单、学习成本低

6.内部RPC和HTTP标加的推广

2.1开发环境

//Go环境下载
https://go.dev

image.png

2.2基础语法-变量

package main
​
import "fmt"func main() {
    fmt.Println("Hello,World!")
}
​

Go 语言的变量声明形式与其他主流静态语言有一个显著的差异,那就是它 将变量名放在了类型的前面

如果你没有显式为变量赋予初值,Go 编译器会为变量赋予这个类型的零值

var a int //a的值为0

image.png

变量声明块(block)的语法

var (
    a int = 128
    b int8 = 6
    s string = "hello"
    c rune = 'a'
    t bool = true
)

一行变量声明中同时声明多个变量

var a, b, c int = 5, 6, 7

省略类型信息的声明

适用于在变量声明的同时显式赋予变量初值的情况

var b = 13

短变量声明,只能在函数体,方法体内使用

Go 编译器会根据右侧变量初值自动推导出变量的类型

b := 13
​
​
package main
​
import (
    "fmt"
    "math"
)
​
func main() {
    //省略变量类型
    var a = "initial"var b, c int = 1, 2var d = truevar e float64//短变量声明,只能在函数体,方法体内使用
    f := float32(e)
​
    g := a + "foo"
    fmt.Println(a, b, c, d, e, f)
    fmt.Println(g)
​
    //不能改变量叫常量,常量在定义的时候必须赋值
    const s string = "constant"//省略常量类型,编译器也会自动推断类型
    const h = 50000000
    const i = 3e20 / h
    fmt.Println(s, h, math.Sin(h), math.Sin(i))
​
}
​

2.3 if else

package main
​
import "fmt"
​
func main() {
    //if 语句... 后面必须要接 {}
    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }
​
    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }
​
    //支持声明 if 语句的自用变量
    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}
​

if 语句支持在布尔表达式前声明自用变量,这些变量作用域仅限于 if 语句的代码块内部。使用 if 自用变量可以一定程度简化代码,并增强与同函数内其他变量的隔离,但这也十分容易导致变量遮蔽问题

2.4循环

Go语言的循环只有一种for

package main
​
import "fmt"func main() {
    i := 1
    for {
        fmt.Println("loop")
        break
    }
    for j := 7; j < 9; j++ {
        fmt.Println(j)
    }
​
    for n := 0; n < 5; n++ {
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }
}
​

2.5switch

package main
​
import "fmt"func main() {
​
​
 
    finger := 7
    switch finger {
    case 1:
        fmt.Println("道生一")
    case 2:
        fmt.Println("一生二")
    case 3:
        fmt.Println("二生三")
    case 4:
        fmt.Println("三生万物")
    default:
        fmt.Println("宕机")
    }
​
​
    //case一次判断多个值
    num := 9
    switch num {
    case 1, 3, 5, 7, 9:
        fmt.Println("奇数")
    case 2, 4, 6, 8, 10:
        fmt.Println("偶数")
    }
​
    //case中使用表达式
    age := 19
    switch {
    case age < 0:
        fmt.Println("啊良~")
    case age > 18:
        fmt.Println("真武山真英雄")
    default:
        fmt.Println("风雪庙神仙台")
    }
}

Go 语言的 switch 语句继承自 C 语言,但“青出于蓝而胜于蓝”,Go 不但修正了 C 语言中 switch 语句默认执行下一个 case 的“坑”,还对 switch 语句进行了改进与创新,包括支持更多类型、支持表达式列表等,让 switch 的表达力得到进一步提升。除了使用常规表达式作为 switch 表达式和 case 表达式之外,Go switch 语句又创新性地支持 type switch,也就是用类型信息作为分支条件判断的操作数。在 Go 中,这种使用方式也是 switch 所独有的。这里,我们要注意的是只有接口类型变量才能使用 type switch,并且所有 case 语句中的类型必须实现 switch 关键字后面变量的接口类型。最后还需要你记住的是 switch 会阻拦 break 语句跳出 for 循环

2.6数组

Go 语言的数组是一个长度固定的、由同构类型元素组成的连续序列

数组元素的类型可以为任意的 Go 原生类型或自定义类型, 数组的长度必须在声明数组变量时提供, 只能用整型数字面值或常量表达式作为 N 值

var arr [N]T

Go 提供了预定义函数 len 可以用于获取一个数组类型变量的长度,通过 unsafe 包提供的 Sizeof 函数

    var arr = [6]int{1, 2, 3, 4, 5, 6}
    fmt.Println("数组长度", len(arr))
    fmt.Println("数组大小", unsafe.Sizeof(arr))

和基本数据类型一样,我们声明一个数组类型变量的同时,也可以显式地对它进行初始

化。如果不进行显式初始化,那么数组中的元素值就是它类型的零值

    var arr1 [6]int
    fmt.Printf("%v\n", arr1)//[0,0,0,0,0,0]

如果要显式地对数组初始化,我们需要在右值中显式放置数组类型,并通过大括号的方式

给各个元素赋值

也可以忽略掉右值初始化表达式中数组类型的长度,用“…”替代,Go 编译器会根据数组元素的个数,自动计算出数组长度

var arr2 = [6]int{
        11, 22, 33, 44, 5, 5,
    }
    fmt.Printf("%v\n", arr2)
​
​
    var arr3 = [...]int{21, 22, 23}
    fmt.Printf("%T\n", arr3)

长度较大的稀疏数组进行显式初始化

var arr4 = [...]int{
        99: 39, // 将第100个元素(下标值为99)的值赋值为39,其余元素值均为0
    }
    fmt.Printf("%T\n", arr4) //[100]int

数组类型自身也可以作为数组元素的类型,这样就会产生多维数组

var mArr [2][3][4]int
    fmt.Printf("%v\n", mArr)

img

数组类型变量是一个整体,这就意味着一个数组变量表示的是整个数组。这点与 C 语言完全不同,在 C 语言中,数组变量可视为指向数组第一个元素的指针。这样一来,无论是参与迭代,还是作为实际参数传给一个函数 / 方法,Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销

2.7切片

数组在使用上确有两点不足:固定的元素个数,以及传值机制下导致的开销较大

与数组声明相比,切片声明仅仅是少了一个“长度”属性

    var nums = []int{1, 2, 3, 4, 5, 6}
    fmt.Printf("%v\n", nums)
    fmt.Printf("len: %d cap: %d\n", len(nums), cap(nums))

内置函数 append,我们可以动态地向切片中添加元素

    nums = append(nums, 1, 2, 3)
    fmt.Printf("%v\n", nums)
    fmt.Printf("len: %d cap: %d\n", len(nums), cap(nums))

Go 切片在运行时其实是一个三元组结构,它在 Go 运行时中的表示如下

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array: 是指向底层数组的指针;

len: 是切片的长度,即切片中当前元素的个数;

cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值。

img

Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长

度与切片初始元素个数相同

通过 make 函数来创建切片,并指定底层数组的长度

s1 := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度

如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len

sl := make([]byte, 6) // cap = len = 6

array[low : high : max] 语法基于一个已存在的数组创建切片。这种方式被称为数组的切片化

    arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    s2 := arr[3:7:9]
    fmt.Printf("%v\n", s2) //4 5 6 7 

切片的长度(len)是 high - low,它的容量是 max - low。而且,由于切片 sl 的底层数组就是数组 arr,对切片 sl 中元素的修改将直接影响数组 arr 变量

    s2[0] += 100
    fmt.Printf("%v\n%v\n", arr, s2)

切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销

我们在进行数组切片化的时候,通常省略 max,而 max 的默认值为数组的长度

针对一个已存在的数组,我们还可以建立多个操作数组的切片,这些切片共享同一底层数组,切片对底层数组的操作也同样会反映到其他切片中

切片与数组最大的不同,就在于其长度的不定长,这种不定长需要 Go 运行时提供支持,这种支持就是切片的“动态扩容”

基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”

2.8map

map是go提供的一种抽象数据类型, 表示一组无序的键值对,map中的每个key都是唯一

map[key _type]value_type

key与value的类型可以相同,也可以不同

	var map1 map[string]int
	var map2  map[string]string

map 类型要保证 key 的唯一性。Go 语言中要求,key 的类型必须支持“==”和“!=”两种比较操作符

在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较

	s1 := []int{}
	s2 := []int{}
	f1 := func() {}
	f2 := func() {}
	m1 := map[int]string{}
	m2 := map[int]string{}
	fmt.Println(s1 == s2)
	fmt.Println(f1 == f2)
	fmt.Println(m1 == m2)

函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的

声明map类型变量

如果我们没有显式地赋予 map 变量初值,map 类型变量的默认值为 nil
var m map[int]string

初值为零值 nil 的切片类型变量,可以借助内置的 append 的函数进行操作,这种在 Go 语言中被称为“零值可用

map类型没有初始化不能直接进行操作

    var m map[string]int
    m["key"] = 1 //发生运行时异常panic: assignment to entry in nil map

复合字面值初始化map类型变量

m := map[int]string{}
m1 := map[int][]string{
        1: []string{"vali_1", "vali_2"},
        2: []string{"vali_1"},
        3: []string{"vali_3"},
    }
    type Position struct {
        x float32
        y float64
    }
    m2 := map[Position]string{
        Position{29.11, 23.77}: "school",
    }
    //语法糖
    m3 := map[Position]string{
        {1, 2}: "number",
    }

make对map类型变量进行显示初始化

m1 := make(map[int]string) // 未指定初始容量
m2 := make(map[int]string, 8) // 指定初始容量为8

map 类型的容量不会受限于它的初始容量值,当其中的键值对数量超过初始容量

后,Go 运行时会自动增加 map 类型的容量,保证后续键值对的正常插入

map操作

面对一个非 nil 的 map 类型变量只需要把 value 赋值给 map 中对应的 key 就可以了

m := make(map[int]string)
m[1] = "value1"
m[2] = "value2"
m[3] = "value3"

插入新键值对的时候,某个 key 已经存在于 map 中了,那插入操作就会用新值覆盖旧值

m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}
m["key1"] = 11 // 11会覆盖掉"key1"对应的旧值1
m["key3"] = 3  // 此时m为map[key1:11 key2:2 key3:3]

获取键值对数量

m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}
fmt.Println(len(m)) // 2
m["key3"] = 3  
fmt.Println(len(m)) // 3

对key的查询

m := map[string]int{}
v, ok := m["key1"]
if !ok {
    //key1不在map中
}
//"key1"在map中,v将被赋予"key1"键对应的value

使用“comma ok”惯用法对 map 进行键查找和键值读取操作

使用delete()内建函数从map中删除一组键值对

delete(map, key)
● map:表示要删除键值对的map
● key:表示要删除的键值对的键

2.9range

不关心元素的值时,我们可以省略代表元素值的变量 v,只声明代表下标值 的变量 i

for i := range sl {
  // ... 
}

我们不关心元素下标,只关心元素值,那么我们可以用空标识符替代代表下 标值的变量 i。这个空标识符不能省略

for _, v := range sl {
//...
}

既不关心下标值,也不关心元素值

for _, _ = range sl {
  // ... 
}
for range sl {
  // ... 
}

我们要对 map 进行循环操作,for range 是唯一的方法

var m = map[string]int {
  "Rob" : 67,
    "Russ" : 39,
    "John" : 29,
}
for k, v := range m {
    println(k, v)
}

2.10函数

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

定义
package main
​
import "fmt"func sayHello() {
    fmt.Println("Hello")
}
​
//定义函数并且指定函数返回值
func intSum(a int, b int) (ret int) {
    ret = a + b
    return
}
​
//func intSum(a int, b int) int{
//  ret := a + b
//  return ret
//}//函数接收可变参数,参数名...
//可变参数在函数体内是切片类型
func intSum1(a ...int) {
    fmt.Println(a)
    fmt.Printf("%T\n", a)
}
​
//定义多个返回值的函数
func intSum2(a, b int) (sum, sub int) {
    sum = a + b
    sub = a - b
    return
}
​
func main() {
    sayHello()
    ret := intSum(19, 90)
    fmt.Println(ret)
​
    intSum1(1, 2, 3)
​
    sum, sub := intSum2(10, 90)
 
    fmt.Printf("sum:%d sub:%d\n", sum, sub)
}
函数作为变量使用
package main
​
import "fmt"//全局变量
var num int = 10//
func testGlobal() {
    num = 100
    fmt.Println("全局变量:", num)
}
​
func main() {
    //testGlobal()
    //函数可以作为变量
    abc := testGlobal
    fmt.Println(abc)
    fmt.Printf("%T\n", abc)
    abc()
}
函数作为参数传递
package main
​
​
import "fmt"
​
​
func add(x, y int) int {
    return x + y
}
func sub(x, y int) int {
    return x - y
}
func calc(x, y int, op func(int, int) int) int {
    return op(x, y)
}
​
func main() {
    //函数作为参数传递
    ret1 := calc(100, 200, add)
    ret2 := calc(100, 200, sub)
    fmt.Println(ret1)
    fmt.Println(ret2)
}
匿名函数
package main
​
import "fmt"//匿名函数
func main() {
​
    //定义并执行匿名函数
    func() {
        fmt.Println("匿名函数")
    }()
​
    /*
        sayHello := func() {
            fmt.Println("匿名函数")
        }
​
        sayHello()
    */
​
}
切片类型作为函数参数

slice、map、channel都是引用类型,它们作为函数参数时其实跟普通struct没什么区别,都是对struct内部的各个字段做一次拷贝传到函数内部。

package main
​
import "fmt"func slice_arg_1(arr []int) { //slice作为参数,实际上是把slice的arrayPointer、len、cap拷贝了一份传进来
    arr[0] = 1                //修改底层数据里的首元素
    arr = append(arr, 1) //arr的len和cap发生了变化,不会影响实参
}
​
func main() {
    arr := []int{8}
    slice_arg_1(arr)
    fmt.Println(arr[0])   //1
    fmt.Println(len(arr)) //1
}
package main
​
​
import "fmt"
​
​
//函数传递数组只是拷贝原数组
//函数传递切片只拷贝切片但共用底层数组,如果切片扩容就会更改底层数组
​
​
func foo(arr []int) {
    arr = append(arr, 8)
    fmt.Printf("%p %v %d %d\n", &arr, arr, len(arr), cap(arr))
}
func main() {
    arr := []int{2, 5}
    fmt.Printf("%p %v %d %d\n", &arr, arr, len(arr), cap(arr))
    foo(arr)
    fmt.Printf("%p %v %d %d\n", &arr, arr, len(arr), cap(arr))
​
​
}
package main
​
​
import "fmt"
​
​
type User struct {
    Name string
}
​
​
//结构体slice传参
/* 传slice,对sclice的3个字段进行了拷贝
拷贝的是底层数组的指针
所以修改底层数组的元素会反应到原数组上,除非切片扩容超过初始容量
    type slice struct {
    array unsafe.Pointer
    len int
    cap int
    }
​
​
*/
func update_users(user []User) {
    user[0].Name = "光绪"
    user = append(user, User{Name: "雍正"})
    fmt.Println(user, len(user), cap(user))
}
func main() {
    users := []User{{Name: "康熙"}}
    fmt.Println(users, len(users), cap(users))
​
​
    update_users(users)
    fmt.Println(users, len(users), cap(users))
​
​
}

append函数接收的就是不定长参数。

package main
​
import (
    "fmt"
)
​
func main() {
    s := "123我爱你中国"
    arr := []byte(s)
    brr := []rune(s)
​
    var crr []byte
    var drr []rune
    //append接受的是可变参数
    crr = append([]byte{}, arr...)
    drr = append([]rune{}, brr...)
​
    fmt.Println(crr)
    fmt.Println(drr)
​
    arr = append(arr, 1, 2, 3)
    arr = append(arr, 7)
    arr = append(arr)
​
    fmt.Println(arr)
​
    //string并不会隐式转化rune
    str := "我爱你中国123"
    fmt.Printf("%T\n", str[4])
​
    slice := append([]byte("hello "), "world"...)
    //...自动把"world"转成byte切片,等价于[]byte("world")...
    slice2 := append([]rune("hello "), []rune("world")...)
    //需要显式把"world"转成rune切片
​
    fmt.Println(slice)
    fmt.Println(slice2)
​
}
闭包

指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

package main
​
​
import "fmt"
​
​
// 定义一个函数并且它的返回值是一个函数
// 把函数作为返回值
// 闭包 = 函数 + 外层变量的引用
func a(name string) func() {
    return func() {
        fmt.Println("匿名函数", name)
    }
}
​
​
func main() {
    r := a("sunyan") //此时r就是一个闭包
    r()              //相当于执行了a()函数内部的匿名函数
​
​
}
package main
​
​
import "fmt"
​
​
// 定义一个函数并且它的返回值是一个函数
// 把函数作为返回值
// 闭包 = 函数 + 外层变量的引用
func a(name string) func() {
    return func() {
        fmt.Println("匿名函数", name)
    }
}
​
​
func main() {
    r := a("sunyan") //此时r就是一个闭包
    r()              //相当于执行了a()函数内部的匿名函数
​
​
}
package main
​
​
import (
    "fmt"
    "strings"
)
​
​
func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}
​
​
func main() {
    r := makeSuffixFunc(".txt")
    ret := r("sunyan")
    fmt.Println(ret)
}

2.11指针

指针地址、指针类型和指针取值

Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

a := 10
ptr := &a
package main
​
​
import "fmt"
​
​
func fun(num *int) {
    *num = 100
}
​
​
func main() {
    a := 10
    b := &a
​
​
    c := *b //指针取值
    fmt.Println(c, *b)
​
​
    num := 1
    fun(&num)
    fmt.Println(num)
​
​
}
  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

内置函数new

func new(Type) *Type
  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

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

// new make
func main() {
    //指针,map没有初始化不能使用
    //var a *int     a = nil
    //a = new(int)   对指针初始化
    //fmt.Println(a)// new函数得到一个指针,并且该指针的值为此类型的0值
    a := new(int)
    fmt.Printf("%T %v\n", a, *a)
​
    b := new(string)
    fmt.Printf("%T %v\n", b, *b)
​
    var oneMap map[string]int
    oneMap = make(map[string]int,10)
    oneMap["sun"] = 111
​
}
  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

2.12结构体

package main
​
import "fmt"type myInt int   //自定义数据类型
type myint = int //类型别名func main() {
    var num myInt = 111
    fmt.Printf("%T %d\n", num, num)
    var num1 myint = 222
    fmt.Printf("%T %v\n", num1, num1)
}

img

package main
​
import "fmt"//定义结构体
type Person struct {
    name string
    age  int
    city string
}
​
​
func main() {
    //结构体实例化
    var person Person
    person.name = "sun"
    person.age = 19
    person.city = "wuhan"
​
    fmt.Printf("%#v\n", person)
​
    //匿名结构体通常用于只使用一次的情况
    var user struct {//声明stu是一个结构体
        name string
        age  int
    }
    user.name = "yan"
    user.age = 20
​
    fmt.Printf("%#v\n", user)
}
package main
​
import "fmt"
​
//定义结构体
type Person struct {
    name string
    age  int
    city string
}
​
func main() {
    //结构体指针
​
    var person = new(Person)
    /*
        (*person).name = "sun"
        (*person).age = 19
        (*person).city = "wuhan"
    */
    //go语言语法糖可以直接
    person.name = "sun"
    person.age = 19
    person.city = "wuhan"
    fmt.Printf("%#v\n", person)
​
    //取结构体地址进行实例化
    p := &Person{}
    fmt.Printf("%T\n", p)
    p.name = "yan"
    p.age = 20
    p.city = "wang"
​
    fmt.Printf("%#v\n", p)
​
}
package main
​
import "fmt"//定义结构体
type Person struct {
    name string
    age  int
    city string
}
​
func main() {
    //键值对初始化
    person := Person{
        name: "sunyan",
        age:  20,
    }
    fmt.Printf("%#v\n", person)
    //骚操作
    person1 := &Person{
        name: "sunyan",
        age:  20,
    }
    fmt.Printf("%#v\n", person1)
​
​
    //值的列表进行初始化
    //必须初始化结构体的所有字段
    //初始值填充顺序必须与结构体声明字段一致
    //不能与键值方式混用
    person2 := Person{
        "sun",
        20,
        "shiyan",
    }
    fmt.Printf("%#v\n", person2)
    
    person3 := &Person{
        "sun",
        20,
        "shiyan",
    }
    fmt.Printf("%#v\n", person3)
​
​
}

构造函数

GO中没有像Java的构造函数,构造函数返回结构体指针,节约内存空间

package main
​
​
import "fmt"
​
​
type Person struct {
    name  string
    age   int
    score float64
}
​
​
//构造函数
func newPerson(name string, age int, score float64) *Person {
    return &Person{//&实例化Person
        name:  name,
        age:   age,
        score: score,
    }
}
​
​
func main() {
    person := newPerson("sunyan", 19, 99)
    fmt.Printf("%#v\n", person)
}

2.13方法

Go 语言中的方法的本质就是,一个以方法的 receiver 参数 作为第一个参数的普通函数

package main
​
​
import "fmt"
​
​
type Person struct {
    name  string
    age   int
    score float64
}
​
​
//构造函数
func newPerson(name string, age int, score float64) *Person {
    return &Person{
        name:  name,
        age:   age,
        score: score,
    }
}
​
​
//方法
func (p Person) Demo() {
    fmt.Printf("I want to sutdy Go well! %d\n", p.age)
}
​
func (p1 *Person) setAge(newAge int) {
    p1.age = newAge
}
​
func main() {
    person := newPerson("sunyan", 19, 99)
    //(*person).Demo()语法糖
    person.Demo()
    person.setAge(100)
    fmt.Println(person.age)
​
​
}

Go 方法本质上其实是一个函数,这个函数以方法的 receiver 参数作为第一个参数,Go 编译器会在我们进行方法调用时协助进行这样的转换

string

Go 字符串类型性质
获取长度的时间复杂度是常数时间
string 类型通过函数 / 方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描
述符”,而不是真正的字符串数据

Go 语言源文件默认采用的是 Unicode 字符集, 每个字符都是一个 Unicode 字符

这些 Unicode 字符是以 UTF-8 编码格式存储的

string 类型的数据是不可变的,提高了字符串的并发安全性和存储利用率

无法通过这个变量改变它对应的字符串值的,但可以为一个字符串类型变量进行二次赋值

var str string = "hello"
str[0] = 'H'   // 错误:字符串的内容是不可改变的
str = "gopher" // ok

多行字符串使用反引号定义

    var s string = `         ,_---~~~~~----._
    _,,_,*^____      _____*g*"*,--,
   / __/ /'     ^.  /      \ ^@q   f
  [  @f | @))    |  | @))   l  0 _/   /   ~____ / __ _____/     \   |           _l__l_           I    }          [______]           I
    ]            | | |            |
    ]             ~ ~             |
    |                            |
     |                           |`
    fmt.Printf("%s\n", s)

Go 语言中的字符串值是一个可空的字节序列字节序列中的字节个数称为该字符串的长度。一个个的字节只是孤立数据,不表意

var s1 string = "中国人"
    fmt.Printf("the length of s: %d\n", len(s1))//字符串长度9
    for i := 0; i < len(s1); i++ {
        fmt.Printf("0x%x\n", s1[i]) //Uncode字符值0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
    }

字符串是由一个可空的字符序列构成

var s2 = "中国人"
    fmt.Printf("the character count in s2 is: %d\n", utf8.RuneCountInString(s2))//字符个数
    for _, v := range s2 {
        fmt.Printf("0x%x\n", v)//uncode码点
    }

字符串内部表示

/ $GOROOT/src/reflect/value.go// StringHeader是一个string的运行时表示
type StringHeader struct {
    Data uintptr
    Len  int
}

string 类型其实是一个“描述符”,它本身并不真正存储字符串数据,而仅是由一个指向底层存储的指针(byte数组)和字符串的长度字段组成的

img

Go 使用 rune 这个类型来表示一个 Unicode 码点, 一个 rune 实例就是一个 Unicode 字符

type rune = int32

字符串操作

遍历

for

var s = "中国人"
for i := 0; i < len(s); i++ {
    fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}
每轮迭代得到的的结果都
是组成字符串内容的一个字节,以及该字节所在的下标值

for range

var s = "中国人"
for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}
通过 for range 迭代,我们每轮迭代得到的是字符串中 Unicode 字符的码点
值,以及该字符在字符串中的偏移值

连接

s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"
fmt.Println(s) // Rob Pike, Robert Griesemer, Ken Thompson

strings.Builder、strings.Join、fmt.Sprintf

比较

如果两个字符串的长度不相同,那么不需要比较具体字符串数据,也可以断定两个字符串是不同的。但是如果两个字符串长度相同,就要进一步判断,数据指针是否指向同一块底层存储数据。如果还相同,那么我们可以说两个字符串是等价的,如果不同,那就还需要进一步去比对实际的数据内容。

        s1 := "世界和平"
        s2 := "世界" + "和平"
        fmt.Println(s1 == s2) // true
​
        // !=
        s1 = "Go"
        s2 = "C"
        fmt.Println(s1 != s2) // true
​
        // < and <=
        s1 = "12345"
        s2 = "23456"
        fmt.Println(s1 < s2)  // true
        fmt.Println(s1 <= s2) // true
​
        // > and >=
        s1 = "12345"
        s2 = "123"
        fmt.Println(s1 > s2)  // true
        fmt.Println(s1 >= s2) // true

转换

Go 支持字符串与字节切片、字符串与 rune 切片的双向转换

var s string = "中国人"
                      
// string -> []rune
rs := []rune(s) 
fmt.Printf("%x\n", rs) // [4e2d 56fd 4eba]
                
// string -> []byte
bs := []byte(s) 
fmt.Printf("%x\n", bs) // e4b8ade59bbde4baba
                
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 中国人
                
// []byte -> string
s2 := string(bs)
fmt Println(s2) // 中国人