Go语言的基础知识

413 阅读9分钟

前端尝试转GO学习(第三天)

指针

指针可以创建变量,称为指针变量。指针变量的类型为 *Type,该指针指向一个 Type 类型的变量。指针变量会存储的某个实际变量的内存地址,通过记录变量的地址,间接的操作该变量。

创建指针

定义变量,之后通过获取变量的地址创建指针

func pointFunc()  {
   // 变量 p
   p := "学习"
   // 取变量 p 的地址创建指针 point
   point := &p

   fmt.Print(point) // 0xc000096210
}

先创建指针并分配好内存,再给指针指向的内存地址写入对应的值

// 创建指针
p1 := new(string)
// 给指针指向的内存地址写入对应的值
*p1 = "Go 语言学习"

fmt.Println(p1)

fmt.Println(*p1)

声明指针变量,再从其他变量获取内存地址给指针变量

// 定义变量 x2 
x2 := "React" 
// 声明指针变量 
var p *string 
// 指针初始化 
p = &x2 
fmt.Println(p) 
x := "学习Go"
ptr := &x
fmt.Println("x = ", x)       // x =  面向加薪学习
fmt.Println("*ptr = ", *ptr) // *p =  面向加薪学习
fmt.Println("&x = ", &x)     // &x =  0xc000010270
fmt.Println("ptr = ", ptr)   // p =  0xc000010270

&:从一个变量中取到其内存地址。

*:如果在赋值操作值的左边,指该指针指向的变量;如果在赋值操作符的右边,指从一个指针变量中取得变量值,又称指针的解引用。

指针的类型

*(指向变量值的数据类型) 就是对应的指针类型。

func pointerType() {
   testStr := "Go"
   testIntNumber := 1
   testBool := false
   testFloatNumber := 3.2
   fmt.Printf("type of &mystr is :%T\n", &testStr)     // *string
   fmt.Printf("type of &myint is :%T\n", &testIntNumber)     // *int
   fmt.Printf("type of &mybool is :%T\n", &testBool)   // *bool
   fmt.Printf("type of &myfloat is :%T\n", &testFloatNumber) // *float64
}

指针的零值

如果指针声明后没有进行初始化,其默认零值是 nil

func zeroPointer() {
   var zeroPoint *int
   fmt.Println(zeroPoint) // <nil>
}

函数传递指针参数

在函数中对指针参数所做的修改,在函数返回后会保存相应的修改。

func changeByPointer(value *int) {
   *value = 200
}
func funcPoint() {
   test01 := 1
   point01 := &test01
   fmt.Println("before", *point01) // 1
   changeByPointer(point01)
   fmt.Println("after", *point01) // 200
}

函数传入的是指针参数(内存地址),因此在函数内的修改是在内存地址上的修改,在函数执行后还会保留结果。

指针与切片

切片与指针一样是引用类型。通过一个函数改变一个数组的值,可以将该数组的切片当作参数传给函数,也可以将这个数组的指针当作参数传给函数。

// 使用切片
func changeSlice(value []int) {
   value[0] = 200
}

// 使用数组指针
func changeArray(value *[3]int) {
   (*value)[0] = 200
}

func arrayPoint() {
   test01 := [3]int{10, 20, 30}
   changeSlice(test01[:])
   fmt.Println(test01) // [200 20 30]

   test02 := [3]int{100, 200, 300}
   changeArray(&test02)
   fmt.Println(test02) // [200 200 300]
}

Go 中不支持指针运算

x := [...]int{20, 30, 40}
p := &x
p++ // error

结构体

结构体(struct)  是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。在 Go 中,没有像 C++ 中的 class 类的概念,只有 struct 结构体的概念,所以也没有继承。

结构体的声明

在 Go 语言 中使用下面的语法是对结构体的声明。

type struct_name struct {
    attribute_name1   attribute_type
    attribute_name2   attribute_type
    ...
}

定义一个Person 结构体。

type Person struct {
	name   string
	age int
	sex  int
}

声明了一个结构体类型 Person ,有 name 、 age 和 sex 三个属性。

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

var Person struct {
    name    string
    // 相同类型的属性声明在同一行
    age,  sex    int
}

创建命名的结构体

func createPerson()  {

   // 没有被显式初始化时,结构体的字段将会默认赋为相应类型的零值。
   person_zero := Person{} 
   
   person_one := Person{
      name: "小明",
      age:  18,
      sex:  1,
   }
   person_two := Person{
      "小红",
      17,
      1,
   }

   fmt.Println(person_one)
   fmt.Println(person_two)
}

创建匿名结构体

person_three := struct {
   name     string
   age, sex int
}{
   name: "小蓝",
   age:  14,
   sex:  0,
}

访问结构体的字段

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

fmt.Println(person_one.name, person_one.age, person_one.sex)
// 小明 18 1

当然也使用点操作符 . 可以用于对结构体的字段的赋值。

person_one.name = "小鹿"

指向结构体的指针

person_four := &Person{
   "小黑",
   17,
   1,
}
fmt.Println((*person_four).name)
fmt.Println(person_four.name)

在上面的程序中, person_four 是一个指向结构体 Person 的指针,上面用 (*person_four).name 访问 person_four 的 name 字段,上面的 person_four.name 代替 (*person_four).name 的解引用访问。

匿名字段

在创建结构体时,字段可以只有类型没有字段名,这种字段称为 匿名字段(Anonymous Field)  。

type UniquePerson struct {
   string
   int
}

person_five := UniquePerson{
   "hhhhh",
   111,
}

