Go基础语法(五)

字符串

  • Go 语言中的字符串是一个字节切片。把内容放在双引号""之间,我们可以创建一个字符串。
  • Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码。
package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}


func printChars(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%c ",s[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
}

如果换成这个字符串:Señor,进行切片遍历,可以发现输出的字节是不对的:S e à ± o r。
这是因为 ñ 的 Unicode 代码点(Code Point)是 U+00F1。它的 UTF-8 编码占用了 c3 和 b1 两个字节。它的 UTF-8 编码占用了两个字节 c3 和 b1。而我们打印字符时,却假定每个字符的编码只会占用一个字节,这是错误的。在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间。那么我们该怎么办呢?rune 能帮我们解决这个难题。

rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。让我们修改一下上面的程序,用 rune 来打印字符。

package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {
    runes := []rune(s)
    for i:= 0; i < len(runes); i++ {
        fmt.Printf("%c ",runes[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
    fmt.Printf("\n\n")
    name = "Señor"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
}

在上面代码的第 14 行,字符串被转化为一个 rune 切片。然后我们循环打印字符。

字符串的 for range 循环
func printCharsAndBytes(s string) {
    for index, rune := range s {
        fmt.Printf("%c starts at byte %d\n", rune, index)
    }
}

func main() {
    name := "Señor"
    printCharsAndBytes(name)
}

使用 for range 循环遍历了字符串。循环返回的是是当前 rune 的字节位置。

字符串的长度

utf8 package 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func length(s string) {  
    fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s))
}
func main() { 
    word1 := "Señor" 
    length(word1)
    word2 := "Pets"
    length(word2)
}
字符串是不可变的

Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。

package main

import (  
    "fmt"
)

func mutate(s string)string {  
    s[0] = 'a'//any valid unicode character within single quote is a rune 
    return s
}
func main() {  
    h := "hello"
    fmt.Println(mutate(h))
}

为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

package main

import (  
    "fmt"
)

func mutate(s []rune) string {  
    s[0] = 'a' 
    return string(s)
}
func main() {  
    h := "hello"
    fmt.Println(mutate([]rune(h)))
}

指针

学过C语言的知道,指针是一种存变量内存地址的变量。


Go基础语法(五)
image.png

如上图所示,变量 b 的值为 156,而 b 的内存地址为 0x1040a124。变量 a 存储了 b 的地址。我们就称 a 指向了 b。

指针的声明

例如:指针变量的类型为 *T,该指针指向一个 T 类型的变量。

package main

import (
    "fmt"
)

func main() {
    b := 255
    var a *int = &b
    fmt.Printf("Type of a is %T\n", a)
    fmt.Println("address of b is", a)
}

运行结果:
Type of a is *int  
address of b is 0x1040a124
指针的零值(Zero Value)

指针的零值是 nil。

package main

import (  
    "fmt"
)

func main() {  
    a := 25
    var b *int
    if b == nil {
        fmt.Println("b is", b)
        b = &a
        fmt.Println("b after initialization is", b)
    }
}
指针的解引用

指针的解引用可以获取指针所指向的变量的值。将 a 解引用的语法是 *a。

package main  
import (  
    "fmt"
)

func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)

    *a++
    fmt.Println("new value of b is", b)
}

上述代码使用指针的解引用修改了b的值。

向函数传递指针参数
package main

import (  
    "fmt"
)

