[TOC]
我们不会把所有的代码都写在同一个文件下面,那么怎么调用其他模块下的内容呢?我们引入了包
包的引入使得我们可以去调用自己或者别人的模块代码,方便了我们的开发
我们第一次在电脑上打印出hello world的时候,就引入了一个fmt包
package main
import "fmt" //引入fmt包
func main(){
fmt.Println("Hello world!") //Println是fmt包里面的一个函数
}我们可以根据自己的需要创建自定义包。一个包可以简单理解为一个存放.go文件的文件夹。
该文件夹下面的所有.go文件都要在非注释的第一行添加如下声明,声明该文件归属的包。
package packagename
另外需要注意一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下。
包名为main的包是应用程序的入口包
在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("Hello world!")。
如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。
例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。
var Name string // 可在包外访问的方法
var class string // 仅限包内访问的字段
要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整的引入声明语句格式如下:
import importname "path/to/package"
其中:
- importname:引入的包名,通常都省略。默认值为引入包的包名。
- path/to/package:引入包的路径名称,必须使用双引号包裹起来。
- Go语言中禁止多个包相互导入
一个Go源码文件中可以同时引入多个包,例如:
import "fmt"
import "net/http"
import "os"
当然可以使用批量引入的方式。
import (
"fmt"
"net/http"
"os"
)
如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。
一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。
import _ "github.com/go-sql-driver/mysql"
init函数不用显示调用就可以自己执行,比如
package main
import "fmt"
func init() {
fmt.Println("第一遍执行init")
}
func main() {
fmt.Println("这是main函数")
}
func init() {
fmt.Println("第二遍执行init")
}
//这个的执行顺序是什么?任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
*Go语言中的指针不能进行偏移和运算*,因此Go语言中的指针操作不像C语言中的那么复杂,我们只需要记住两个符号:&(取地址)和*(取值)。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的类型如(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
取变量指针的语法如下:
ptr := &v // v的类型为T
其中:
- v:代表被取地址的变量,类型为
T - ptr:用于接收地址的变量,ptr的类型就为
*T,称做T的指针类型。*代表指针。
举个例子:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}输出如下:
type of b:*int
type of c:int
value of c:10总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
*对指针变量进行取值(\*)操作,可以获得指针变量指向的原变量的值*。
指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Printl
n(a) // 100
}尝试通过空指针访问其指向的值将导致运行时错误(panic),因此在使用空指针之前,通常需要检查它是否为nil,否则会出现空指针异常,比如
package main
import "fmt"
func main() {
var p *int = nil // 声明一个指向int类型的空指针
// 尝试通过空指针p访问其指向的值,这将导致运行时panic
// 因为p是空指针,没有指向任何有效的内存地址
fmt.Println(*p) // 这里会发生空指针异常
}介绍结构体之前我们需要先说一些与其相关的基础知识
Go语言中可以使用type关键字来定义自定义如string、int、bool等的数据类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型
type MyInt int
通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名规定:本质上是同一个类型。就像一个孩子小时候有小名,这和他的名字都指向同一个人。
type 类型的别名 = 类型名
类型别名 和原类型是同一种类型。自定义类型是一种全新的类型。
类型别名的类型只会在代码中存在,编译完成时并不会存在。
类型别名 和 自定义类型 的意义
自定义类型 : 举个例子,我们想给 int 类型定义一个方法,但是又不想改变int本身的性质。可以基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
类型别名: 如果有一个非常长的类型名字,比如map[int]string,如果在代码中反复使用这个类型,那将会变得很啰嗦。
但是如果你使用类型别名来代替它,比如data,那么你只需使用Data这个简短的名字就可以代替长长的类型名字了,比如
// 定义一个类型别名Data,它等同于map[int]string
type Data map[int]stringGo语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,比如我想要描述一个学生信息,包括姓名,年龄,成绩,我们就需要用到结构体(struct)
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个刚刚的Student(学生)结构体,代码如下:
type Student struct {
name string
age int
Grade float64
}对于没有初始化的结构体,它的字段值都是对于类型的零值,比如
package main
import "fmt"
type Student struct {
name string
age int
Grade float64
}
func main() {
var Stu Student
fmt.Println(Stu) //{ 0 0}
}使用字段名初始化结构体
Stu := Student{
name: "Alice",
age: 20,
Grade: 3.5,
}忽略字段名初始化结构体
// 这里要注意的是初始化字段的顺序必须是声明结构体时的字段顺序
Stu2 := Student{
"Bob",
22,
3.8,
}我们可以在结构体名字和字段之前用.来连接,以此访问结构体的字段,比如
var Stu Student
Stu.name = "小明"
Stu.age = 18//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf(user1)//{小王子 男 {山东 威海}}
}当你声明一个指向结构体的指针变量并访问其字段时,你可以直接使用点 . 操作符来访问字段,而不需要显式地解引用指针(也就是使用*号)。这是 Go 语言为了简化编码过程而提供的便利,比如
type Person struct {
Name string
Age int
}
func main() {
// 创建一个Person类型的指针
p := &Person{Name: "John", Age: 30}
// 通过指针访问结构体的字段
fmt.Println((*p).Name) // 输出: John
fmt.Println((*p).Age) // 输出: 30
fmt.Println(p.Name)
fmt.Println(p.Age)
}Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。
只有特定的接收者变量才可以调用对应的方法。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}其中
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写。例如,
Person类型的接收者变量应该命名为p。 - 接收者类型:接收者类型和参数类似,可以是
指针类型和非指针类型。 - 方法名、参数列表、返回参数:具体格式与函数定义相同。
比如
package main
import "fmt"
type Student struct {
Name string
Age int
Grade float64
}
func NewStudent(name string, age int, grade float64) *Student {
return &Student{
Name: name,
Age: age,
Grade: grade,
}
}
func (s Student) study() {
fmt.Printf("我是%s我正在卷", s.Name)
}
func main() {
stu := NewStudent("小明", 18, 4.0)
stu.study()
}指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。 例如我们为Student添加一个SetAge方法,来修改实例变量的年龄。
func (s *Student)SetAge(age int) {
s.Age = age
}func main() {
stu := NewStudent("小明", 18, 4.0)
fmt.Println("修改前age=", stu.Age) //18
stu.SetAge(17)
fmt.Println("修改后age=", stu.Age) //17
}当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
func (s Student)SetAge(age int) {
s.Age = age
}
func main() {
stu := NewStudent("小明", 18, 4.0)
fmt.Println("修改前age=", stu.Age) //18
stu.SetAge(17)
fmt.Println("修改后age=", stu.Age) //18
}当我们同时有指针类型和值类型的接收者时,编译器会提示结构体 student 在值接收器和指针接收器上都有方法。Go 文档不推荐使用此类用法
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么,而不是它是什么。
我们日常会一般会听到几种接口,比如usb接口(硬件接口),应用程序接口(api),以及我们这里所说的接口类型。
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子,定义一个包含Write方法的Writer接口。
type Writer interface{
Write([]byte) error
}实现接口是什么意思呢,比如要实现上方的Writer接口,我们就要实现这个接口中所定义的方法,实现了接口定义的所有方法,我们就实现了这个接口,下面我们举一个简单的例子
假设有两款音乐播放器,它们都可以播放水声,那么我们可以顶一个Sounder接口,Music1和Music2都实现了这个接口
package main
import "fmt"
type Sounder interface {
water()
}
type Music1 struct{}
type Music2 struct{}
func (m1 Music1) water() {
fmt.Println("这是music1的水声")
}
func (m2 Music2) water() {
fmt.Println("这是music2的水声")
}
func Play(s Sounder) {
s.water()
}
func main() {
var m2 Music2
var m1 Music1
Play(m1) //这是music1的水声
Play(m2) //这是music2的水声
}因为这里Sounder里面只有一个water()方法,所以只需要给Music1和Music2结构体添加一个water方法就可以满足Sounder接口的要求,实现了方法就实现了这个接口,所有我们可以将两个结构体作为参数传给Play(s Sounder)函数
在 Go 语言中,接口断言是一种检查接口变量是否具有特定具体类型的方法。接口断言的基本语法如下:
value, ok := interfaceVariable.(Type)其中:
interfaceVariable是一个接口类型的变量Type是你想要断言的具体类型- 如果
interfaceVariable实际上是一个Type类型的值,那么value将会被赋值为该值,并且ok将会是true。如果interfaceVariable不是Type类型,那么value将会是Type类型的零值,而ok将会是false - 对于空接口
interface{},任何类型的值都可以被赋值给它,因此对接口断言的需求更加常见
假设我们有一个接口类型的变量 var i interface{},并且我们不确定它到底是什么类型,但我们想检查它是否是一个 int 类型
package main
import "fmt"
func main() {
var i interface{} = 42
// 尝试断言为int类型
if v, ok := i.(int); ok {
fmt.Println("i是一个int类型的值:", v)
} else {
fmt.Println("i不是int类型的值")
}
// 尝试断言为 string
if _, ok := i.(string); !ok {
fmt.Println("i不是string类型的值")
}
}其实这里的接口类型和硬件接口是非常相似的,我们的鼠标,键盘,u盘的接口都是一种形状,就是方便节约空间,如果每个外设的接口都不一样,那么电脑所提供空间肯定是不够的,而这种做成这种”形状“,类比到我们今天的接口类型,就可以理解为实现了接口。
等级不代表难度
可以选择LV1到LV3或者单独完成LVX
LV1: 温度转换器
实现一个温度转换器系统,支持摄氏度和华氏度之间的相互转换
自定义结构体,实现以下方法:
ToFahrenheit():将摄氏温度转换为华氏温度,并更新Fahrenheit字段。ToCelsius():将华氏温度转换为摄氏温度,并更新Celsius字段
LV2: 字符串工具包
创建一个名为 utils 的包。
在包内实现以下函数:
Reverse(s string) string:将字符串反转。IsPalindrome(s string) bool:判断字符串是否为回文(如aba,abcba,abba)
编写主程序,导入 utils 包并测试这些函数
LV3:计算几何图片面积
定义一个接口 Shape,包含一个方法 Area() float64。
定义两个结构体 Circle 和 Rectangle(矩形),或自定义几何图形,并分别实现 Area 方法,并计算面积
LVX:实现一个电子商务系统(学有余力可完成)
设计一个电子商务平台,该平台有多种类型的商品,例如电子产品、家居用品和服装等。你需要设计以下结构体和功能:
- 实现商品结构体:包含商品的名称、价格和库存数量等信息。
- 实现接口:定义商品的库存管理功能,包括检查库存数量、更新库存数量和打印库存信息,出售,进货等等。
- 实现电子产品结构体:继承自商品结构体,同时具有电子产品特有的属性,例如品牌和型号。
- 实现接口:定义电子产品结构体的库存管理功能(同上),以及打印品牌型号信息的功能
(不一定要完全按照要求的逻辑,有自己的想法即可,可以做了多少交多少,半成品也没关系)
完成后提交到邮箱lihaoyu@lanshan.email