fmt.Println("person_five", person_five)

结构体中虽然这两个字段没有字段名,但匿名字段的名称默认就是它的类型。所以上面的结构体 UniquePerson 有两个名为 string 和 int 的字段。

嵌套结构体

结构体的字段也可能是另外一个结构体,这样的结构体称为 嵌套结构体(Nested Structs)

type Author struct {
   name string
   age  int
}

type Person01 struct {
   name   string
   price  int
   author Author
}

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用 == 或 != 运算符进行比较。可以通过==运算符或 DeeplyEqual()函数比较两个结构相同的类型并包含相同的字段值。因此下面两个比较的表达式是等价的:

person_01 := Person{
   name: "abc",
   age:  12,
   sex:  0,
}
person_02 := Person{
   name: "abc",
   age:  12,
   sex:  0,
}

fmt.Println(person_01 == person_02)

给结构体定义方法

与 JS 中 Class 不同的是,Go 中结构体无法定义方法。

type Person02 struct {
   name string
   age  int
}

func (p Person02) speak() {
   fmt.Println("name", p.name)
   fmt.Println("age", p.age)
}

person_03 := Person02{
   name: "奶油桃子",
   age:  12,
}

person_03.speak()

// name 奶油桃子
// age 12

上面的程序中定义了一个与结构体 Person02 绑定的方法 speak() ,其中 speak 是方法名, (p Person02) 表示将此方法与 Person02 的实例绑定,这在 Go 语言中称为接收者,而 p 表示实例本身。

函数

函数的声明

func function_name(params_list) (return_list) {
    //函数体
}

函数声明使用 func 关键词,后面依次接 function_name(函数名) , params_list(参数列表) , return_list(返回值列表) 以及 函数体 。

无参数列表和返回值

func test01() {
   fmt.Println("test")
}

函数返回一个无名变量,返回值列表的括号省略

func sum(x int, y int) int {
   return x + y
}

参数的类型一致,只在最后一个参数后添加该类型

func sub(x , y int) int {
   return x - y
}

可变参数

多个类型一致的参数

参数中加 ... 用来接收传入的参数。如果该函数下有其他类型的参数,这些其他参数必须放在参数列表的前面,...必须放在最后。这一点和 JS 的 args 还是有一些类似的。

func sum(x int, y int, args ...int) int {
   sum := x + y
   for _, item := range args {
      sum += item
   }
   return sum
}

func main() {
   fmt.Println(sum(10, 20, 1, 2, 3, 4, 5, 6)) // 51
}
多个类型不一致的参数

如果传多个参数的类型都不一样,可以指定类型为 ...interface{} ,然后再遍历。

func PrintType(args ...interface{}) {
   for _, arg := range args {
      switch arg.(type) {
      case int:
         fmt.Println(arg, "type is int.")
      case string:
         fmt.Println(arg, "type is string.")
      case float64:
         fmt.Println(arg, "type is float64.")
      default:
         fmt.Println(arg, "is an unknown type.")
      }
   }
}

func main() {
   PrintType(12, 1.677, "哈哈哈哈")
}

匿名函数

没有名字的函数叫 匿名函数 ,只有函数逻辑体,而没有函数名。

func (params_list) (return_list) {
    body
}

内部方法与外部方法

在 Go 语言中,函数名通过首字母大小写实现控制对方法的访问权限。

  • 当方法的首字母为 小写 时,这个方法是 Private ,其他包是无法访问的。

  • 当方法的首字母为 大写 时,这个方法对于 所有包 都是 Public ,其他包可以随意调用。

包(package)  用于组织 Go 源代码,提供了更好的可重用性与可读性。Go 语言有超过 100 个的标准包,这些库为大多数的程序提供了必要的基础组件。

main

包含一个 main() 函数,是程序运行的入口。

package packagename 代码指定了某源文件属于某个包。放在每一个源文件的第一行。

package main

import "fmt"

func main() {
    fmt.Println("Go")
}

创建包

创建自定义的 dog 包,按照 Go 的惯例,应该用包名命名该文件夹。所以应当先创建一个 dog 文件夹,位于该目录下创建一个 dog.go 源文件,函数名的首字母要大写。

package dog

import "fmt"

type Dog struct {
   color string
   name  string
   sex   int
}

func Adog() {
   dog01 := Dog{
      "黑色",
      "桃桃",
      1,
   }

   fmt.Print("dog\n", dog01)
}

导入包

使用包之前我们需要导入包,在 GoLand 中会帮你自动导入所需要的包。导入包的语法为 import path ,其中 path 可以是相对于工作区文件夹的相对路径,也可以是绝对路径。

包的初始化

每个包都允许有一个或多个 init 函数, init 函数不应该有任何返回值类型和参数,在代码中也不能显式调用它,当这个包被导入时,就会执行这个包的 init 函数,做初始化任务, init 函数优先于 main 函数执行。该函数形式如下:

func init() {
}

包的初始化顺序:首先初始化 包级别(Package Level)  的变量,紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。如果一个包导入了另一个包,会先初始化被导入的包。尽管一个包可能会被导入多次,但是它只会被初始化一次。

包的匿名导入

导入一个没有使用的包编译会报错。但有时候我们只是想执行包里的 init 函数来执行一些初始化任务的话应该怎么办呢?使用匿名导入的方法,使用 空白标识符(Blank Identifier)  :

import _ "fmt" 

由于导入时会执行该包里的 init 函数,所以编译仍会将此包编译到可执行文件中。

参考文章:go-edu.cn/

本文正在参加技术专题18期-聊聊Go语言框架