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 类型,零值就是空字符串 ""

    1. 示例

    没有 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

  1. 控制 JSON 字段名

    • Go 的命名习惯是驼峰(CamelCase),但很多 API 或配置文件使用下划线风格(snake_case)。
    • 用 tag 可以让 JSON 输出/输入匹配 API 要求。
  2. 省略空值

    • 有些 API 不希望你传空字符串或零值。
    • 使用 omitempty 可以减少不必要的数据,提高传输效率。
  3. 兼容不同格式

    • 除了 JSON,还可以用 tag 给 XML、YAML、数据库 ORM 等库提供映射规则,比如:
      Name string `json:"name" xml:"name" db:"name_column"`

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 实例的指针,
如果不加接收者,你就没法直接访问它的字段 mutraders


接收者类型可以是值类型或指针类型

在 Go 中,方法接收者可以写成两种形式:

  1. 值接收者

    func (t TraderManager) Foo() { ... }

    调用时会复制一份对象,方法里修改字段不会影响原对象。

  2. 指针接收者

    func (t *TraderManager) Foo() { ... }

    调用时传入对象地址,可以在方法里修改原对象的字段,而且避免复制大结构体带来的性能开销。
    在你的例子中,AddTrader 要往 tm.traders map 中添加元素,所以必须用指针接收者才能修改原数据。


总结

  • (tm *TraderManager) 是方法的 接收者声明,让这个函数成为 TraderManager 类型的方法。
  • 有了接收者,这个方法就能访问和操作该类型实例的字段,就像面向对象语言里的成员方法一样。
  • 用指针接收者是为了:
    • 修改原对象状态;
    • 避免结构体复制开销;
    • 保持一致性(Go 官方建议,如果某类型的方法中有一个用指针接收者,其它也统一用指针)。

如果你愿意,我可以帮你画一个 普通函数 vs 方法调用流程图,让你更直观地理解 (tm *TraderManager) 在内存中的作用,你要吗?



相关文章:
菜鸟教程 | Go基础语法学习

为者常成,行者常至