《一周学会go语言并应用》 by王奇疏
( 欢迎加入go语言群: 218160862 , 群内有实践)
零、安装go语言,配置环境及IDE
这部分内容不多,请参考我的这篇安装环境《安装go语言,配置环境及IDE》
日常只有2条命令:
go run 文件路径/xxx.go 运行go程序
go build 文件路径/xxx.go 编译为二进制文件或exe文件
如果你不想每次都敲这些命令,附送1个《一键编译go文件命令.bat》 只能windows下使用,( 一般情况下,ide也是用同样的原理,ide的运行/编译也是利用这2个命令集成到ide里面 )
go build 命令生成的exe文件(或二进制文件)还是很小的,5M左右。 比c++ c#等要大一些。 这边是Go的1个ide,叫做liteIde,是中国人写的,他还写了1个goqt 界面类库, 很不错。
第一节、语法
1)注释: // 单行注释 /* 多行注释 *//
2). 【基本语法】
一个go语言程序项目只能有1个 package main包、1个main函数, 像C语言一样。用 import 关键字导入类包。
go语言变量和函数名的首字母大写,表示公开的意思,相当于类里面的public属性修饰。小写则不公开,别的package包不能引用。
例子:
package main
import "fmt" // fmt是标准包,提供基本的打印函数 Println等,类似于C语言里面的 #include <stdio.h>
func main( ){ // 大括号不能换行
fmt.Println( "hello world~! go程序,你好。" )
}
go语言代码不需要以分号结束。类包导入后 和 变量定义后必须使用,不然编译器报错。(可以通过下划线符号来回避_)
以下开始,为了省略代码,这些: package mian main(){ } 就不写了。
关于fmt标准包的函数列表,请参考: 《go语言手册.chm》 builtin 这个内置包,里面有详细说明。
标准输入、输出函数有:
fmt.Printf( ) 这个函数按照C语言的printf() 里面的参数, 打印变量。
fmt.Println( ) 这个函数可以 直接打印 【任何变量】,非常好用于调试显示信息。
func Scanf( format string, a ...interface{}) (n int, err error)
Scanf从标准输入扫描文本,根据format 参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。
3). 操作运算符
支持常见的运算符: + - * / %
逻辑运算符: && || == != ! < <= >= >
二进制运算: & | ~ ^ >> << &^
go的特殊运算符: <- 表示接收传递的信号
字符串连接符是加号: "hello " + " world~! "
赋值运算符: = 和 := (这个是声明并赋值)
// go语言不支持三元运算符 ? : ,需要自己模拟三元运算符函数。!!! 模拟方法,详见第二节:常见用法
4). 变量:
go定义变量的时候,是变量名在左边, 类型在右边。
var str string = "字符串"
var num int = 10
我想省略 var 和数据类型,写得更简短一点,怎么办呢?使用类型自动推导,定义并赋值给变量:
str := "字符串"
num := 12
(最常用的就是这种。赋值符号 := , 只能在函数的局部变量中使用,不能在函数外的全局变量中使用)
可以一次定义多个变量
a, b, c, d := 1, 2, 3, 4
特殊变量下划线,在需要不使用变量的时候用到( python等语言中也有这个特殊的弃用变量 )
i := 10
_ = i
5). 常量:
const a int = 3
const (
b int = 3
c int = 99
)
// 常量中的枚举. 当使用关键字iota 定义常量组中时,返回: 从常量定义处第n个常量开始计数,自增
const (
d int = 3
e // 如果不提供类型和初始值,那么视作与上1个常量值和类型都相同
i1 = iota // 这个常量是第3个常量,故i1 == 3 。关键字 iota导致枚举 从第n个常量开始计数
i2 // 等于4
i3 // 等于5
i4 // 等于6
)
// 下面定义一组文件尺寸的单位,从kb到tb
const (
// 下列常量按照表达式进行赋值. 表达式与iota结合时,其下的常量都将按照此表达式进行计算。
kb int64 = 1 << (10 * iota) // 这条表达式的值是:1024 , 此时 iota == 0 ,故表示2的10次方,
mb // 此时iota = 1 , 并与表达式结合, 【故这条表达式表示: 2的 (10*iota) 次方】
gb // 3的 (10*iota) 次方
tb
)
6). 分支 和 循环
go语言的 if 表达式不需要括号:
if true {
fmt.Println( "条件为真" )
} else{
}
// 在if中赋值,并判断
if i := 1; i == 1 {
fmt.Println( "条件为真" )
}
// 在go语言中switch不需要break来终止,会自动跳出。 只有使用 fallthrough 关键字的时候才会继续往下走。
switch i {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3:
fmt.Printf("3")
default:
fmt.Printf("Default")
}
【循环遍历】
//一般的for循环
for i := 0; i < 10 ; i++ {
fmt.Println( i )
}
// 1维纯数字数组
arr := [...] int { 8, 2, 3, 11, 324, 133, 1, 45, 13, 7, 14 }
for i := 0; i < len( arr ); i++ {
fmt.Println( arr[ i ] )
}
//使用for range 来循环,相当于别的语言foreach( list k=>v )
// 输出一维索引数组( php中的索引数组,其他语言中的map, json )
myInfo := map[ string ] string { "id":"1", "name":"王奇疏", "sex":"man", }
for k, v := range myInfo {
fmt.Println( "k=", k, " v=", v )
}
输出:
id : 1
name : 王奇疏
sex : man
// 输出二维索引数组( 例如数据库多行记录 或 1个json ) 【这是用原始方式实现字典,还有另1种使用struct方式实现字典】
personList := map[ int ]( map[ string ] string ) {
0: {
"name": "李雷",
"sex": "man",
"age": "18",
"birth": "1993-02-18",
},
1: {
"name": "韩梅梅",
"sex": "women",
"age": "19",
"birth": "1992-11-03",
},
2: {
"name": "王奇疏",
"sex": "man",
"age": "???",
"birth": "xxx-01-11",
},
}
for i, person := range personList {
fmt.Printf(
"%d.%s%s今年%s岁,%s\n",
i+1,
person["name"],
ifReturn(person["sex"] == "man", "先生", "女士"),
person["age"],
ifReturn(person["sex"] == "man", "他今晚决定去酒吧", "她今晚去洗头") )
}
// 模拟三元运算符。 go 不支持三元运算符,需要自己模拟简化!!! 奇葩.
func ifReturn( condition bool, trueVal, falseVal interface{} ) interface{} {
if condition {
return trueVal
}
return falseVal
}
输出
1.李雷先生今年18岁,他今晚决定去酒吧
2.韩梅梅女士今年19岁,她今晚去洗头
3.王奇疏先生今年???岁,他今晚决定去酒吧
7). 定义函数
func echo( msg string ) {
fmt.Println( msg )
}
// 函数返回值,可以返回多个值. 参数跟变量一样
func sum2( a, b int ) ( int, bool ) {
return a + b , true
}
func sum( arr []int ) int { // 返回int型的值
sum := 0
for i := 0; i < len( arr ); i++ {
sum += arr[ i ]
}
return sum
}
调用函数:
arr := [...] int { 8, 2, 3, 11, 324, 133, 1, 45, 13, 7, 14 }
total =: sum( arr )
fmt.Println( total )
// func关键字后面可以跟着括号,为类型添加方法 给该函数标明所属的类(面向对象,标明该函数所属的类),来给该类定义方法. 如果没有类的类型,则纯粹是一个函数.
// 这里的 obj myClass ,表示给myClass类添加了一个方法。这是面向对象的用法。
func ( obj myClass ) myFun ( str string ) {
}
8). 数据类型
基本类型:
类型 长度 默认值 说明
----------------------------------------------------------
bool 1 false
byte 1 0 uint8
rune 4 0 Unicode Code Point, int32
int,uint 4 8 0 32 或 64 位
int8,uint8 1 0 -128 ~ 127, 0 ~ 255
int16,uint16 2 0 -32768 ~ 32767, 0 ~ 65535
int32,uint32 4 0 -21亿 ~ 21 亿, 0 ~ 42 亿
int64,uint64 8 0
float32 4 0.0
float64 8 0.0
complex64 8
complex128 128
string "" UTF-8 字符串
array 值类型
struct 值类型
slice nil 引用类型
map nil 引用类型
channel nil 引用类型
interface nil 接口 或 表示任意类型的数据类型
function nil 函数
uintptr 4 或 8 存储指针的 uint32 或 uint64 整数
空指针值为 nil
----------------------------------------------------------
数学上支持八进制、十六进制 表示法,以及科学记数法。标准库 math 定义了各数字类型取值范围。
a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
请参考《go语言手册.chm》 builtin 这个内置包,里面有数据类型的详细说明。
需要说明的是:数据类型是严格区分的, 数据类型之间只有【互相兼容的类型】才能直接互相赋值,否则:要么通过强制类型转换来赋值,要么需要通过类包来转换。
例如 这样会报错:
var i int = 3
var j int64 = i
_ = j
报错信息: xxx.go:错误所在的行数 : cannot use i (type int) as type int64 in assignment
说明 int 和 int64 不是1个类型。 类型转换,会在下一节讲到。
第二节、常见用法( 类型转换、常用数组、json map字典、使用第三方类库如mysql )
让字符串保持原样,不处理转义问题,用1个小引号括起来像这样: str := ` 字符串'"'''''sfsdfsdf `
// go语言不支持三元运算符 ? : ,需要自己模拟三元运算符函数。!!! 奇葩.
func ifReturn( condition bool, trueVal, falseVal interface{} ) interface{} {
if condition {
return trueVal
}
return falseVal
}
1).数据类型之间的转换 ,常用的转换函数
上一节讲到: 数据类型是严格区分的, 数据类型之间只有【互相兼容的类型】才能直接互相赋值,否则:要么通过强制类型转换来赋值,要么需要通过类包来转换。
例如: int 和 int64 不是一个类型。
类型转换的方法, 强制转换:通过 类型名( ) 这种方式强制转换
var i int = 123
var j int64 = int64( i )
_ = j
【go类型转换】
(1). 数据类型是严格区分的, go必须显示调用函数来做类型转换,没有隐式转换的说法。
特别注意 if 条件中只能是bool型,不能是 if 0 之类。 可以用表达式来处理这类 if exp != 0
例如这是错误的:
if 0 == false {
fmt.Println( " 0等于flase吗?不相等。if中只能使用布尔型进行判断,不能用 0 空等其它类型的值 与 布尔型进行判断" )
}
if ! 0 {
fmt.Println( " 这也是错的" )
}
if中只能使用布尔型进行判断,不能用 0 空等其它类型的值 与 布尔型进行判断。
可以通过转换为表达式来处理
res := 1 == 1
if res {
fmt.Println( "这是正确的" )
}
(2). 只有两种互相兼容的数据类型的变量,才可以使用 类型() 这种方式来进行转换,否则需要使用类库 或者 自行处理。
如: int8和int32 是兼容的,可以使用 int32( i ) 进行转换。 类似的还有 float32( ) 、 string( ) 、 []byte( "abc" )
(3). 其它不兼容的数据类型的转换方法,使用常用的类库进行转换: fmt.sprintf, strconv.Itoa(i) strconv.Atoi(s) , ParseInt() ParseFloat()等
还有就是 强制类型转换表达式, 如 _var.( *int32 ) 这种方式也叫做断言。
fmt.sprintf( ) 将1个变量打印转换为字符串
strconv.Itoa(i) 使用字符串转换类包strconv, Itoa()是表示数字转成字符串( integer to array )
strconv.Atoi(s) 这个是相反,表示字符串 转成 数字( array to integer )
ParseInt() ParseFloat() 解释为数字、浮点数
(4). 常用例子
1、整形到字符串:
var i int = 1
var s string
s = strconv.Itoa(i) 或者 s = FormatInt(int64(i), 10)
2、字符串到整形
var s string = "1"
var i int
i, err = strconv.Atoi(s) 或者 i, err = ParseInt(s, 10, 0)
3、字符串到float(32 / 64)
var s string = 1
var f float32
f, err = ParseFloat(s, 32)
float 64的时候将上面函数中的32转为64即可
4、整形到float或者float到整形, 直接使用float(i) 或者 int(f) 直接进行转换即可
5、 []byte数组 转为字符串: string( buf )
str := string( []byte{ 'a','b','c' } ) // byte转字符串
buf := []byte( "abc" ) // 字符串转byte
【字符串处理】:
go的字符串支持直接根据索引 index 截取字符串,例如:
str := "hello world"
fmt.Print( str[0:5] ) // 从0到第5字节, 相当于php里面的 substr( 0, 4, str )
tmp := fmt.Sprintf("%d", 22221999
fmt.Println( tmp[5:]) // 从第5字节开始, 相当于php里面的 substr( 5, ... , str )
这个跟python的切片截取是一样一样的,关于切片等下数组还会讲到。
关于go语言的字符串处理,详见go语言中文手册的2个类包: strings 和 strconv ,包括了:Trim Split Join Replace Repeat ToLower ToUpper HasPrefix( php的strpos ) Contains( 包含 ) 等函数,以及类型转换函数。建议学习时候,对这2个类包 大概鸟览一遍。
(5). 类型转换踩过的坑
(暂略)
2). 常用数组、 map字典等操作
前面第一节,语法的时候发过2个例子。
// 1维纯数字数组
arr := [...] int { 8, 2, 3, 11, 324, 133, 1, 45, 13, 7, 14 }
for i := 0; i < len( arr ); i++ {
fmt.Println( arr[ i ] )
}
// 输出一维索引数组( php中的索引数组,其他语言中的map, json )
myInfo := map[ string ] string { "id":"1", "name":"王奇疏", "sex":"man", }
// 这句声明,表示 map[ 索引类型 ] 值类型
for k, v := range myInfo {
fmt.Println( "k=", k, " v=", v )
}
输出:
id : 1
name : 王奇疏
sex : man
数组、切片、map这几种结构篇幅占用较多,只需要简单掌握 初始化定义和 几个函数即可:len(list), append( ) , cap( ) .
请参考我的这篇文章学习,很快能使用:《go语言的 数组、slice、map使用》
3). 安装第3方类包/类库,如mysql类库。
(1).类包管理
可以给包起别名,或者在包名前面加1个点号.表示省略包名
package main
import(
"fmt" f
."strconv" // 我们下面调用的 Itoa() ,其实应该是 strconv.Itoa( 123 )才对。 但是这里使用了.号进行省略包名,所以下面就简单了。(除非包名很长,一般不建议省略)
)
func main( ){
fmt.println( Itoa( 123 ) )
}
(2). 安装第3方类包/类库
首先你要找到第三方类包所在的网址,
例如:
http://github.com/go-sql-driver/mysql
分为自动安装 和 手动下载 然后安装。
很简单,主要是执行2条命令: go get 网址 和 go install 网址 。
详细步骤和原理只需要10几行文字说明,请参考 我的这篇文章:《go get安装第三方包的前提条件和步骤》(对于github来说,需要先安装 git 工具)
类库一旦安装之后,就可以在代码中 import xxx类库来使用了。
三、常用类库
1.时间处理、定时器、随机数
格式化输出当前的时间点:
fmt.Println( time.Now().Format("2006-01-02 15:04:05") ) // 这是个奇葩,传入的值必须是这个时间点, 据说是go诞生之日, 记忆方法:6-1-2-3-4-5
会输出当前时间: 2015-12-31 19:32:20 , 你只要在Format( )参数里面输入你想要的格式就行。
更多时间处理的内容,参考go手册: time 包
golang 定时器: 启动的时候执行一次,以后每天晚上12点执行。 # stevewang • 2015-03-04 17:17
func startTimer( callBack func() ) {
go func() {
for {
// 执行回调函数. 启动的时候执行一次
callBack()
now := time.Now()
// 计算下一个零点
next := now.Add( time.Hour * 24 )
next = time.Date( next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location() )
t := time.NewTimer( next.Sub(now) )
// 传递信号给线程
<- t.C
}
} ()
}
随机数:
从下面的例子可以看见,go语言生成随机数需要2行代码: // 播下种子(这里用时间戳作为种子), 调用rand.Intn( 最大数字 ) 生成随机数
rand.Seed(time.Now().Unix())
num := rand.Intn( 100) fmt.Println( num )
例子:
package main
import(
"fmt"
"time"
"math/rand" // 随机数所需要使用的包 (go语言的类包不到30个,多翻翻go手册就熟悉了)
)
func main(){
list := [...][]int{} // 用于存放数据的数组
max := 200 // 随机数的最大值 0~200
// 生成20组(条) 二维数据
for i := 0 ; i < 20; i++ {
// 播下种子, 生成随机数
rand.Seed(time.Now().Unix())
n := rand.Intn( max )
// 每条数据 生成随机N个数字
tmp := []int{}
for j := 0 ; j < n; j++ {
rand.Seed( time.Now().Unix() - int64( j ) )
tmp = append( tmp, rand.Intn( max ) )
}
list[i] = tmp
}
fmt.Println( "list=", list )
}
2.文件读写
// 文件读写 只需要简单掌握几个函数即可:
fp, e := os.Stat(file) // 判断文件是否存在
// 建立文件函数:
func Create(name string) (file *File, err Error)
func NewFile(fd int, name string) *File
// 打开文件函数:
func Open(name string) (file *File, err Error)
func OpenFile(name string, flag int, perm uint32) (file *File, err Error)
// 这行使用追加模式写入文件,如果文件不存在则创建
fp, err := os.OpenFile( LOGFILE, os.O_CREATE|os.O_APPEND|os.O_RDWR,0660 )
fp.WriteString( " 写入内容: " )
更详细更实用的例子,请参考这篇文章《go语言文件操作》以及《go语言手册》
3.网络
主要有参考手册中的 net包,tcp、udp、http等,请参考《go语言编程》
创建http服务器很简单
func httpServer(){
//设置访问的路由, 使用回调函数处理
http.HandleFunc("/", sayhelloName)
// 创建http服务器
fp := http.ListenAndServe(":9007",nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
_ = fp
}
// httpServer的回调处理函数
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //解析参数, 默认是不会解析的
fmt.Println(r.Form) //这些是服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello astaxie!") //输出到客户端的信息
}
4. 数据库
通过安装 mysql等数据库接口,进行操作。参考《go get安装第三方包的前提条件和步骤》, 然后安装这个包 http://github.com/go-sql-driver/mysql
5.ui界面
目前go有一些ui界面库,主要有 goqt(QT跨平台界面库)、 walk (只能在win平台使用)
特点: goqt 由于qt本身的体积比较大,带上qt的dll就有15M了,整个软件打包成exe后 一般超过20M。 优点是跨平台、通用,适合大型软件;
walk 体积很小,一般界面只有5M,但只有win平台能使用。
两个库目前都不是很完善。
需要说的是, golang的IDE之一 liteIde就是用 goqt写的,liteIde的作者就是 goqt的作者,而且是中国人,感觉非常好。
这是用walk编写的一个桌面软件。
6. go语言调用windows或者调用C语言。
1. go语言让windows发出声音,或者播放音乐的例子:会发出alert警告的声音
package main
func main(){
winSound()
}
// golang 让windows发出警告的声音 todo 需要完善播放mp3之类
func winSound( ) {
funInDllFile, err := syscall.LoadLibrary("Winmm.dll") // 调用的dll文件
if err != nil {
print("cant not call : syscall.LoadLibrary , errorInfo :" + err.Error())
}
defer syscall.FreeLibrary(funInDllFile)
// 调用的dll里面的函数是:
funName := "PlaySound"
// 注册一长串调用代码,简化为 _win32Fun 变量.
win32Fun, err := syscall.GetProcAddress(syscall.Handle(funInDllFile), funName)
// 通过syscall.Syscall6()去调用win32的xxx函数,因为xxx函数有3个参数,故需取Syscall6才能放得下. 最后的3个参数,设置为0即可
_, _, err = syscall.Syscall6(
uintptr(win32Fun), // 调用的函数名
3, // 指明该函数的参数数量
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("alert") ) ), // 该函数的参数1. 可通过msdn查找函数名 查参数含义
// SystemStart
uintptr( 0 ), // 该函数的参数2.
uintptr( 0 ), // 该函数的参数3.
0,
0,
0 )
}
2. go语言调用C语言
(暂略)
四、机制
goroutine 协程。
channel 使用消息传递机制 来同步和协调线程之间的操作顺序。
select 不阻塞处理。
请参考《go语言编程》
// 异常机制: defer的作用是, 在程序退出时(或函数返回时) 执行的表达式的时候出错,就报错。
func writeFile( fileName string ){
fp, err := os.OpenFile( LOGFILE, os.O_CREATE|os.O_APPEND|os.O_RDWR,0660 )
defer fp.Close()
}
// go语言实现回调函数、go语言的匿名函数 和 闭包:
func call ( callBack func() ) {
// 执行回调函数.
callBack()
}
func echo ( ){
fmt.Println( "123" )
}
call( echo )
五、go语言的 struct结构体和类,面向对象和 interface{}
1). go语言使用 struct结构体来定义1个类
// 人类对象: 拥有 id、姓名、性别、国籍等属性
type person struct {
// 人的id、姓名、性别、生日、国籍、 居住地、祖籍
id int32
name string
sex int8
birthDate string
country string
}
// 实例化类/结构体的方法一般有4种: p表示person简写
p1 := new( person )
p1.id = 1
p1.name = "王奇疏"
p1.sex = 1
p2 := &person { }
p3 := &person { id:1, name:"王奇疏", sex:1 }
p4 := &person { 1, "王奇疏", 1 }
这4种都是实例化1个对象,基本可以通用。
2). 面向对象一般有2种封装方式:继承和组合,go语言全部使用 组合 的方式来实现面向对象。组合的好处是解耦方便。
下面是给 person类 添加几个方法: 吃饭、走路、说话、笑 的方法
func ( p person ) eat( food string ){
fmt.Println( p.name, " 吃了 ", food )
}
func ( p person ) walk( ){
fmt.Println( p.name, " 走了一段路 " )
}
func ( p person ) say( msg string ){
fmt.Println( p.name, " 说: " , msg )
}
func ( p person ) laugh( ){
fmt.Println( " LOL ^_^ " , p.name )
}
调用例子:
p4 := &person { id:1, name:"王奇疏", sex:1 }
p4.eat( "一鲸(斤)黑鱼" )
3). go语言的继承 也是使用组合的方式来实现。
接上, 新建1个student类继承自 person 。
type student struct{
person // go使用组合的方式,将person类当做student的属性,从这种方式继承了person类的属性和方法。
school string
}
// 给学生类添加 上课的方法
func ( s student ) goToSchool( ){
fmt.Println( p.name, " 在 ", p.school, " 上学" )
}
// s表示 student 简写
s1 := &student { school:"xxx科技大学" } // 注意go没有构造函数, 这里初始化的时候 还没有继承person类,先初始化之后才继承person类。才能使用继承的属性和方法
s1.id = 123
s1.name = "王奇疏"
s1.sex = 1
s1.eat( "一鲸(斤)米饭" )
s1.goToSchool()
输出:
王奇疏 吃了 一鲸(斤)米饭
王奇疏 在 xxx科技大学 上学
4). go语言的接口 interface 契约编程, 是使用 隐式接口 的方式实现 接口。
当1个类 只要实现了某个接口的所有方法, 那么这个类不用任何显示声明,即表示这个类已经实现了这个接口。 这种隐式实现接口,也导致了代码不方便管理的一些问题/比如移动代码之后 也许就出问题了。
// 定义1个人类接口
type Iperson interface {
func eat( food string )
func walk( )
func say( msg string )
func laugh( )
}
由于上述 person类已经编写了 这4个方法,所以person类就已经自动实现了 Iperson这个接口。
person类不需要像别的语言一样显式声明 implement Iperson,就已经自动实现了该接口。
5). 设计模式。 (暂略)
六、引用和指针
(暂略)
参考书籍《go语言编程》( 许式伟 )
七、多平台交叉编译( windos、linux、arm安卓苹果 、 mips路由器 )
(暂略)
gdb调试: 可以用gdb对 go编译的二进制文件进行调试,请参考gdb文档。
八、常见错误
(暂略)
有疑问加站长微信联系(非本文作者)