Go 从入门到实战学习
一、语法
1、简单示例
package main
import "fmt"
func main()
{ // 错误,{ 不能在单独的行上
fmt.Println("Hello, World!")
}
在 Go 语言中,一个可执行程序只能有一个 main() 函数,并且它必须位于 package main 中。
具体来说:
- Go 程序是按 包(package) 来组织的。
- 如果你要编译成可执行文件(而不是库),必须有一个
package main,并且在该包中定义一个func main()作为程序入口。 - 在同一个包里,不允许有重复的函数名,因此在同一个
package main中不能出现多个main()方法。 - 如果你的项目有多个
.go文件,并且这些文件都属于同一个package main,那么只能有一个文件中定义func main()。 - 你可以在不同的包中定义其他名字为
main的函数,但它们不会作为程序入口被调用。例如:package foo func main() { // 这是 foo 包里的 main,不会被当作入口 } - 如果你真的需要多个可执行入口,可以创建多个 不同的
package main,分别放在不同的目录,每个都有自己的main()函数,然后分别编译运行。
? 举个例子:
project/
├── cmd1/
│ └── main.go // package main, 有 func main()
├── cmd2/
│ └── main.go // package main, 有 func main()
└── lib/
└── util.go // package lib, 工具函数
这样你就能编译出两个独立的可执行文件:
go build ./cmd1
go build ./cmd2
Go 中 map 和 struct 对比示例
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
实例:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
访问结构体成员
如果要访问结构体成员,需要使用点号 . 操作符,格式为:
结构体.成员名"
示例:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)
/* 打印 Book2 信息 */
fmt.Printf( "Book 2 title : %s\n", Book2.title)
fmt.Printf( "Book 2 author : %s\n", Book2.author)
fmt.Printf( "Book 2 subject : %s\n", Book2.subject)
fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)
}
结构体和MAP 对比:
// struct 固定字段
type User struct {
Name string
Age int
}
u := User{Name: "Bob", Age: 30}
// u.Email = "xxx" // ❌ 编译错误,因为没有 Email 字段
// map 动态键值对
m := make(map[string]interface{})
m["Name"] = "Bob"
m["Age"] = 30
m["Email"] = "bob@example.com" // ✅ 可以随时加新字段
实战:结构体
// TraderConfig 单个trader的配置
type TraderConfig struct {
ID string `json:"id"`
Name string `json:"name"`
AIModel string `json:"ai_model"` // "qwen" or "deepseek"
// 交易平台选择(二选一)
Exchange string `json:"exchange"` // "binance" or "hyperliquid"
// 币安配置
BinanceAPIKey string `json:"binance_api_key,omitempty"`
BinanceSecretKey string `json:"binance_secret_key,omitempty"`
// Hyperliquid配置
HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"`
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr,omitempty"`
HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"`
// Aster配置
AsterUser string `json:"aster_user,omitempty"` // Aster主钱包地址
AsterSigner string `json:"aster_signer,omitempty"` // Aster API钱包地址
AsterPrivateKey string `json:"aster_private_key,omitempty"` // Aster API钱包私钥
// AI配置
QwenKey string `json:"qwen_key,omitempty"`
DeepSeekKey string `json:"deepseek_key,omitempty"`
// 自定义AI API配置(支持任何OpenAI格式的API)
CustomAPIURL string `json:"custom_api_url,omitempty"`
CustomAPIKey string `json:"custom_api_key,omitempty"`
CustomModelName string `json:"custom_model_name,omitempty"`
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
}
好的,你问的这个部分:
BinanceAPIKey string `json:"binance_api_key,omitempty"`
后面的 反引号里的内容 在 Go 语言里叫 struct tag(结构体标签),是给结构体字段附加的元数据(metadata),主要用于告诉某些库在处理这个字段时该怎么做。
1. json:"binance_api_key,omitempty" 的作用
这是专门给 Go 的 encoding/json 包(以及其他兼容的 JSON 序列化库)用的标签,表示:
-
json:"binance_api_key"
当结构体被序列化成 JSON 时,这个字段在 JSON 中的键名是"binance_api_key",而不是 Go 变量名BinanceAPIKey。
同样,在反序列化时,JSON 中的"binance_api_key"会被映射到这个字段。 -
omitempty
如果这个字段的值是零值(empty value),在序列化成 JSON 时会被省略,不会出现在输出结果中。
对于string类型,零值就是空字符串""。- 示例
没有 omitempty
type Config struct { BinanceAPIKey string `json:"binance_api_key"` }
cfg := Config{}
data, _ := json.Marshal(cfg)
fmt.Println(string(data))
输出:
```json
{"binance_api_key":""}
即使是空字符串,也会输出。
有 omitempty
type Config struct {
BinanceAPIKey string `json:"binance_api_key,omitempty"`
}
cfg := Config{}
data, _ := json.Marshal(cfg)
fmt.Println(string(data))
输出:
{}
因为值是空字符串,所以直接省略了这个字段。
3. 为什么要用 struct tag
-
控制 JSON 字段名
- Go 的命名习惯是驼峰(CamelCase),但很多 API 或配置文件使用下划线风格(snake_case)。
- 用 tag 可以让 JSON 输出/输入匹配 API 要求。
-
省略空值
- 有些 API 不希望你传空字符串或零值。
- 使用
omitempty可以减少不必要的数据,提高传输效率。
-
兼容不同格式
- 除了 JSON,还可以用 tag 给 XML、YAML、数据库 ORM 等库提供映射规则,比如:
Name string `json:"name" xml:"name" db:"name_column"`
- 除了 JSON,还可以用 tag 给 XML、YAML、数据库 ORM 等库提供映射规则,比如:
4. 总结
在你的例子里:
BinanceAPIKey string `json:"binance_api_key,omitempty"`
含义就是:
当转换成 JSON 时,这个字段对应的 key 是
"binance_api_key";如果它是空字符串,就不要把它写进 JSON。
这在配置文件或 API 请求中很常见,因为:
- 保证 JSON 格式符合外部接口要求。
- 避免发送无意义的空参数。
Go 普通函数 和 方法(method) 的区别
// AddTrader 添加一个trader
func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, leverage config.LeverageConfig) error {
tm.mu.Lock()
defer tm.mu.Unlock()
if _, exists := tm.traders[cfg.ID]; exists {
return fmt.Errorf("trader ID '%s' 已存在", cfg.ID)
}
// 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{
ID: cfg.ID,
Name: cfg.Name,
AIModel: cfg.AIModel,
Exchange: cfg.Exchange,
BinanceAPIKey: cfg.BinanceAPIKey,
BinanceSecretKey: cfg.BinanceSecretKey,
HyperliquidPrivateKey: cfg.HyperliquidPrivateKey,
HyperliquidWalletAddr: cfg.HyperliquidWalletAddr,
HyperliquidTestnet: cfg.HyperliquidTestnet,
AsterUser: cfg.AsterUser,
AsterSigner: cfg.AsterSigner,
AsterPrivateKey: cfg.AsterPrivateKey,
CoinPoolAPIURL: coinPoolURL,
UseQwen: cfg.AIModel == "qwen",
DeepSeekKey: cfg.DeepSeekKey,
QwenKey: cfg.QwenKey,
CustomAPIURL: cfg.CustomAPIURL,
CustomAPIKey: cfg.CustomAPIKey,
CustomModelName: cfg.CustomModelName,
ScanInterval: cfg.GetScanInterval(),
InitialBalance: cfg.InitialBalance,
BTCETHLeverage: leverage.BTCETHLeverage, // 使用配置的杠杆倍数
AltcoinLeverage: leverage.AltcoinLeverage, // 使用配置的杠杆倍数
MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
}
// 创建trader实例
at, err := trader.NewAutoTrader(traderConfig)
if err != nil {
return fmt.Errorf("创建trader失败: %w", err)
}
tm.traders[cfg.ID] = at
log.Printf("✓ Trader '%s' (%s) 已添加", cfg.Name, cfg.AIModel)
return nil
}
普通函数:
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
你这个问题其实就是 Go 普通函数 和 方法(method) 的区别。
我们先对比一下你给的两个例子:
1. 普通函数
func max(num1, num2 int) int {
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
- 没有接收者(receiver)。
- 调用方式:
result := max(3, 5) - 属于包级别的函数,只和参数有关,不依赖某个特定对象的状态。
2. 方法(带接收者)
func (tm *TraderManager) AddTrader(...) error {
// ...
}
(tm *TraderManager)就是 方法接收者(receiver)。- 表示这个函数是
TraderManager类型的方法,可以访问和修改TraderManager的字段。 - 调用方式:
manager := &TraderManager{} err := manager.AddTrader(...) - 类似于其他语言里的“类的成员方法”,但 Go 没有 class,而是通过 类型 + 方法接收者 来实现面向对象风格。
* 为什么要这样写 `(tm TraderManager)`?**
因为 AddTrader 的逻辑需要操作 TraderManager 的内部数据,比如:
tm.mu.Lock()
tm.traders[cfg.ID] = at
这里的 tm 是当前 TraderManager 实例的指针,
如果不加接收者,你就没法直接访问它的字段 mu 和 traders。
接收者类型可以是值类型或指针类型
在 Go 中,方法接收者可以写成两种形式:
-
值接收者
func (t TraderManager) Foo() { ... }调用时会复制一份对象,方法里修改字段不会影响原对象。
-
指针接收者
func (t *TraderManager) Foo() { ... }调用时传入对象地址,可以在方法里修改原对象的字段,而且避免复制大结构体带来的性能开销。
在你的例子中,AddTrader要往tm.tradersmap 中添加元素,所以必须用指针接收者才能修改原数据。
总结
(tm *TraderManager)是方法的 接收者声明,让这个函数成为TraderManager类型的方法。- 有了接收者,这个方法就能访问和操作该类型实例的字段,就像面向对象语言里的成员方法一样。
- 用指针接收者是为了:
- 修改原对象状态;
- 避免结构体复制开销;
- 保持一致性(Go 官方建议,如果某类型的方法中有一个用指针接收者,其它也统一用指针)。
如果你愿意,我可以帮你画一个 普通函数 vs 方法调用流程图,让你更直观地理解 (tm *TraderManager) 在内存中的作用,你要吗?
相关文章:
菜鸟教程 | Go基础语法学习
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)