func change(val *int) {  
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}
不要向函数传递数组的指针,而应该使用切片
package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    (*arr)[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

a[x] 是 (a)[x] 的简写形式,因此上面代码中的 (arr)[0] 可以替换为 arr[0]。下面我们用简写形式重写以上代码。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    arr[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

这种方式向函数传递一个数组指针参数,并在函数内修改数组。尽管它是有效的,但却不是 Go 语言惯用的实现方式。我们最好使用切片来处理。

接下来我们用切片来重写之前的代码:

package main

import (  
    "fmt"
)

func modify(sls []int) {  
    sls[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

我们将一个切片传递给了 modify 函数。在 modify 函数中,我们把切片的第一个元素修改为 90。程序也会输出 [90 90 91]。所以别再传递数组指针了,而是使用切片吧。上面的代码更加简洁,也更符合 Go 语言的习惯。

Go 不支持指针运算

Go 并不支持其他语言(例如 C)中的指针运算。

package main

func main() {  
    b := [...]int{109, 110, 111}
    p := &b
    p++
}

上面的程序会抛出编译错误:main.go:6: invalid operation: p++ (non-numeric type *[3]int)

结构体

什么是结构体?

结构体是用户定义的类型,表示若干个字段(Field)的集合。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。

声明结构体

例如,一个职员有 firstName、lastName 和 age 三个属性,而把这些属性组合在一个结构体 employee 中就很合理。

type Employee struct {
    firstName string
    lastName  string
    age       int
}

还可以这样声明:

type Employee struct {
    firstName, lastName string
    age, salary         int
}

上面的结构体 Employee 称为 命名的结构体(Named Structure)。我们创建了名为 Employee 的新类型,而它可以用于创建 Employee 类型的结构体变量

声明结构体时也可以不用声明一个新类型,这样的结构体类型称为 匿名结构体(Anonymous Structure)

//匿名结构体
var employee struct {
    firstName, lastName string
    age int
}
创建命名的结构体
package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {

    //creating structure using field names
    emp1 := Employee{
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  "Anderson",
    }

    //creating structure without using field names
    emp2 := Employee{"Thomas", "Paul", 29, 800}

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
}

上述代码中两种方法创建结构体,第一种无需顺序对应声明时结构体中的字段,第二种必须顺序对应,不能乱序。

创建匿名结构体
package main

import (
    "fmt"
)

func main() {
    emp3 := struct {
        firstName, lastName string
        age, salary         int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

    fmt.Println("Employee 3", emp3)
}

之所以称这种结构体是匿名的,是因为它只是创建一个新的结构体变量 em3,而没有定义任何结构体类型。

结构体的零值(Zero Value)

当定义好的结构体并没有被显式地初始化时,该结构体的字段将默认赋为零值。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp4 Employee //zero valued structure
    fmt.Println("Employee 4", emp4)
}

string 的零值:(""),int的零值:0

当然还可以为某些字段指定初始值,而忽略其他字段。这样,忽略的字段名会赋值为零值。

访问结构体的字段

点号操作符 . 用于访问结构体的字段。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp6 := Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", emp6.firstName)
    fmt.Println("Last Name:", emp6.lastName)
    fmt.Println("Age:", emp6.age)
    fmt.Printf("Salary: $%d", emp6.salary)
}

还可以创建零值的 struct,以后再给各个字段赋值。

package main

import (
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp7 Employee
    emp7.firstName = "Jack"
    emp7.lastName = "Adams"
    fmt.Println("Employee 7:", emp7)
}
结构体的指针

还可以创建指向结构体的指针。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", (*emp8).firstName)
    fmt.Println("Age:", (*emp8).age)
}

Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (*emp8).firstName

匿名字段

当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    p := Person{"Naveen", 50}
    fmt.Println(p)
}

虽然匿名字段没有名称,但其实匿名字段的名称就是为它的类型。比如在上面的 Person 结构体里,虽说字段是匿名的,但 Go 默认这些字段名是它们各自的类型。所以 Person 结构体有两个名为 string 和 int 的字段。

可以这样操作:

func main() {  
    var p1 Person
    p1.string = "naveen"
    p1.int = 50
    fmt.Println(p1)
}
嵌套结构体(Nested Structs)

结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。

package main

import (  
    "fmt"
)

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age int
    address Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.address = Address {
        city: "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:",p.age)
    fmt.Println("City:",p.address.city)
    fmt.Println("State:",p.address.state)
}
提升字段(Promoted Fields)

匿名字段为结构体的字段称之为提升字段。

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age  int
    Address
}

上述代码中Person结构体中的匿名字段Address也是一个结构体,而Address结构体中有两个字段,访问这两个字段就像在 Person 里直接声明的一样,因此我们称之为提升字段。

package main

import (
    "fmt"
)

type Address struct {
    city, state string
}
type Person struct {
    name string
    age  int
    Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city) //city is promoted field
    fmt.Println("State:", p.state) //state is promoted field
}
导出结构体和字段

如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。

package computer

type Spec struct { //exported struct  
    Maker string //exported field
    model string //unexported field
    Price int //exported field
}
package main

import "structs/computer"  
import "fmt"

func main() {  
    var spec computer.Spec
    spec.Maker = "apple"
    spec.Price = 50000
    fmt.Println("Spec:", spec)
}

如果访问未导出的字段 model,编译器会提示错误。

结构体相等性(Structs Equality)

结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。

如果结构体包含不可比较的字段,则结构体变量也不可比较。

先看可比较的示例:

type name struct {
    firstName string
    lastName string
}


func main() {
    name1 := name{"Steve", "Jobs"}
    name2 := name{"Steve", "Jobs"}
    if name1 == name2 {
        fmt.Println("name1 and name2 are equal")
    } else {
        fmt.Println("name1 and name2 are not equal")
    }

    name3 := name{firstName:"Steve", lastName:"Jobs"}
    name4 := name{}
    name4.firstName = "Steve"
    if name3 == name4 {
        fmt.Println("name3 and name4 are equal")
    } else {
        fmt.Println("name3 and name4 are not equal")
    }
}

再看不可比较的示例:

package main

import (  
    "fmt"
)

type image struct {  
    data map[int]int
}

func main() {  
    image1 := image{data: map[int]int{
        0: 155,
    }}
    image2 := image{data: map[int]int{
        0: 155,
    }}
    if image1 == image2 {
        fmt.Println("image1 and image2 are equal")
    }
}

结构体类型 image 包含一个 map 类型的字段。由于 map 类型是不可比较的,因此 image1 和 image2 也不可比较。如果运行该程序,编译器会报错:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)。

如果以上文章对你有帮助,记得点赞加关注作者,后续持续更新哦~~~~

上一篇:Go基础语法(六)


下一篇:Go基础语法(四)