一般我们在学习一门语言的时候都是从helloworld开始学习的,下面我们就一起学习一下如何使用Go语言打印这个hello World.
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
和我们学习C语言的时候很类似,GO语言的基本组成有”
在go语言当中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。具体分类如下:
定义变量我们一般使用var关键字。具体格式为var 变量名字 变量类型。下面我们演示一个代码定义一个整型变量。
func main() {
var a int =10
//注意如果定义的时候不给初始值系统会给这个默认值
fmt.Println(a)
}
注意在go语言当中如果定义了一个变量如果没用初始化,则变量默认为零值。所谓的零值也就是变量没有做初始化时系统默认设置的值。下面让我们看看基本数据类型不初始化系统会赋那些零值。
在go语言当中如果定义变量时没有指定变量的类型,可以通过初始值推导出变量的类型。下面我看一下这个代码
package main
import "fmt"
func main() {
var a = 10//根据初始值推导a的类型
fmt.Println(a)
}
在go语言的函数内部我们可以使用:=来定义变量。使用的格式为typename:=value。这个一条语句包含了声明和初始化。
package main
import "fmt"
func main() {
a:=10
fmt.Println(a)
}
注意这个只能在函数内部这样,而定义这个全局变量需要使用的是var不能使用:=来定义,下面我们来看看如何定义这个全局变量
import "fmt"
var (
a int = 10
b float64 = 12.2
name string = "ksy"
)
func main() {
a := 10
fmt.Println(a)
}
什么是匿名变量?在go语言当中’_'代表的是这个匿名变量。它可以像其它标识符一样用于变量的声明和赋值。任何值赋给这个标识符都将会被抛弃,不能够再后续代码当中继续使用,在使用匿名变量时,只需要在变量声明的地方使用下划线进行替换即可。下面我们来看一个代码
func main() {
a := 10
fmt.Println(a)
_ =a
}
注意:匿名变量不占用内存,匿名变量不会因为多次声明而无法使用,这点是需要注意的。
作用域指的是已声明的标识符所表示的常量、类型、函数或者包在源代码中的作用范围,在此我们主要看一下go中变量的作用域,根据变量定义位置的不同,可以分为一下三个类型:
1.函数内定义的变量为局部变量,这种局部变量的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。这种变量在存在于函数被调用时,销毁于函数调用结束后。
2.函数外定义的变量为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,甚至可以使用import引入外部包来使用。全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
3.函数定义中的变量成为形式参数,定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。形式参数会作为函数的局部变量来使用。
和基础数据类型一样,在使用指针变量之前我们首先需要声明指针,基本格式为:var var_name *var-type .其中var_name是指针的名字,var-type 指的是指针的类型。下面我们来演示一下这个
如何来定义这个指针变量
func main() {
var ip * int
var fp * float32
}
上面分别定义了两个指针一个指向了整型一个指向了浮点型。指针的初始化就是取出相应变量的地址并对其进行赋值,具体步骤如下:
func main() {
a := 10
var ptr *int = &a
fmt.Println(*ptr)
}
当一个指针被定义后没有分配任何变量时,它的值为nil。也就是我们经常说的空指针,其概念和其它语言的null,NULL,nullptr是一样的。都是指这个零值或者空值
go语言数组的声明需要指明元素的类型和元素的个数,这一点和c/c++是一样的,其基本格式如下:
var variable_name [SIZE] variable_type.这样我们就可以定义一个一维数组,下面我们写一段代码来举例:
func main() {
var arr[10]int//定义一个数组
}
在go语言当中数组的初始化方式还是很多的,下面我们一个一个的来看看如何进行初始化:
1.直接初始化
func main() {
var arr =[5]int{1,2,3,4,5}
fmt.Println(arr)
}
2.直接通过字面量在声明数组的同时快速初始化数组:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr)
}
3.数组的长度不确定但是编译器可以通过元素的个数推断出数组的长度,在[]当中使用’…'就可以了,下面我们举一个列子
func main() {
arr := [...]int{1, 2, 3, 4, 5}
fmt.Println(arr)
}
4.数组的长度是确定的但是我们可以指定下标进行初始化.
func main() {
arr := [5]int{1: 2, 4: 23}
//注意其它位置编译器会初始化默认的零值
fmt.Println(arr)
}
注意:
初始化数组当中{}里面元素的个数不能超过数组的长度。如果我们不指定数组的大小Go语言会根据元素的个数来设置数组的大小。
下面我们看看go语言当中的数组名的含义。我们在c语言当中学习到了数组名表示的是首元素的地址。而在go语言当中数组名仅仅表示整个数组。
所以在go语言当中一个数组变量被赋值或者传递时会复制整个数组过去,如果数组过大那么这个开销还是特别大的。所以了在这种情况下往往需要传递一个数组指针过去。在c语言当中我们也学习过数组指针,下面我们来看看这个数组指针如何来定义
func main() {
arr := [5]int{1: 2, 4: 23}
//注意其它位置编译器会初始化默认的零值
var ptr =&arr//ptr此时是一个是数组指针
}
数组指针了除了在作为参数传递时能够减少拷贝,还能利用其和for range来变量数组,下面就以上面这个数组为例来遍历数组:
func main() {
arr := [5]int{1: 2, 4: 23}
//注意其它位置编译器会初始化默认的零值
var ptr = &arr //ptr此时是一个是数组指针
for i, v := range ptr {
fmt.Printf("%d :%d\n", i, v)
}
}
具体关于go语言的循环语句我们在后面在做讨论
在c语言里面我们也学过这个二维数组,下面我们一起来看看这个go语言里面的二维数组如何定义并且初始化的。
func main() {
var arr = [5][5]int{}
fmt.Println(arr)
}
上面我们定义了一个5行5列的二维数组,当时我们没有指定这个每个元素的初始值所以了系统会默认初始化为零值。下面我们在来看看两种指定初始值的
func main() {
var arr1= [5][5]int{{1, 2, 3,4,5}, {1, 4, 6,7,8}}//只初始化前两行
var arr2=[...][4]int{{1},{2,3,5}}//编译器自动根据行数进行推导
fmt.Println(arr1)
fmt.Println(arr2)
}
下面我们来演示一段小的demo,各位老铁可以看看这个程序输出的是什么
package main
import "fmt"
func Update1(arr [5]int) {
arr[0] = 100
}
func Update2(arr *[5]int) {
arr[0] = 101
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
Update1(arr)
fmt.Printf("Update1:%d\n", arr[0])
Update2(&arr)
fmt.Printf("Update2:%d\n", arr[0])
}
各位老铁可以看看这段代码输出的是什么?这段代码输出的是这个 1和101这是为什么呢?前面我们说过go语言中是这个值传递Update1是传值所以了这个会拷贝一份给它,函数当中arr发生改变不会影响外面的。方式二:是通过传递地址所以了函数内发生了改变会影响到外部的arr
通过上述数组的学习,我们就可以直接定义多个同类型的变量,但这往往也是一种限制,只能存储同一种类型的数据,而我们在结构体中就可以定义多个不同的数据类型。下面我们来看看go语言当中的结构体如何进行声明
type user struct {
id int
score float32
enrollment time.Time
name, addr string //多个字段类型相同时可以简写到一行里
}
有了这个结构体类型之后,我们可以使用这个结构体类型来定义这个结构体变量,下面我们来看看如何定义这个结构体变量
package main
import (
"fmt"
"time"
)
type user struct {
id int
score float32
enrollment time.Time
name, addr string //多个字段类型相同时可以简写到一行里
}
func main() {
var u user //声明,会用相应类型的默认值初始化struct里的每一个字段
u = user{} //用相应类型的默认值初始化struct里的每一个字段
u = user{id: 3, name: "zcy"} //赋值初始化
u = user{4, 100.0, time.Now(), "zcy", "beijing"}
//赋值初始化,可以不写字段名,但需要跟结构体定义里的字段顺序一致
u.enrollment = time.Now() //给结构体的成员变量赋值
}
如果我们需要访问结构体我们需要使用点号来访问其个数和c语言当中结构体的访问是一样的。下面我们来看看如何这个代码即可
package main
import (
"fmt"
"time"
)
type user struct {
id int
score float32
enrollment time.Time
name, addr string //多个字段类型相同时可以简写到一行里
}
func main() {
var u user //声明,会用相应类型的默认值初始化struct里的每一个字段
u = user{} //用相应类型的默认值初始化struct里的每一个字段
u = user{id: 3, name: "zcy"} //赋值初始化
u = user{4, 100.0, time.Now(), "zcy", "beijing"}
//赋值初始化,可以不写字段名,但需要跟结构体定义里的字段顺序一致
u.enrollment = time.Now() //给结构体的成员变量赋值
fmt.Printf("id=%d, enrollment=%v, name=%s\n", u.id, u.enrollment, u.name)
//访问结构体的成员变量
}
关于结构体指针的定义和申明同样可以套用前文中讲到的指针的相关定义,从而使用一个指针变量存放一个结构体变量的地址。
定义一个结构体变量的语法:var struct_pointer *Books。
这种指针变量的初始化和上文指针部分的初始化方式相同struct_pointer = &Book1,但是和c语言中有所不同,使用结构体指针访问结构体成员仍然使用.操作符。格式如下:struct_pointer.title由于比较的简单所以了在这里就不做过多的演示
在这里需要特别注意的是在go语言当中函数和方法不太一样,有明确的概念区分函数不属于任何结构体类型也就是说函数没有接收者。而方法是属于某个结构体的
package main
import (
"fmt"
"time"
)
type user struct {
id int
score float32
enrollment time.Time
name, addr string //多个字段类型相同时可以简写到一行里
}
//可以把user理解为hello函数的参数,即hello(u user, man string)
func (u user) hello(man string) {
fmt.Println("hi " + man + ", my name is " + u.name)
}
//函数里不需要访问user的成员,可以传匿名,甚至_也不传
func (_ user) think(man string) {
fmt.Println("hi " + man + ", do you know my name?")
}
func main() {
var u user //声明,会用相应类型的默认值初始化struct里的每一个字段
u = user{} //用相应类型的默认值初始化struct里的每一个字段
u = user{id: 3, name: "zcy"} //赋值初始化
u = user{4, 100.0, time.Now(), "zcy", "beijing"}
//赋值初始化,可以不写字段名,但需要跟结构体定义里的字段顺序一致
u.enrollment = time.Now() //给结构体的成员变量赋值
fmt.Printf("id=%d, enrollment=%v, name=%s\n", u.id, u.enrollment, u.name)
//访问结构体的成员变量
}
匿名结构体了从字面上来理解就是这个结构体没有这个名字,这也就意味着它只能使用一次
package main
var stu struct { //声明stu是一个结构体,但这个结构体是匿名的
Name string
Addr string
}
func main(){
//注意匿名结构体只能使用一次
stu.Name = "zcy"
stu.Addr = "bj"
}
当然结构体当中也可以有这个匿名字段,我们在访问时可以直接使用这个类型进行访问和操作
在go语言当中字符串是一个不可改变的字节序列,字符串的底层其实就是byte数组,字符串和数组不同字符串里面的元素是不能修改的,是一个只读的字节数组。每个字符串的长度虽然固定但是字符串的长度并不是字符串类型的一部分。
o语言字符串的底层结构在reflect.StringHeader中定义,具体如下:
type StringHeader struct {
Data uintptr
Len int
}
也就是说字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制,所以我们也可以将字符串数组看作一个结构体数组。
字符串和数组类似,内置的len函数返回字符串的长度。
下面我们来看一下这个字符串是如何使用的
package main
import "fmt"
func main() {
var str string = "ksy jia you"
for i := 0; i < len(str); i++ {
fmt.Printf("%c", str[i])
}
}
至于go语言的循环在后面细细的说明
在c语言当中我们经常会遇到一些这个转义字符比如\n和"这些字符通常有这个特殊的含义。如果需要表示其原来的含义我们需要使用’'来进行转义。而在go语言当中我们可以这样做也可以使用这个
``将要包含的字符串,那么这个字符串就会按照原有的样子呈现出来不会进行任何转义。
func main() {
var str string = `"hello \n ddddd"`
fmt.Println(str)
}
在go语言当中字符串可以将字符串强制类型转换为[]byte字节序列和[]rune序列。每个强制类型转换都有可能存在这个隐式类型转换。字符串和[]rune的转换要更为特殊一些,因为一般这种强制类型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应的[]byte和[]int32类型是完全不同的内部布局,因此这种转换可能隐含重新分配内存的操作
func main() {
var arr []byte = []byte("abc")
fmt.Println(arr)
}
在这里只介绍go语言当中常见的几种字符串拼接的方法。其它的老铁可以自行百度。
1.通过go语言原生的拼接方式+号
func main() {
var str string
str += "abc"
fmt.Println(str)
}
这种方式使用起来是最简单的,很多语言都支持这种方式。在使用+操作符进行拼接时会遍历两个字符串并开辟一块新的空间存储这两个字符串。有大量的字符串时这种效率是非常的低的
2.通过字符串格式化函数进行拼接fmt.Sprintf
在go语言当中可以使用fmt.Sprintf进行字符串格式化所以可以使用这种方式进行拼接
func main() {
str1 := "abc"
str2 := "bcd"
str3 := fmt.Sprintf("%s%s", str1, str2)
fmt.Println(str3)
}
Sprintf的原理我们在后面的文章当中会涉及在这里就不谈了,其效率也不高
3.通过strings.Builder
在go语言当中提供了一个专门操作字符串的库叫做strings,使用string.Builder方法可以进行字符串的拼接,可以通过里面的writeString方法进行字符串的拼接,使用方式如下:
func main() {
sb := strings.Builder{}
str1 := "kys"
str2 := "liu yuan zhi"
sb.WriteString(str1)
sb.WriteString(str2)
fmt.Println(sb.String())
}
其实了strings.Builder的实现原理非常的简单,下面我们来看看整个定义
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}
addr字段主要是做copycheck,buf字段是一个byte类型的切片,这个就是用来存放字符串内容的,提供的writeString()方法就是向切片buf中追加数据:其提供的String方法无非就是将字节数组强制类型转化为字符串。
4.通过 strings.Join进行拼接
func main() {
str := []string{"ksy", "liu yuanzhi"}
s := strings.Join(str, "") //第二个参数表示的是拼接的字符串直接使用什么进行分隔
fmt.Println(s)
}
最后总结一下:
使用strings.builder进行字符串拼接都是高效的,不过要主要使用方法,记得调用grow进行容量分配,才会高效。strings.join的性能约等于strings.builder,在已经字符串slice的时候可以使用,未知时不建议使用,构造切片也是有性能损耗的;如果进行少量的字符串拼接时,直接使用+操作符是最方便也是性能最高的,可以放弃strings.builder的使用。不能一概而论,具体情况具体分析。
slice和数组一样内置len函数用来求这个数组的长度,同样的还内置这个cap函数。但是slice和数组不同的是slice的cap一定是大于等于len的不像数组一样len总是等于cap的,下面我们来看看slice在底层的结构到底咱啥样.
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片是一个结构体,包含三个成员变量,array指向一块连续的内存空间,cap表示这块内存的大小,len表示目前该内存里存储了多少元素。
当我们想定义声明一个切片时可以如下:在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息·(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型
在go语言当中我们可以通过append函数往这个切片里面添加元素,如果空间不够了它在底层会自动的扩容。下面我们来演示一下这个往切片里面添加元素
func main() {
var arr[]int
arr=append(arr,1)//往切片里面添加1个数据
arr=append(arr,1,2,3)//往切片里面添加3个数据
fmt.Println(arr)
}
1.往尾部添加元素
通过指定起止下标,可以从大切片中截取一个子切片。 下面我们来演示一下什么是子切片
func main() {
arr1 := []int{1, 2, 3, 4, 5}
arr2 := arr1[1 : len(arr1)]//arr2就是一个子切片
fmt.Println(arr2)
}
刚开始,子切片和母切片共享底层的内存空间,修改子切片会反映到母切片上,在子切片上执行append会把新元素放到母切片预留的内存空间上。当子切片不断执行append,耗完了母切片预留的内存空间,子切片跟母切片就会发生内存分离,此后两个切片没有任何关系。
下面我们就来举个列子
func main() {
arr1 := []int{1, 2, 3, 4, 5}
arr2 := arr1[1:len(arr1)] //arr2就是一个子切片
fmt.Printf("arr1:%p arr2:%p\n", &arr1[1], &arr2[0])
fmt.Println("##########################")
arr2 = append(arr2, 1)
fmt.Printf("arr1:%p arr2:%p", &arr1[1], &arr2[0])
}
2.在切片开头位置添加元素或者切片
func main() {
arr1 := []int{1, 2, 2, 3}
arr2 := append([]int{0}, arr1...)
//在头部添加一个元素
fmt.Println(arr2)
//在开头添加一个切片
arr2 = append([]int{11, 12}, arr2...)
fmt.Println(arr2)
}
注意:注意:在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
3.删除尾部元素和中间元素
首先我们来看看删除这个尾部元素
a = []int{1, 2, 3, ...}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
删除中间元素
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1], ...)
a = append(a[:i], a[i+N:], ...)
比如我们需要删除第i个元素
map表的底层原理是哈希表,其结构体定义如下:
type Map struct {
Key *Type // Key type
Elem *Type // Val (elem) type
Bucket *Type // 哈希桶
Hmap *Type // 底层使用的哈希表元信息
Hiter *Type // 用于遍历哈希表的迭代器
}
其中的Hmap 的具体化数据结构如下:
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // map目前的元素数目
flags uint8 // map状态(正在被遍历/正在被写入)
B uint8 // 哈希桶数目以2为底的对数(哈希桶的数目都是 2 的整数次幂,用位运算来计算取余运算的值, 即 N mod M = N & (M-1)))
noverflow uint16 //溢出桶的数目, 这个数值不是恒定精确的, 当其 B>=16 时为近似值
hash0 uint32 // 随机哈希种子
buckets unsafe.Pointer // 指向当前哈希桶的指针
oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针
nevacuate uintptr // 桶进行调整时指示的搬迁进度
extra *mapextra // 表征溢出桶的变量
}
map是无序的(原因为无序写入以及扩容导致的元素顺序发生变化),每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
map的长度是不固定的,也就是和slice一样,也是一种引用类型
内置的len函数同样适用于map,返回map拥有的key的数量
map的key可以是所有可比较的类型,如布尔型、整数型、浮点型、复杂型、字符串型……也可以键。下面我们看看如何使用map
var m map[string]int //声明map,指定key和value的数据类型
m = make(map[string]int) //初始化,容量为0
m = make(map[string]int, 200) //初始化,容量为5。强烈建议初始化时给一个合适的容量,减少扩容的概率
m = map[string]int{"语文": 0, "数学": 39} //初始化时直接赋值
m["英语"] = 59 //往map里添加key-value对
m ["英语"] = 70 //会覆盖之前的值
delete(m, "数学") //从map里删除key-value对
1.根据key值计算出哈希值
2.取哈希值低位和hmap.B取模确定bucket位置
3.查找该key是否已经存在,如果存在则直接更新值
4.如果没有找到key,则将这一对key-value插入
2.判断某个key值是否存在
if value, exists := m["语文"]; exists {
fmt.Println(value)
} else {
fmt.Println("map里不存在[语文]这个key")
}
注意如果我们直接使用value去接收那么即使key不存在它也会返回value默认的零值
for key, value := range m {
fmt.Printf("%s=%d\n", key, value)
}
fmt.Println("-----------")
//多次遍历map返回的顺序是不一样的,但相对顺序是一样的,因为每次随机选择一个开始位置,然后顺序遍历
for key, value := range m {
fmt.Printf("%s=%d\n", key, value)
}
fmt.Println("-----------")
//一边遍历一边修改
for key, value := range m {
m[key] = value + 1
}
for key, value := range m {
fmt.Printf("%s=%d\n", key, value)
}
fmt.Println("-----------")
//for range取得的是值拷贝
for _, value := range m {
value = value + 1
}
for key, value := range m {
fmt.Printf("%s=%d\n", key, value)
}
map中的key可以是任意能够用==操作符比较的类型,不能是函数、map、切片,以及包含上述3中类型成员变量的的struct。map的value可以是任意类型。
go语言里面的channel比较的复杂在这里我们只是简单的了解一下在后面在深入研究
channel(管道)底层是一个环形队列(先进先出),send(插入)和recv(取走)从同一个位置沿同一个方向顺序执行。sendx表示最后一次插入元素的位置,recvx表示最后一次取走元素的位置。
var ch chan int //管道的声明
ch = make(chan int, 8) //管道的初始化,环形队列里可容纳8个int
ch <- 1 //往管道里写入(send)数据
ch <- 2
ch <- 3
ch <- 4
ch <- 5
v := <-ch //从管道里取走(recv)数据
fmt.Println(v)
v = <-ch
fmt.Println(v)
read_only := make (<-chan int) //定义只读的channel
write_only := make (chan<- int) //定义只写的channel
定义只读和只写的channel意义不大,一般用于在参数传递中。
//只能向channel里写数据
func send(c chan<- int) {
c <- 1
}
//只能取channel中的数据
func recv(c <-chan int) {
_ = <-c
}
//返回一个只读channel
func (c *Context) Done() <-chan struct{} {
return nil
}
可以通过for range的方式遍历管道,遍历前必须先关闭管道,禁止再写入元素。
close(ch) //遍历前必须先关闭管道,禁止再写入元素
//遍历管道里剩下的元素
for ele := range ch {
fmt.Println(ele)
}
总结:
slice、map和channel是go语言里的3种引用类型,都可以通过make函数来进行初始化(申请内存分配)。因为它们都包含一个指向底层数据结构的指针,所以称之为“引用”类型。引用类型未初始化时都是nil,可以对它们执行len()函数,返回0。
go中时使用for实现循环的,共有三种形式:
除此以外,for循环还可以直接使用range对slice、map、数组以及字符串等进行迭代循环,格式如下:
for key, value := range oldmap {
newmap[key] = value
}
break主要用于循环语句跳出循环,和c语言中的使用方式是相同的。且在多重循环的时候还可以使用label标出想要break的循环。
实例代码如下:
func main() {
for i:=0;i<10;i++{
if i==6{
break
}
}
}
continue和break的用法非常的类似在这里就不演示了,下面我们来看看这个goto.
goto语句主要是无条件转移到过程中指定的行。goto语句通常和条件语句配合使用,可用来实现条件转移、构成循环以及跳出循环体等功能。但是并不主张使用goto语句,以免造成程序流程混乱。
示例代码如下:
var a int = 0
LOOP: for a<5 {
if a == 2 {
a = a+1
goto LOOP
}
fmt.Printf("%d\n", a)
a++
}
以上代码中的LOOP就是一个标签,当运行到goto语句的时候,此时执行流就会跳转到LOOP标志的哪一行上。