commit
3ff7cbbe17
61 changed files with 9454 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||
/etcd |
|||
/redis |
@ -0,0 +1,391 @@ |
|||
Advanced Query |
|||
Smart Select Fields |
|||
In GORM, you can efficiently select specific fields using the Select method. This is particularly useful when dealing with large models but requiring only a subset of fields, especially in API responses. |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string |
|||
Age int |
|||
Gender string |
|||
// hundreds of fields |
|||
} |
|||
|
|||
type APIUser struct { |
|||
ID uint |
|||
Name string |
|||
} |
|||
|
|||
// GORM will automatically select `id`, `name` fields when querying |
|||
db.Model(&User{}).Limit(10).Find(&APIUser{}) |
|||
// SQL: SELECT `id`, `name` FROM `users` LIMIT 10 |
|||
NOTE In QueryFields mode, all model fields are selected by their names. |
|||
|
|||
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ |
|||
QueryFields: true, |
|||
}) |
|||
|
|||
// Default behavior with QueryFields set to true |
|||
db.Find(&user) |
|||
// SQL: SELECT `users`.`name`, `users`.`age`, ... FROM `users` |
|||
|
|||
// Using Session Mode with QueryFields |
|||
db.Session(&gorm.Session{QueryFields: true}).Find(&user) |
|||
// SQL: SELECT `users`.`name`, `users`.`age`, ... FROM `users` |
|||
Locking |
|||
GORM supports different types of locks, for example: |
|||
|
|||
// Basic FOR UPDATE lock |
|||
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users) |
|||
// SQL: SELECT \* FROM `users` FOR UPDATE |
|||
The above statement will lock the selected rows for the duration of the transaction. This can be used in scenarios where you are preparing to update the rows and want to prevent other transactions from modifying them until your transaction is complete. |
|||
|
|||
The Strength can be also set to SHARE which locks the rows in a way that allows other transactions to read the locked rows but not to update or delete them. |
|||
|
|||
db.Clauses(clause.Locking{ |
|||
Strength: "SHARE", |
|||
Table: clause.Table{Name: clause.CurrentTable}, |
|||
}).Find(&users) |
|||
// SQL: SELECT \* FROM `users` FOR SHARE OF `users` |
|||
The Table option can be used to specify the table to lock. This is useful when you are joining multiple tables and want to lock only one of them. |
|||
|
|||
Options can be provided like NOWAIT which tries to acquire a lock and fails immediately with an error if the lock is not available. It prevents the transaction from waiting for other transactions to release their locks. |
|||
|
|||
db.Clauses(clause.Locking{ |
|||
Strength: "UPDATE", |
|||
Options: "NOWAIT", |
|||
}).Find(&users) |
|||
// SQL: SELECT \* FROM `users` FOR UPDATE NOWAIT |
|||
Another option can be SKIP LOCKED which skips over any rows that are already locked by other transactions. This is useful in high concurrency situations where you want to process rows that are not currently locked by other transactions. |
|||
|
|||
For more advanced locking strategies, refer to Raw SQL and SQL Builder. |
|||
|
|||
SubQuery |
|||
Subqueries are a powerful feature in SQL, allowing nested queries. GORM can generate subqueries automatically when using a \*gorm.DB object as a parameter. |
|||
|
|||
// Simple subquery |
|||
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders) |
|||
// SQL: SELECT \* FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders"); |
|||
|
|||
// Nested subquery |
|||
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users") |
|||
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results) |
|||
// SQL: SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%") |
|||
From SubQuery |
|||
GORM allows the use of subqueries in the FROM clause, enabling complex queries and data organization. |
|||
|
|||
// Using subquery in FROM clause |
|||
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{}) |
|||
// SQL: SELECT \* FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18 |
|||
|
|||
// Combining multiple subqueries in FROM clause |
|||
subQuery1 := db.Model(&User{}).Select("name") |
|||
subQuery2 := db.Model(&Pet{}).Select("name") |
|||
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{}) |
|||
// SQL: SELECT \* FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p |
|||
Group Conditions |
|||
Group Conditions in GORM provide a more readable and maintainable way to write complex SQL queries involving multiple conditions. |
|||
|
|||
// Complex SQL query using Group Conditions |
|||
db.Where( |
|||
db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")), |
|||
).Or( |
|||
db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"), |
|||
).Find(&Pizza{}) |
|||
// SQL: SELECT \* FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge") |
|||
IN with multiple columns |
|||
GORM supports the IN clause with multiple columns, allowing you to filter data based on multiple field values in a single query. |
|||
|
|||
// Using IN with multiple columns |
|||
db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users) |
|||
// SQL: SELECT \* FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user")); |
|||
Named Argument |
|||
GORM enhances the readability and maintainability of SQL queries by supporting named arguments. This feature allows for clearer and more organized query construction, especially in complex queries with multiple parameters. Named arguments can be utilized using either sql.NamedArg or map[string]interface{}{}, providing flexibility in how you structure your queries. |
|||
|
|||
// Example using sql.NamedArg for named arguments |
|||
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user) |
|||
// SQL: SELECT \* FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" |
|||
|
|||
// Example using a map for named arguments |
|||
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user) |
|||
// SQL: SELECT \* FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1 |
|||
For more examples and details, see Raw SQL and SQL Builder |
|||
|
|||
Find To Map |
|||
GORM provides flexibility in querying data by allowing results to be scanned into a map[string]interface{} or []map[string]interface{}, which can be useful for dynamic data structures. |
|||
|
|||
When using Find To Map, it’s crucial to include Model or Table in your query to explicitly specify the table name. This ensures that GORM understands which table to query against. |
|||
|
|||
// Scanning the first result into a map with Model |
|||
result := map[string]interface{}{} |
|||
db.Model(&User{}).First(&result, "id = ?", 1) |
|||
// SQL: SELECT \* FROM `users` WHERE id = 1 LIMIT 1 |
|||
|
|||
// Scanning multiple results into a slice of maps with Table |
|||
var results []map[string]interface{} |
|||
db.Table("users").Find(&results) |
|||
// SQL: SELECT \* FROM `users` |
|||
FirstOrInit |
|||
GORM’s FirstOrInit method is utilized to fetch the first record that matches given conditions, or initialize a new instance if no matching record is found. This method is compatible with both struct and map conditions and allows additional flexibility with the Attrs and Assign methods. |
|||
|
|||
// If no User with the name "non_existing" is found, initialize a new User |
|||
var user User |
|||
db.FirstOrInit(&user, User{Name: "non_existing"}) |
|||
// user -> User{Name: "non_existing"} if not found |
|||
|
|||
// Retrieving a user named "jinzhu" |
|||
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user) |
|||
// user -> User{ID: 111, Name: "Jinzhu", Age: 18} if found |
|||
|
|||
// Using a map to specify the search condition |
|||
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) |
|||
// user -> User{ID: 111, Name: "Jinzhu", Age: 18} if found |
|||
Using Attrs for Initialization |
|||
When no record is found, you can use Attrs to initialize a struct with additional attributes. These attributes are included in the new struct but are not used in the SQL query. |
|||
|
|||
// If no User is found, initialize with given conditions and additional attributes |
|||
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user) |
|||
// SQL: SELECT \* FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1; |
|||
// user -> User{Name: "non_existing", Age: 20} if not found |
|||
|
|||
// If a User named "Jinzhu" is found, `Attrs` are ignored |
|||
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user) |
|||
// SQL: SELECT \* FROM USERS WHERE name = 'Jinzhu' ORDER BY id LIMIT 1; |
|||
// user -> User{ID: 111, Name: "Jinzhu", Age: 18} if found |
|||
Using Assign for Attributes |
|||
The Assign method allows you to set attributes on the struct regardless of whether the record is found or not. These attributes are set on the struct but are not used to build the SQL query and the final data won’t be saved into the database. |
|||
|
|||
// Initialize with given conditions and Assign attributes, regardless of record existence |
|||
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user) |
|||
// user -> User{Name: "non_existing", Age: 20} if not found |
|||
|
|||
// If a User named "Jinzhu" is found, update the struct with Assign attributes |
|||
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user) |
|||
// SQL: SELECT \* FROM USERS WHERE name = 'Jinzhu' ORDER BY id LIMIT 1; |
|||
// user -> User{ID: 111, Name: "Jinzhu", Age: 20} if found |
|||
FirstOrInit, along with Attrs and Assign, provides a powerful and flexible way to ensure a record exists and is initialized or updated with specific attributes in a single step. |
|||
|
|||
FirstOrCreate |
|||
FirstOrCreate in GORM is used to fetch the first record that matches given conditions or create a new one if no matching record is found. This method is effective with both struct and map conditions. The RowsAffected property is useful to determine the number of records created or updated. |
|||
|
|||
// Create a new record if not found |
|||
result := db.FirstOrCreate(&user, User{Name: "non_existing"}) |
|||
// SQL: INSERT INTO "users" (name) VALUES ("non_existing"); |
|||
// user -> User{ID: 112, Name: "non_existing"} |
|||
// result.RowsAffected // => 1 (record created) |
|||
|
|||
// If the user is found, no new record is created |
|||
result = db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user) |
|||
// user -> User{ID: 111, Name: "jinzhu", Age: 18} |
|||
// result.RowsAffected // => 0 (no record created) |
|||
Using Attrs with FirstOrCreate |
|||
Attrs can be used to specify additional attributes for the new record if it is not found. These attributes are used for creation but not in the initial search query. |
|||
|
|||
// Create a new record with additional attributes if not found |
|||
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user) |
|||
// SQL: SELECT \* FROM users WHERE name = 'non_existing'; |
|||
// SQL: INSERT INTO "users" (name, age) VALUES ("non_existing", 20); |
|||
// user -> User{ID: 112, Name: "non_existing", Age: 20} |
|||
|
|||
// If the user is found, `Attrs` are ignored |
|||
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user) |
|||
// SQL: SELECT \* FROM users WHERE name = 'jinzhu'; |
|||
// user -> User{ID: 111, Name: "jinzhu", Age: 18} |
|||
Using Assign with FirstOrCreate |
|||
The Assign method sets attributes on the record regardless of whether it is found or not, and these attributes are saved back to the database. |
|||
|
|||
// Initialize and save new record with `Assign` attributes if not found |
|||
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) |
|||
// SQL: SELECT \* FROM users WHERE name = 'non_existing'; |
|||
// SQL: INSERT INTO "users" (name, age) VALUES ("non_existing", 20); |
|||
// user -> User{ID: 112, Name: "non_existing", Age: 20} |
|||
|
|||
// Update found record with `Assign` attributes |
|||
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user) |
|||
// SQL: SELECT \* FROM users WHERE name = 'jinzhu'; |
|||
// SQL: UPDATE users SET age=20 WHERE id = 111; |
|||
// user -> User{ID: 111, Name: "Jinzhu", Age: 20} |
|||
Optimizer/Index Hints |
|||
GORM includes support for optimizer and index hints, allowing you to influence the query optimizer’s execution plan. This can be particularly useful in optimizing query performance or when dealing with complex queries. |
|||
|
|||
Optimizer hints are directives that suggest how a database’s query optimizer should execute a query. GORM facilitates the use of optimizer hints through the gorm.io/hints package. |
|||
|
|||
import "gorm.io/hints" |
|||
|
|||
// Using an optimizer hint to set a maximum execution time |
|||
db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{}) |
|||
// SQL: SELECT _ /_+ MAX_EXECUTION_TIME(10000) \*/ FROM `users` |
|||
Index Hints |
|||
Index hints provide guidance to the database about which indexes to use. They can be beneficial if the query planner is not selecting the most efficient indexes for a query. |
|||
|
|||
import "gorm.io/hints" |
|||
|
|||
// Suggesting the use of a specific index |
|||
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{}) |
|||
// SQL: SELECT \* FROM `users` USE INDEX (`idx_user_name`) |
|||
|
|||
// Forcing the use of certain indexes for a JOIN operation |
|||
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{}) |
|||
// SQL: SELECT \* FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`) |
|||
These hints can significantly impact query performance and behavior, especially in large databases or complex data models. For more detailed information and additional examples, refer to Optimizer Hints/Index/Comment in the GORM documentation. |
|||
|
|||
Iteration |
|||
GORM supports the iteration over query results using the Rows method. This feature is particularly useful when you need to process large datasets or perform operations on each record individually. |
|||
|
|||
You can iterate through rows returned by a query, scanning each row into a struct. This method provides granular control over how each record is handled. |
|||
|
|||
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows() |
|||
defer rows.Close() |
|||
|
|||
for rows.Next() { |
|||
var user User |
|||
// ScanRows scans a row into a struct |
|||
db.ScanRows(rows, &user) |
|||
|
|||
// Perform operations on each user |
|||
} |
|||
This approach is ideal for complex data processing that cannot be easily achieved with standard query methods. |
|||
|
|||
FindInBatches |
|||
FindInBatches allows querying and processing records in batches. This is especially useful for handling large datasets efficiently, reducing memory usage and improving performance. |
|||
|
|||
With FindInBatches, GORM processes records in specified batch sizes. Inside the batch processing function, you can apply operations to each batch of records. |
|||
|
|||
// Processing records in batches of 100 |
|||
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx \*gorm.DB, batch int) error { |
|||
for \_, result := range results { |
|||
// Operations on each record in the batch |
|||
} |
|||
|
|||
// Save changes to the records in the current batch |
|||
tx.Save(&results) |
|||
|
|||
// tx.RowsAffected provides the count of records in the current batch |
|||
// The variable 'batch' indicates the current batch number |
|||
|
|||
// Returning an error will stop further batch processing |
|||
return nil |
|||
}) |
|||
|
|||
// result.Error contains any errors encountered during batch processing |
|||
// result.RowsAffected provides the count of all processed records across batches |
|||
FindInBatches is an effective tool for processing large volumes of data in manageable chunks, optimizing resource usage and performance. |
|||
|
|||
Query Hooks |
|||
GORM offers the ability to use hooks, such as AfterFind, which are triggered during the lifecycle of a query. These hooks allow for custom logic to be executed at specific points, such as after a record has been retrieved from the database. |
|||
|
|||
This hook is useful for post-query data manipulation or default value settings. For more detailed information and additional hook types, refer to Hooks in the GORM documentation. |
|||
|
|||
func (u *User) AfterFind(tx *gorm.DB) (err error) { |
|||
// Custom logic after finding a user |
|||
if u.Role == "" { |
|||
u.Role = "user" // Set default role if not specified |
|||
} |
|||
return |
|||
} |
|||
|
|||
// Usage of AfterFind hook happens automatically when a User is queried |
|||
Pluck |
|||
The Pluck method in GORM is used to query a single column from the database and scan the result into a slice. This method is ideal for when you need to retrieve specific fields from a model. |
|||
|
|||
If you need to query more than one column, you can use Select with Scan or Find instead. |
|||
|
|||
// Retrieving ages of all users |
|||
var ages []int64 |
|||
db.Model(&User{}).Pluck("age", &ages) |
|||
|
|||
// Retrieving names of all users |
|||
var names []string |
|||
db.Model(&User{}).Pluck("name", &names) |
|||
|
|||
// Retrieving names from a different table |
|||
db.Table("deleted_users").Pluck("name", &names) |
|||
|
|||
// Using Distinct with Pluck |
|||
db.Model(&User{}).Distinct().Pluck("Name", &names) |
|||
// SQL: SELECT DISTINCT `name` FROM `users` |
|||
|
|||
// Querying multiple columns |
|||
db.Select("name", "age").Scan(&users) |
|||
db.Select("name", "age").Find(&users) |
|||
Scopes |
|||
Scopes in GORM are a powerful feature that allows you to define commonly-used query conditions as reusable methods. These scopes can be easily referenced in your queries, making your code more modular and readable. |
|||
|
|||
Defining Scopes |
|||
Scopes are defined as functions that modify and return a gorm.DB instance. You can define a variety of conditions as scopes based on your application’s requirements. |
|||
|
|||
// Scope for filtering records where amount is greater than 1000 |
|||
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { |
|||
return db.Where("amount > ?", 1000) |
|||
} |
|||
|
|||
// Scope for orders paid with a credit card |
|||
func PaidWithCreditCard(db *gorm.DB) *gorm.DB { |
|||
return db.Where("pay_mode_sign = ?", "C") |
|||
} |
|||
|
|||
// Scope for orders paid with cash on delivery (COD) |
|||
func PaidWithCod(db *gorm.DB) *gorm.DB { |
|||
return db.Where("pay_mode_sign = ?", "COD") |
|||
} |
|||
|
|||
// Scope for filtering orders by status |
|||
func OrderStatus(status []string) func(db *gorm.DB) *gorm.DB { |
|||
return func(db *gorm.DB) *gorm.DB { |
|||
return db.Where("status IN (?)", status) |
|||
} |
|||
} |
|||
Applying Scopes in Queries |
|||
You can apply one or more scopes to a query by using the Scopes method. This allows you to chain multiple conditions dynamically. |
|||
|
|||
// Applying scopes to find all credit card orders with an amount greater than 1000 |
|||
db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders) |
|||
|
|||
// Applying scopes to find all COD orders with an amount greater than 1000 |
|||
db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) |
|||
|
|||
// Applying scopes to find all orders with specific statuses and an amount greater than 1000 |
|||
db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders) |
|||
Scopes are a clean and efficient way to encapsulate common query logic, enhancing the maintainability and readability of your code. For more detailed examples and usage, refer to Scopes in the GORM documentation. |
|||
|
|||
Count |
|||
The Count method in GORM is used to retrieve the number of records that match a given query. It’s a useful feature for understanding the size of a dataset, particularly in scenarios involving conditional queries or data analysis. |
|||
|
|||
Getting the Count of Matched Records |
|||
You can use Count to determine the number of records that meet specific criteria in your queries. |
|||
|
|||
var count int64 |
|||
|
|||
// Counting users with specific names |
|||
db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count) |
|||
// SQL: SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2' |
|||
|
|||
// Counting users with a single name condition |
|||
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count) |
|||
// SQL: SELECT count(1) FROM users WHERE name = 'jinzhu' |
|||
|
|||
// Counting records in a different table |
|||
db.Table("deleted_users").Count(&count) |
|||
// SQL: SELECT count(1) FROM deleted_users |
|||
Count with Distinct and Group |
|||
GORM also allows counting distinct values and grouping results. |
|||
|
|||
// Counting distinct names |
|||
db.Model(&User{}).Distinct("name").Count(&count) |
|||
// SQL: SELECT COUNT(DISTINCT(`name`)) FROM `users` |
|||
|
|||
// Counting distinct values with a custom select |
|||
db.Table("deleted_users").Select("count(distinct(name))").Count(&count) |
|||
// SQL: SELECT count(distinct(name)) FROM deleted_users |
|||
|
|||
// Counting grouped records |
|||
users := []User{ |
|||
{Name: "name1"}, |
|||
{Name: "name2"}, |
|||
{Name: "name3"}, |
|||
{Name: "name3"}, |
|||
} |
|||
|
|||
db.Model(&User{}).Group("name").Count(&count) |
|||
// Count after grouping by name |
|||
// count => 3 |
|||
GitHub tag (latest SemVer) |
@ -0,0 +1,221 @@ |
|||
Associations |
|||
Auto Create/Update |
|||
GORM automates the saving of associations and their references when creating or updating records, using an upsert technique that primarily updates foreign key references for existing associations. |
|||
|
|||
Auto-Saving Associations on Create |
|||
When you create a new record, GORM will automatically save its associated data. This includes inserting data into related tables and managing foreign key references. |
|||
|
|||
user := User{ |
|||
Name: "jinzhu", |
|||
BillingAddress: Address{Address1: "Billing Address - Address 1"}, |
|||
ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, |
|||
Emails: []Email{ |
|||
{Email: "jinzhu@example.com"}, |
|||
{Email: "jinzhu-2@example.com"}, |
|||
}, |
|||
Languages: []Language{ |
|||
{Name: "ZH"}, |
|||
{Name: "EN"}, |
|||
}, |
|||
} |
|||
|
|||
// Creating a user along with its associated addresses, emails, and languages |
|||
db.Create(&user) |
|||
// BEGIN TRANSACTION; |
|||
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY DO NOTHING; |
|||
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); |
|||
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING; |
|||
// INSERT INTO "languages" ("name") VALUES ('ZH'), ('EN') ON DUPLICATE KEY DO NOTHING; |
|||
// INSERT INTO "user_languages" ("user_id","language_id") VALUES (111, 1), (111, 2) ON DUPLICATE KEY DO NOTHING; |
|||
// COMMIT; |
|||
|
|||
db.Save(&user) |
|||
Updating Associations with FullSaveAssociations |
|||
For scenarios where a full update of the associated data is required (not just the foreign key references), the FullSaveAssociations mode should be used. |
|||
|
|||
// Update a user and fully update all its associations |
|||
db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user) |
|||
// SQL: Fully updates addresses, users, emails tables, including existing associated records |
|||
Using FullSaveAssociations ensures that the entire state of the model, including all its associations, is reflected in the database, maintaining data integrity and consistency throughout the application. |
|||
|
|||
Skip Auto Create/Update |
|||
GORM provides flexibility to skip automatic saving of associations during create or update operations. This can be achieved using the Select or Omit methods, which allow you to specify exactly which fields or associations should be included or excluded in the operation. |
|||
|
|||
Using Select to Include Specific Fields |
|||
The Select method lets you specify which fields of the model should be saved. This means that only the selected fields will be included in the SQL operation. |
|||
|
|||
user := User{ |
|||
// User and associated data |
|||
} |
|||
|
|||
// Only include the 'Name' field when creating the user |
|||
db.Select("Name").Create(&user) |
|||
// SQL: INSERT INTO "users" (name) VALUES ("jinzhu"); |
|||
Using Omit to Exclude Fields or Associations |
|||
Conversely, Omit allows you to exclude certain fields or associations when saving a model. |
|||
|
|||
// Skip creating the 'BillingAddress' when creating the user |
|||
db.Omit("BillingAddress").Create(&user) |
|||
|
|||
// Skip all associations when creating the user |
|||
db.Omit(clause.Associations).Create(&user) |
|||
NOTE: |
|||
For many-to-many associations, GORM upserts the associations before creating join table references. To skip this upserting, use Omit with the association name followed by .\*: |
|||
|
|||
// Skip upserting 'Languages' associations |
|||
db.Omit("Languages.\*").Create(&user) |
|||
To skip creating both the association and its references: |
|||
|
|||
// Skip creating 'Languages' associations and their references |
|||
db.Omit("Languages").Create(&user) |
|||
Using Select and Omit, you can fine-tune how GORM handles the creation or updating of your models, giving you control over the auto-save behavior of associations. |
|||
|
|||
Select/Omit Association fields |
|||
In GORM, when creating or updating records, you can use the Select and Omit methods to specifically include or exclude certain fields of an associated model. |
|||
|
|||
With Select, you can specify which fields of an associated model should be included when saving the primary model. This is particularly useful for selectively saving parts of an association. |
|||
|
|||
Conversely, Omit lets you exclude certain fields of an associated model from being saved. This can be useful when you want to prevent specific parts of an association from being persisted. |
|||
|
|||
user := User{ |
|||
Name: "jinzhu", |
|||
BillingAddress: Address{Address1: "Billing Address - Address 1", Address2: "addr2"}, |
|||
ShippingAddress: Address{Address1: "Shipping Address - Address 1", Address2: "addr2"}, |
|||
} |
|||
|
|||
// Create user and his BillingAddress, ShippingAddress, including only specified fields of BillingAddress |
|||
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user) |
|||
// SQL: Creates user and BillingAddress with only 'Address1' and 'Address2' fields |
|||
|
|||
// Create user and his BillingAddress, ShippingAddress, excluding specific fields of BillingAddress |
|||
db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user) |
|||
// SQL: Creates user and BillingAddress, omitting 'Address2' and 'CreatedAt' fields |
|||
Delete Associations |
|||
GORM allows for the deletion of specific associated relationships (has one, has many, many2many) using the Select method when deleting a primary record. This feature is particularly useful for maintaining database integrity and ensuring related data is appropriately managed upon deletion. |
|||
|
|||
You can specify which associations should be deleted along with the primary record by using Select. |
|||
|
|||
// Delete a user's account when deleting the user |
|||
db.Select("Account").Delete(&user) |
|||
|
|||
// Delete a user's Orders and CreditCards associations when deleting the user |
|||
db.Select("Orders", "CreditCards").Delete(&user) |
|||
|
|||
// Delete all of a user's has one, has many, and many2many associations |
|||
db.Select(clause.Associations).Delete(&user) |
|||
|
|||
// Delete each user's account when deleting multiple users |
|||
db.Select("Account").Delete(&users) |
|||
NOTE: |
|||
It’s important to note that associations will be deleted only if the primary key of the deleting record is not zero. GORM uses these primary keys as conditions to delete the selected associations. |
|||
|
|||
// This will not work as intended |
|||
db.Select("Account").Where("name = ?", "jinzhu").Delete(&User{}) |
|||
// SQL: Deletes all users with name 'jinzhu', but their accounts won't be deleted |
|||
|
|||
// Correct way to delete a user and their account |
|||
db.Select("Account").Where("name = ?", "jinzhu").Delete(&User{ID: 1}) |
|||
// SQL: Deletes the user with name 'jinzhu' and ID '1', and the user's account |
|||
|
|||
// Deleting a user with a specific ID and their account |
|||
db.Select("Account").Delete(&User{ID: 1}) |
|||
// SQL: Deletes the user with ID '1', and the user's account |
|||
Association Mode |
|||
Association Mode in GORM offers various helper methods to handle relationships between models, providing an efficient way to manage associated data. |
|||
|
|||
To start Association Mode, specify the source model and the relationship’s field name. The source model must contain a primary key, and the relationship’s field name should match an existing association. |
|||
|
|||
var user User |
|||
db.Model(&user).Association("Languages") |
|||
// Check for errors |
|||
error := db.Model(&user).Association("Languages").Error |
|||
Finding Associations |
|||
Retrieve associated records with or without additional conditions. |
|||
|
|||
// Simple find |
|||
db.Model(&user).Association("Languages").Find(&languages) |
|||
|
|||
// Find with conditions |
|||
codes := []string{"zh-CN", "en-US", "ja-JP"} |
|||
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages) |
|||
Appending Associations |
|||
Add new associations for many to many, has many, or replace the current association for has one, belongs to. |
|||
|
|||
// Append new languages |
|||
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) |
|||
|
|||
db.Model(&user).Association("Languages").Append(&Language{Name: "DE"}) |
|||
|
|||
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"}) |
|||
Replacing Associations |
|||
Replace current associations with new ones. |
|||
|
|||
// Replace existing languages |
|||
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) |
|||
|
|||
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) |
|||
Deleting Associations |
|||
Remove the relationship between the source and arguments, only deleting the reference. |
|||
|
|||
// Delete specific languages |
|||
db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) |
|||
|
|||
db.Model(&user).Association("Languages").Delete(languageZH, languageEN) |
|||
Clearing Associations |
|||
Remove all references between the source and association. |
|||
|
|||
// Clear all languages |
|||
db.Model(&user).Association("Languages").Clear() |
|||
Counting Associations |
|||
Get the count of current associations, with or without conditions. |
|||
|
|||
// Count all languages |
|||
db.Model(&user).Association("Languages").Count() |
|||
|
|||
// Count with conditions |
|||
codes := []string{"zh-CN", "en-US", "ja-JP"} |
|||
db.Model(&user).Where("code IN ?", codes).Association("Languages").Count() |
|||
Batch Data Handling |
|||
Association Mode allows you to handle relationships for multiple records in a batch. This includes finding, appending, replacing, deleting, and counting operations for associated data. |
|||
|
|||
Finding Associations: Retrieve associated data for a collection of records. |
|||
db.Model(&users).Association("Role").Find(&roles) |
|||
Deleting Associations: Remove specific associations across multiple records. |
|||
db.Model(&users).Association("Team").Delete(&userA) |
|||
Counting Associations: Get the count of associations for a batch of records. |
|||
db.Model(&users).Association("Team").Count() |
|||
Appending/Replacing Associations: Manage associations for multiple records. Note the need for matching argument lengths with the data. |
|||
var users = []User{user1, user2, user3} |
|||
|
|||
// Append different teams to different users in a batch |
|||
// Append userA to user1's team, userB to user2's team, and userA, userB, userC to user3's team |
|||
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC}) |
|||
|
|||
// Replace teams for multiple users in a batch |
|||
// Reset user1's team to userA, user2's team to userB, and user3's team to userA, userB, and userC |
|||
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC}) |
|||
Delete Association Record |
|||
In GORM, the Replace, Delete, and Clear methods in Association Mode primarily affect the foreign key references, not the associated records themselves. Understanding and managing this behavior is crucial for data integrity. |
|||
|
|||
Reference Update: These methods update the association’s foreign key to null, effectively removing the link between the source and associated models. |
|||
No Physical Record Deletion: The actual associated records remain untouched in the database. |
|||
Modifying Deletion Behavior with Unscoped |
|||
For scenarios requiring actual deletion of associated records, the Unscoped method alters this behavior. |
|||
|
|||
Soft Delete: Marks associated records as deleted (sets deleted_at field) without removing them from the database. |
|||
db.Model(&user).Association("Languages").Unscoped().Clear() |
|||
Permanent Delete: Physically deletes the association records from the database. |
|||
// db.Unscoped().Model(&user) |
|||
db.Unscoped().Model(&user).Association("Languages").Unscoped().Clear() |
|||
Association Tags |
|||
Association tags in GORM are used to specify how associations between models are handled. These tags define the relationship’s details, such as foreign keys, references, and constraints. Understanding these tags is essential for setting up and managing relationships effectively. |
|||
|
|||
Tag Description |
|||
foreignKey Specifies the column name of the current model used as a foreign key in the join table. |
|||
references Indicates the column name in the reference table that the foreign key of the join table maps to. |
|||
polymorphic Defines the polymorphic type, typically the model name. |
|||
polymorphicValue Sets the polymorphic value, usually the table name, if not specified otherwise. |
|||
many2many Names the join table used in a many-to-many relationship. |
|||
joinForeignKey Identifies the foreign key column in the join table that maps back to the current model’s table. |
|||
joinReferences Points to the foreign key column in the join table that links to the reference model’s table. |
|||
constraint Specifies relational constraints like OnUpdate, OnDelete for the association. |
@ -0,0 +1,92 @@ |
|||
Belongs To |
|||
Belongs To |
|||
A belongs to association sets up a one-to-one connection with another model, such that each instance of the declaring model “belongs to” one instance of the other model. |
|||
|
|||
For example, if your application includes users and companies, and each user can be assigned to exactly one company, the following types represent that relationship. Notice here that, on the User object, there is both a CompanyID as well as a Company. By default, the CompanyID is implicitly used to create a foreign key relationship between the User and Company tables, and thus must be included in the User struct in order to fill the Company inner struct. |
|||
|
|||
// `User` belongs to `Company`, `CompanyID` is the foreign key |
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyID int |
|||
Company Company |
|||
} |
|||
|
|||
type Company struct { |
|||
ID int |
|||
Name string |
|||
} |
|||
Refer to Eager Loading for details on populating the inner struct. |
|||
|
|||
Override Foreign Key |
|||
To define a belongs to relationship, the foreign key must exist, the default foreign key uses the owner’s type name plus its primary field name. |
|||
|
|||
For the above example, to define the User model that belongs to Company, the foreign key should be CompanyID by convention |
|||
|
|||
GORM provides a way to customize the foreign key, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyRefer int |
|||
Company Company `gorm:"foreignKey:CompanyRefer"` |
|||
// use CompanyRefer as foreign key |
|||
} |
|||
|
|||
type Company struct { |
|||
ID int |
|||
Name string |
|||
} |
|||
Override References |
|||
For a belongs to relationship, GORM usually uses the owner’s primary field as the foreign key’s value, for the above example, it is Company‘s field ID. |
|||
|
|||
When you assign a user to a company, GORM will save the company’s ID into the user’s CompanyID field. |
|||
|
|||
You are able to change it with tag references, e.g: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyID string |
|||
Company Company `gorm:"references:Code"` // use Code as references |
|||
} |
|||
|
|||
type Company struct { |
|||
ID int |
|||
Code string |
|||
Name string |
|||
} |
|||
NOTE GORM usually guess the relationship as has one if override foreign key name already exists in owner’s type, we need to specify references in the belongs to relationship. |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyID int |
|||
Company Company `gorm:"references:CompanyID"` // use Company.CompanyID as references |
|||
} |
|||
|
|||
type Company struct { |
|||
CompanyID int |
|||
Code string |
|||
Name string |
|||
} |
|||
CRUD with Belongs To |
|||
Please checkout Association Mode for working with belongs to relations |
|||
|
|||
Eager Loading |
|||
GORM allows eager loading belongs to associations with Preload or Joins, refer Preloading (Eager loading) for details |
|||
|
|||
FOREIGN KEY Constraints |
|||
You can setup OnUpdate, OnDelete constraints with tag constraint, it will be created when migrating with GORM, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyID int |
|||
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` |
|||
} |
|||
|
|||
type Company struct { |
|||
ID int |
|||
Name string |
|||
} |
@ -0,0 +1,239 @@ |
|||
Connecting to a Database |
|||
GORM officially supports the databases MySQL, PostgreSQL, SQLite, SQL Server, and TiDB |
|||
|
|||
MySQL |
|||
import ( |
|||
"gorm.io/driver/mysql" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
func main() { |
|||
// refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details |
|||
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" |
|||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) |
|||
} |
|||
NOTE: |
|||
To handle time.Time correctly, you need to include parseTime as a parameter. (more parameters) |
|||
To fully support UTF-8 encoding, you need to change charset=utf8 to charset=utf8mb4. See this article for a detailed explanation |
|||
|
|||
MySQL Driver provides a few advanced configurations which can be used during initialization, for example: |
|||
|
|||
db, err := gorm.Open(mysql.New(mysql.Config{ |
|||
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name |
|||
DefaultStringSize: 256, // default size for string fields |
|||
DisableDatetimePrecision: true, // disable datetime precision, which not supported before MySQL 5.6 |
|||
DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB |
|||
DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB |
|||
SkipInitializeWithVersion: false, // auto configure based on currently MySQL version |
|||
}), &gorm.Config{}) |
|||
Customize Driver |
|||
GORM allows to customize the MySQL driver with the DriverName option, for example: |
|||
|
|||
import ( |
|||
\_ "example.com/my_mysql_driver" |
|||
"gorm.io/driver/mysql" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
db, err := gorm.Open(mysql.New(mysql.Config{ |
|||
DriverName: "my_mysql_driver", |
|||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name, refer https://github.com/go-sql-driver/mysql#dsn-data-source-name |
|||
}), &gorm.Config{}) |
|||
Existing database connection |
|||
GORM allows to initialize \*gorm.DB with an existing database connection |
|||
|
|||
import ( |
|||
"database/sql" |
|||
"gorm.io/driver/mysql" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
sqlDB, err := sql.Open("mysql", "mydb_dsn") |
|||
gormDB, err := gorm.Open(mysql.New(mysql.Config{ |
|||
Conn: sqlDB, |
|||
}), &gorm.Config{}) |
|||
PostgreSQL |
|||
import ( |
|||
"gorm.io/driver/postgres" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai" |
|||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) |
|||
We are using pgx as postgres’s database/sql driver, it enables prepared statement cache by default, to disable it: |
|||
|
|||
// https://github.com/go-gorm/postgres |
|||
db, err := gorm.Open(postgres.New(postgres.Config{ |
|||
DSN: "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai", |
|||
PreferSimpleProtocol: true, // disables implicit prepared statement usage |
|||
}), &gorm.Config{}) |
|||
Customize Driver |
|||
GORM allows to customize the PostgreSQL driver with the DriverName option, for example: |
|||
|
|||
import ( |
|||
\_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
db, err := gorm.Open(postgres.New(postgres.Config{ |
|||
DriverName: "cloudsqlpostgres", |
|||
DSN: "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable", |
|||
}) |
|||
Existing database connection |
|||
GORM allows to initialize \*gorm.DB with an existing database connection |
|||
|
|||
import ( |
|||
"database/sql" |
|||
"gorm.io/driver/postgres" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
sqlDB, err := sql.Open("pgx", "mydb_dsn") |
|||
gormDB, err := gorm.Open(postgres.New(postgres.Config{ |
|||
Conn: sqlDB, |
|||
}), &gorm.Config{}) |
|||
GaussDB |
|||
import ( |
|||
"gorm.io/driver/gaussdb" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=8000 sslmode=disable TimeZone=Asia/Shanghai" |
|||
db, err := gorm.Open(gaussdb.Open(dsn), &gorm.Config{}) |
|||
We are using gaussdb-go as gaussdb’s database/sql driver, it enables prepared statement cache by default, to disable it: |
|||
|
|||
// https://github.com/go-gorm/gaussdb |
|||
db, err := gorm.Open(gaussdb.New(gaussdb.Config{ |
|||
DSN: "user=gorm password=gorm dbname=gorm port=8000 sslmode=disable TimeZone=Asia/Shanghai", |
|||
PreferSimpleProtocol: true, // disables implicit prepared statement usage |
|||
}), &gorm.Config{}) |
|||
Customize Driver |
|||
GORM allows to customize the GaussDB driver with the DriverName option, for example: |
|||
|
|||
import ( |
|||
\_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/gaussdb" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
db, err := gorm.Open(gaussdb.New(gaussdb.Config{ |
|||
DriverName: "cloudsqlgaussdb", |
|||
DSN: "host=project:region:instance user=gaussdb dbname=gaussdb password=password sslmode=disable", |
|||
}) |
|||
Existing database connection |
|||
GORM allows to initialize \*gorm.DB with an existing database connection |
|||
|
|||
import ( |
|||
"database/sql" |
|||
"gorm.io/driver/gaussdb" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
sqlDB, err := sql.Open("gaussdbgo", "mydb_dsn") |
|||
gormDB, err := gorm.Open(gaussdb.New(gaussdb.Config{ |
|||
Conn: sqlDB, |
|||
}), &gorm.Config{}) |
|||
SQLite |
|||
import ( |
|||
"gorm.io/driver/sqlite" // Sqlite driver based on CGO |
|||
// "github.com/glebarez/sqlite" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
// github.com/mattn/go-sqlite3 |
|||
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{}) |
|||
NOTE: You can also use file::memory:?cache=shared instead of a path to a file. This will tell SQLite to use a temporary database in system memory. (See SQLite docs for this) |
|||
|
|||
SQL Server |
|||
import ( |
|||
"gorm.io/driver/sqlserver" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
// github.com/denisenkom/go-mssqldb |
|||
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" |
|||
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{}) |
|||
TiDB |
|||
TiDB is compatible with MySQL protocol. You can follow the MySQL part to create a connection to TiDB. |
|||
|
|||
There are some points noteworthy for TiDB: |
|||
|
|||
You can use gorm:"primaryKey;default:auto_random()" tag to use AUTO_RANDOM feature for TiDB. |
|||
TiDB supported SAVEPOINT from v6.2.0, please notice the version of TiDB when you use this feature. |
|||
TiDB supported FOREIGN KEY from v6.6.0, please notice the version of TiDB when you use this feature. |
|||
import ( |
|||
"fmt" |
|||
"gorm.io/driver/mysql" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
type Product struct { |
|||
ID uint `gorm:"primaryKey;default:auto_random()"` |
|||
Code string |
|||
Price uint |
|||
} |
|||
|
|||
func main() { |
|||
db, err := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:4000)/test"), &gorm.Config{}) |
|||
if err != nil { |
|||
panic("failed to connect database") |
|||
} |
|||
|
|||
db.AutoMigrate(&Product{}) |
|||
|
|||
insertProduct := &Product{Code: "D42", Price: 100} |
|||
|
|||
db.Create(insertProduct) |
|||
fmt.Printf("insert ID: %d, Code: %s, Price: %d\n", |
|||
insertProduct.ID, insertProduct.Code, insertProduct.Price) |
|||
|
|||
readProduct := &Product{} |
|||
db.First(&readProduct, "code = ?", "D42") // find product with code D42 |
|||
|
|||
fmt.Printf("read ID: %d, Code: %s, Price: %d\n", |
|||
readProduct.ID, readProduct.Code, readProduct.Price) |
|||
} |
|||
Clickhouse |
|||
https://github.com/go-gorm/clickhouse |
|||
|
|||
import ( |
|||
"gorm.io/driver/clickhouse" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
func main() { |
|||
dsn := "tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20" |
|||
db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{}) |
|||
|
|||
// Auto Migrate |
|||
db.AutoMigrate(&User{}) |
|||
// Set table options |
|||
db.Set("gorm:table_options", "ENGINE=Distributed(cluster, default, hits)").AutoMigrate(&User{}) |
|||
|
|||
// Insert |
|||
db.Create(&user) |
|||
|
|||
// Select |
|||
db.Find(&user, "id = ?", 10) |
|||
|
|||
// Batch Insert |
|||
var users = []User{user1, user2, user3} |
|||
db.Create(&users) |
|||
// ... |
|||
} |
|||
Connection Pool |
|||
GORM using database/sql to maintain connection pool |
|||
|
|||
sqlDB, err := db.DB() |
|||
|
|||
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool. |
|||
sqlDB.SetMaxIdleConns(10) |
|||
|
|||
// SetMaxOpenConns sets the maximum number of open connections to the database. |
|||
sqlDB.SetMaxOpenConns(100) |
|||
|
|||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. |
|||
sqlDB.SetConnMaxLifetime(time.Hour) |
|||
Refer Generic Interface for details |
|||
|
|||
Unsupported Databases |
|||
Some databases may be compatible with the mysql or postgres dialect, in which case you could just use the dialect for those databases. |
@ -0,0 +1,87 @@ |
|||
Has Many |
|||
Has Many |
|||
A has many association sets up a one-to-many connection with another model, unlike has one, the owner could have zero or many instances of models. |
|||
|
|||
For example, if your application includes users and credit card, and each user can have many credit cards. |
|||
|
|||
Declare |
|||
// User has many CreditCards, UserID is the foreign key |
|||
type User struct { |
|||
gorm.Model |
|||
CreditCards []CreditCard |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserID uint |
|||
} |
|||
Retrieve |
|||
// Retrieve user list with eager loading credit cards |
|||
func GetAll(db \*gorm.DB) ([]User, error) { |
|||
var users []User |
|||
err := db.Model(&User{}).Preload("CreditCards").Find(&users).Error |
|||
return users, err |
|||
} |
|||
Override Foreign Key |
|||
To define a has many relationship, a foreign key must exist. The default foreign key’s name is the owner’s type name plus the name of its primary key field |
|||
|
|||
For example, to define a model that belongs to User, the foreign key should be UserID. |
|||
|
|||
To use another field as foreign key, you can customize it with a foreignKey tag, e.g: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"` |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserRefer uint |
|||
} |
|||
Override References |
|||
GORM usually uses the owner’s primary key as the foreign key’s value, for the above example, it is the User‘s ID, |
|||
|
|||
When you assign credit cards to a user, GORM will save the user’s ID into credit cards’ UserID field. |
|||
|
|||
You are able to change it with tag references, e.g: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
MemberNumber string |
|||
CreditCards []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"` |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserNumber string |
|||
} |
|||
CRUD with Has Many |
|||
Please checkout Association Mode for working with has many relations |
|||
|
|||
Eager Loading |
|||
GORM allows eager loading has many associations with Preload, refer Preloading (Eager loading) for details |
|||
|
|||
Self-Referential Has Many |
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
ManagerID \*uint |
|||
Team []User `gorm:"foreignkey:ManagerID"` |
|||
} |
|||
FOREIGN KEY Constraints |
|||
You can setup OnUpdate, OnDelete constraints with tag constraint, it will be created when migrating with GORM, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserID uint |
|||
} |
|||
You are also allowed to delete selected has many associations with Select when deleting, checkout Delete with Select for details |
@ -0,0 +1,88 @@ |
|||
Has One |
|||
Has One |
|||
A has one association sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. |
|||
|
|||
For example, if your application includes users and credit cards, and each user can only have one credit card. |
|||
|
|||
Declare |
|||
// User has one CreditCard, UserID is the foreign key |
|||
type User struct { |
|||
gorm.Model |
|||
CreditCard CreditCard |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserID uint |
|||
} |
|||
Retrieve |
|||
// Retrieve user list with eager loading credit card |
|||
func GetAll(db \*gorm.DB) ([]User, error) { |
|||
var users []User |
|||
err := db.Model(&User{}).Preload("CreditCard").Find(&users).Error |
|||
return users, err |
|||
} |
|||
Override Foreign Key |
|||
For a has one relationship, a foreign key field must also exist, the owner will save the primary key of the model belongs to it into this field. |
|||
|
|||
The field’s name is usually generated with has one model’s type plus its primary key, for the above example it is UserID. |
|||
|
|||
When you give a credit card to the user, it will save the User’s ID into its UserID field. |
|||
|
|||
If you want to use another field to save the relationship, you can change it with tag foreignKey, e.g: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
CreditCard CreditCard `gorm:"foreignKey:UserName"` |
|||
// use UserName as foreign key |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserName string |
|||
} |
|||
Override References |
|||
By default, the owned entity will save the has one model’s primary key into a foreign key, you could change to save another field’s value, like using Name for the below example. |
|||
|
|||
You are able to change it with tag references, e.g: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string `gorm:"index"` |
|||
CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"` |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserName string |
|||
} |
|||
CRUD with Has One |
|||
Please checkout Association Mode for working with has one relations |
|||
|
|||
Eager Loading |
|||
GORM allows eager loading has one associations with Preload or Joins, refer Preloading (Eager loading) for details |
|||
|
|||
Self-Referential Has One |
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
ManagerID *uint |
|||
Manager *User |
|||
} |
|||
FOREIGN KEY Constraints |
|||
You can setup OnUpdate, OnDelete constraints with tag constraint, it will be created when migrating with GORM, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` |
|||
} |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserID uint |
|||
} |
|||
You are also allowed to delete selected has one associations with Select when deleting, checkout Delete with Select for details |
@ -0,0 +1,181 @@ |
|||
Many To Many |
|||
Many To Many |
|||
Many to Many add a join table between two models. |
|||
|
|||
For example, if your application includes users and languages, and a user can speak many languages, and many users can speak a specified language. |
|||
|
|||
// User has and belongs to many languages, `user_languages` is the join table |
|||
type User struct { |
|||
gorm.Model |
|||
Languages []Language `gorm:"many2many:user_languages;"` |
|||
} |
|||
|
|||
type Language struct { |
|||
gorm.Model |
|||
Name string |
|||
} |
|||
When using GORM AutoMigrate to create a table for User, GORM will create the join table automatically |
|||
|
|||
Back-Reference |
|||
Declare |
|||
// User has and belongs to many languages, use `user_languages` as join table |
|||
type User struct { |
|||
gorm.Model |
|||
Languages []\*Language `gorm:"many2many:user_languages;"` |
|||
} |
|||
|
|||
type Language struct { |
|||
gorm.Model |
|||
Name string |
|||
Users []*User `gorm:"many2many:user_languages;"` |
|||
} |
|||
Retrieve |
|||
// Retrieve user list with eager loading languages |
|||
func GetAllUsers(db *gorm.DB) ([]User, error) { |
|||
var users []User |
|||
err := db.Model(&User{}).Preload("Languages").Find(&users).Error |
|||
return users, err |
|||
} |
|||
|
|||
// Retrieve language list with eager loading users |
|||
func GetAllLanguages(db \*gorm.DB) ([]Language, error) { |
|||
var languages []Language |
|||
err := db.Model(&Language{}).Preload("Users").Find(&languages).Error |
|||
return languages, err |
|||
} |
|||
Override Foreign Key |
|||
For a many2many relationship, the join table owns the foreign key which references two models, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Languages []Language `gorm:"many2many:user_languages;"` |
|||
} |
|||
|
|||
type Language struct { |
|||
gorm.Model |
|||
Name string |
|||
} |
|||
|
|||
// Join Table: user_languages |
|||
// foreign key: user_id, reference: users.id |
|||
// foreign key: language_id, reference: languages.id |
|||
To override them, you can use tag foreignKey, references, joinForeignKey, joinReferences, not necessary to use them together, you can just use one of them to override some foreign keys/references |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"` |
|||
Refer uint `gorm:"index:,unique"` |
|||
} |
|||
|
|||
type Profile struct { |
|||
gorm.Model |
|||
Name string |
|||
UserRefer uint `gorm:"index:,unique"` |
|||
} |
|||
|
|||
// Which creates join table: user_profiles |
|||
// foreign key: user_refer_id, reference: users.refer |
|||
// foreign key: profile_refer, reference: profiles.user_refer |
|||
NOTE: |
|||
Some databases only allow create database foreign keys that reference on a field having unique index, so you need to specify the unique index tag if you are creating database foreign keys when migrating |
|||
|
|||
Self-Referential Many2Many |
|||
Self-referencing many2many relationship |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Friends []\*User `gorm:"many2many:user_friends"` |
|||
} |
|||
|
|||
// Which creates join table: user_friends |
|||
// foreign key: user_id, reference: users.id |
|||
// foreign key: friend_id, reference: users.id |
|||
Eager Loading |
|||
GORM allows eager loading has many associations with Preload, refer Preloading (Eager loading) for details |
|||
|
|||
CRUD with Many2Many |
|||
Please checkout Association Mode for working with many2many relations |
|||
|
|||
Customize JoinTable |
|||
JoinTable can be a full-featured model, like having Soft Delete,Hooks supports and more fields, you can set it up with SetupJoinTable, for example: |
|||
|
|||
NOTE: |
|||
Customized join table’s foreign keys required to be composited primary keys or composited unique index |
|||
|
|||
type Person struct { |
|||
ID int |
|||
Name string |
|||
Addresses []Address `gorm:"many2many:person_addresses;"` |
|||
} |
|||
|
|||
type Address struct { |
|||
ID uint |
|||
Name string |
|||
} |
|||
|
|||
type PersonAddress struct { |
|||
PersonID int `gorm:"primaryKey"` |
|||
AddressID int `gorm:"primaryKey"` |
|||
CreatedAt time.Time |
|||
DeletedAt gorm.DeletedAt |
|||
} |
|||
|
|||
func (PersonAddress) BeforeCreate(db \*gorm.DB) error { |
|||
// ... |
|||
} |
|||
|
|||
// Change model Person's field Addresses' join table to PersonAddress |
|||
// PersonAddress must defined all required foreign keys or it will raise error |
|||
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{}) |
|||
FOREIGN KEY Constraints |
|||
You can setup OnUpdate, OnDelete constraints with tag constraint, it will be created when migrating with GORM, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Languages []Language `gorm:"many2many:user_speaks;"` |
|||
} |
|||
|
|||
type Language struct { |
|||
Code string `gorm:"primarykey"` |
|||
Name string |
|||
} |
|||
|
|||
// CREATE TABLE `user_speaks` (`user_id` integer,`language_code` text,PRIMARY KEY (`user_id`,`language_code`),CONSTRAINT `fk_user_speaks_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,CONSTRAINT `fk_user_speaks_language` FOREIGN KEY (`language_code`) REFERENCES `languages`(`code`) ON DELETE SET NULL ON UPDATE CASCADE); |
|||
You are also allowed to delete selected many2many relations with Select when deleting, checkout Delete with Select for details |
|||
|
|||
Composite Foreign Keys |
|||
If you are using Composite Primary Keys for your models, GORM will enable composite foreign keys by default |
|||
|
|||
You are allowed to override the default foreign keys, to specify multiple foreign keys, just separate those keys’ name by commas, for example: |
|||
|
|||
type Tag struct { |
|||
ID uint `gorm:"primaryKey"` |
|||
Locale string `gorm:"primaryKey"` |
|||
Value string |
|||
} |
|||
|
|||
type Blog struct { |
|||
ID uint `gorm:"primaryKey"` |
|||
Locale string `gorm:"primaryKey"` |
|||
Subject string |
|||
Body string |
|||
Tags []Tag `gorm:"many2many:blog_tags;"` |
|||
LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"` |
|||
SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"` |
|||
} |
|||
|
|||
// Join Table: blog_tags |
|||
// foreign key: blog_id, reference: blogs.id |
|||
// foreign key: blog_locale, reference: blogs.locale |
|||
// foreign key: tag_id, reference: tags.id |
|||
// foreign key: tag_locale, reference: tags.locale |
|||
|
|||
// Join Table: locale_blog_tags |
|||
// foreign key: blog_id, reference: blogs.id |
|||
// foreign key: blog_locale, reference: blogs.locale |
|||
// foreign key: tag_id, reference: tags.id |
|||
|
|||
// Join Table: shared_blog_tags |
|||
// foreign key: blog_id, reference: blogs.id |
|||
// foreign key: tag_id, reference: tags.id |
|||
Also check out Composite Primary Keys |
@ -0,0 +1,45 @@ |
|||
Polymorphism |
|||
Polymorphism Association |
|||
GORM supports polymorphism association for has one and has many, it will save owned entity’s table name into polymorphic type’s field, primary key value into the polymorphic field |
|||
|
|||
By default polymorphic:<value> will prefix the column type and column id with <value>. |
|||
The value will be the table name pluralized. |
|||
|
|||
type Dog struct { |
|||
ID int |
|||
Name string |
|||
Toys []Toy `gorm:"polymorphic:Owner;"` |
|||
} |
|||
|
|||
type Toy struct { |
|||
ID int |
|||
Name string |
|||
OwnerID int |
|||
OwnerType string |
|||
} |
|||
|
|||
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}}) |
|||
// INSERT INTO `dogs` (`name`) VALUES ("dog1") |
|||
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1",1,"dogs"), ("toy2",1,"dogs") |
|||
You can specify polymorphism properties separately using the following GORM tags: |
|||
|
|||
polymorphicType: Specifies the column type. |
|||
polymorphicId: Specifies the column ID. |
|||
polymorphicValue: Specifies the value of the type. |
|||
type Dog struct { |
|||
ID int |
|||
Name string |
|||
Toys []Toy `gorm:"polymorphicType:Kind;polymorphicId:OwnerID;polymorphicValue:master"` |
|||
} |
|||
|
|||
type Toy struct { |
|||
ID int |
|||
Name string |
|||
OwnerID int |
|||
Kind string |
|||
} |
|||
|
|||
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}}) |
|||
// INSERT INTO `dogs` (`name`) VALUES ("dog1") |
|||
// INSERT INTO `toys` (`name`,`owner_id`,`kind`) VALUES ("toy1",1,"master"), ("toy2",1,"master") |
|||
In these examples, we’ve used a has-many relationship, but the same principles apply to has-one relationships. |
@ -0,0 +1,131 @@ |
|||
Preloading (Eager Loading) |
|||
Preload |
|||
GORM allows eager loading relations in other SQL with Preload, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Username string |
|||
Orders []Order |
|||
} |
|||
|
|||
type Order struct { |
|||
gorm.Model |
|||
UserID uint |
|||
Price float64 |
|||
} |
|||
|
|||
// Preload Orders when find users |
|||
db.Preload("Orders").Find(&users) |
|||
// SELECT _ FROM users; |
|||
// SELECT _ FROM orders WHERE user_id IN (1,2,3,4); |
|||
|
|||
db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) |
|||
// SELECT _ FROM users; |
|||
// SELECT _ FROM orders WHERE user_id IN (1,2,3,4); // has many |
|||
// SELECT _ FROM profiles WHERE user_id IN (1,2,3,4); // has one |
|||
// SELECT _ FROM roles WHERE id IN (4,5,6); // belongs to |
|||
Joins Preloading |
|||
Preload loads the association data in a separate query, Join Preload will loads association data using left join, for example: |
|||
|
|||
db.Joins("Company").Joins("Manager").Joins("Account").First(&user, 1) |
|||
db.Joins("Company").Joins("Manager").Joins("Account").First(&user, "users.name = ?", "jinzhu") |
|||
db.Joins("Company").Joins("Manager").Joins("Account").Find(&users, "users.id IN ?", []int{1,2,3,4,5}) |
|||
Join with conditions |
|||
|
|||
db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users) |
|||
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true; |
|||
Join nested model |
|||
|
|||
db.Joins("Manager").Joins("Manager.Company").Find(&users) |
|||
// SELECT "users"."id","users"."created_at","users"."updated_at","users"."deleted_at","users"."name","users"."age","users"."birthday","users"."company_id","users"."manager_id","users"."active","Manager"."id" AS "Manager**id","Manager"."created_at" AS "Manager**created_at","Manager"."updated_at" AS "Manager**updated_at","Manager"."deleted_at" AS "Manager**deleted_at","Manager"."name" AS "Manager**name","Manager"."age" AS "Manager**age","Manager"."birthday" AS "Manager**birthday","Manager"."company_id" AS "Manager**company_id","Manager"."manager_id" AS "Manager**manager_id","Manager"."active" AS "Manager**active","Manager**Company"."id" AS "Manager**Company**id","Manager**Company"."name" AS "Manager**Company**name" FROM "users" LEFT JOIN "users" "Manager" ON "users"."manager_id" = "Manager"."id" AND "Manager"."deleted_at" IS NULL LEFT JOIN "companies" "Manager**Company" ON "Manager"."company_id" = "Manager**Company"."id" WHERE "users"."deleted_at" IS NULL |
|||
NOTE Join Preload works with one-to-one relation, e.g: has one, belongs to |
|||
|
|||
Preload All |
|||
clause.Associations can work with Preload similar like Select when creating/updating, you can use it to Preload all associations, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CompanyID uint |
|||
Company Company |
|||
Role Role |
|||
Orders []Order |
|||
} |
|||
|
|||
db.Preload(clause.Associations).Find(&users) |
|||
clause.Associations won’t preload nested associations, but you can use it with Nested Preloading together, e.g: |
|||
|
|||
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users) |
|||
Preload with conditions |
|||
GORM allows Preload associations with conditions, it works similar to Inline Conditions |
|||
|
|||
// Preload Orders with conditions |
|||
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) |
|||
// SELECT _ FROM users; |
|||
// SELECT _ FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled'); |
|||
|
|||
db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) |
|||
// SELECT _ FROM users WHERE state = 'active'; |
|||
// SELECT _ FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled'); |
|||
Custom Preloading SQL |
|||
You are able to custom preloading SQL by passing in func(db *gorm.DB) *gorm.DB, for example: |
|||
|
|||
db.Preload("Orders", func(db *gorm.DB) *gorm.DB { |
|||
return db.Order("orders.amount DESC") |
|||
}).Find(&users) |
|||
// SELECT _ FROM users; |
|||
// SELECT _ FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC; |
|||
Nested Preloading |
|||
GORM supports nested preloading, for example: |
|||
|
|||
db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users) |
|||
|
|||
// Customize Preload conditions for `Orders` |
|||
// And GORM won't preload unmatched order's OrderItems then |
|||
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) |
|||
Embedded Preloading |
|||
Embedded Preloading is used for Embedded Struct, especially the |
|||
same struct. The syntax for Embedded Preloading is similar to Nested Preloading, they are divided by dot. |
|||
|
|||
For example: |
|||
|
|||
type Address struct { |
|||
CountryID int |
|||
Country Country |
|||
} |
|||
|
|||
type Org struct { |
|||
PostalAddress Address `gorm:"embedded;embeddedPrefix:postal_address_"` |
|||
VisitingAddress Address `gorm:"embedded;embeddedPrefix:visiting_address_"` |
|||
Address struct { |
|||
ID int |
|||
Address |
|||
} |
|||
} |
|||
|
|||
// Only preload Org.Address and Org.Address.Country |
|||
db.Preload("Address.Country") // "Address" is has_one, "Country" is belongs_to (nested association) |
|||
|
|||
// Only preload Org.VisitingAddress |
|||
db.Preload("PostalAddress.Country") // "PostalAddress.Country" is belongs_to (embedded association) |
|||
|
|||
// Only preload Org.NestedAddress |
|||
db.Preload("NestedAddress.Address.Country") // "NestedAddress.Address.Country" is belongs_to (embedded association) |
|||
|
|||
// All preloaded include "Address" but exclude "Address.Country", because it won't preload nested associations. |
|||
db.Preload(clause.Associations) |
|||
We can omit embedded part when there is no ambiguity. |
|||
|
|||
type Address struct { |
|||
CountryID int |
|||
Country Country |
|||
} |
|||
|
|||
type Org struct { |
|||
Address Address `gorm:"embedded"` |
|||
} |
|||
|
|||
db.Preload("Address.Country") |
|||
db.Preload("Country") // omit "Address" because there is no ambiguity |
|||
NOTE Embedded Preload only works with belongs to relation. |
|||
Values of other relations are the same in database, we can’t distinguish them. |
@ -0,0 +1,179 @@ |
|||
SQL Builder |
|||
Raw SQL |
|||
Query Raw SQL with Scan |
|||
|
|||
type Result struct { |
|||
ID int |
|||
Name string |
|||
Age int |
|||
} |
|||
|
|||
var result Result |
|||
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result) |
|||
|
|||
db.Raw("SELECT id, name, age FROM users WHERE name = ?", "jinzhu").Scan(&result) |
|||
|
|||
var age int |
|||
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age) |
|||
|
|||
var users []User |
|||
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users) |
|||
Exec with Raw SQL |
|||
|
|||
db.Exec("DROP TABLE users") |
|||
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3}) |
|||
|
|||
// Exec with SQL Expression |
|||
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money \* ? + ?", 10000, 1), "jinzhu") |
|||
NOTE GORM allows cache prepared statement to increase performance, checkout Performance for details |
|||
|
|||
Named Argument |
|||
GORM supports named arguments with sql.NamedArg, map[string]interface{}{} or struct, for example: |
|||
|
|||
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user) |
|||
// SELECT \* FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" |
|||
|
|||
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3) |
|||
// SELECT \* FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.`id` LIMIT 1 |
|||
|
|||
// Named Argument with Raw SQL |
|||
db.Raw("SELECT _ FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name", |
|||
sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user) |
|||
// SELECT _ FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1" |
|||
|
|||
db.Exec("UPDATE users SET name1 = @name, name2 = @name2, name3 = @name", |
|||
sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2")) |
|||
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew" |
|||
|
|||
db.Raw("SELECT _ FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2", |
|||
map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(&user) |
|||
// SELECT _ FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2" |
|||
|
|||
type NamedArgument struct { |
|||
Name string |
|||
Name2 string |
|||
} |
|||
|
|||
db.Raw("SELECT _ FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2", |
|||
NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user) |
|||
// SELECT _ FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2" |
|||
DryRun Mode |
|||
Generate SQL and its arguments without executing, can be used to prepare or test generated SQL, Checkout Session for details |
|||
|
|||
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement |
|||
stmt.SQL.String() //=> SELECT \* FROM `users` WHERE `id` = $1 ORDER BY `id` |
|||
stmt.Vars //=> []interface{}{1} |
|||
ToSQL |
|||
Returns generated SQL without executing. |
|||
|
|||
GORM uses the database/sql’s argument placeholders to construct the SQL statement, which will automatically escape arguments to avoid SQL injection, but the generated SQL don’t provide the safety guarantees, please only use it for debugging. |
|||
|
|||
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB { |
|||
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{}) |
|||
}) |
|||
sql //=> SELECT * FROM "users" WHERE id = 100 AND "users"."deleted_at" IS NULL ORDER BY age desc LIMIT 10 |
|||
Row & Rows |
|||
Get result as *sql.Row |
|||
|
|||
// Use GORM API build SQL |
|||
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row() |
|||
row.Scan(&name, &age) |
|||
|
|||
// Use Raw SQL |
|||
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row() |
|||
row.Scan(&name, &age, &email) |
|||
Get result as \*sql.Rows |
|||
|
|||
// Use GORM API build SQL |
|||
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() |
|||
defer rows.Close() |
|||
for rows.Next() { |
|||
rows.Scan(&name, &age, &email) |
|||
|
|||
// do something |
|||
} |
|||
|
|||
// Raw SQL |
|||
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() |
|||
defer rows.Close() |
|||
for rows.Next() { |
|||
rows.Scan(&name, &age, &email) |
|||
|
|||
// do something |
|||
} |
|||
Checkout FindInBatches for how to query and process records in batch |
|||
Checkout Group Conditions for how to build complicated SQL Query |
|||
|
|||
Scan \*sql.Rows into struct |
|||
Use ScanRows to scan a row into a struct, for example: |
|||
|
|||
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (\*sql.Rows, error) |
|||
defer rows.Close() |
|||
|
|||
var user User |
|||
for rows.Next() { |
|||
// ScanRows scan a row into user |
|||
db.ScanRows(rows, &user) |
|||
|
|||
// do something |
|||
} |
|||
Connection |
|||
Run mutliple SQL in same db tcp connection (not in a transaction) |
|||
|
|||
db.Connection(func(tx \*gorm.DB) error { |
|||
tx.Exec("SET my.role = ?", "admin") |
|||
|
|||
tx.First(&User{}) |
|||
}) |
|||
Advanced |
|||
Clauses |
|||
GORM uses SQL builder generates SQL internally, for each operation, GORM creates a \*gorm.Statement object, all GORM APIs add/change Clause for the Statement, at last, GORM generated SQL based on those clauses |
|||
|
|||
For example, when querying with First, it adds the following clauses to the Statement |
|||
|
|||
var limit = 1 |
|||
clause.Select{Columns: []clause.Column{{Name: "*"}}} |
|||
clause.From{Tables: []clause.Table{{Name: clause.CurrentTable}}} |
|||
clause.Limit{Limit: &limit} |
|||
clause.OrderBy{Columns: []clause.OrderByColumn{ |
|||
{ |
|||
Column: clause.Column{ |
|||
Table: clause.CurrentTable, |
|||
Name: clause.PrimaryKey, |
|||
}, |
|||
}, |
|||
}} |
|||
Then GORM build finally querying SQL in the Query callbacks like: |
|||
|
|||
Statement.Build("SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR") |
|||
Which generate SQL: |
|||
|
|||
SELECT \* FROM `users` ORDER BY `users`.`id` LIMIT 1 |
|||
You can define your own Clause and use it with GORM, it needs to implements Interface |
|||
|
|||
Check out examples for reference |
|||
|
|||
Clause Builder |
|||
For different databases, Clauses may generate different SQL, for example: |
|||
|
|||
db.Offset(10).Limit(5).Find(&users) |
|||
// Generated for SQL Server |
|||
// SELECT _ FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY |
|||
// Generated for MySQL |
|||
// SELECT _ FROM `users` LIMIT 5 OFFSET 10 |
|||
Which is supported because GORM allows database driver register Clause Builder to replace the default one, take the Limit as example |
|||
|
|||
Clause Options |
|||
GORM defined Many Clauses, and some clauses provide advanced options can be used for your application |
|||
|
|||
Although most of them are rarely used, if you find GORM public API can’t match your requirements, may be good to check them out, for example: |
|||
|
|||
db.Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&user) |
|||
// INSERT IGNORE INTO users (name,age...) VALUES ("jinzhu",18...); |
|||
StatementModifier |
|||
GORM provides interface StatementModifier allows you modify statement to match your requirements, take Hints as example |
|||
|
|||
import "gorm.io/hints" |
|||
|
|||
db.Clauses(hints.New("hint")).Find(&User{}) |
|||
// SELECT _ /_+ hint \*/ FROM `users` |
@ -0,0 +1,239 @@ |
|||
Update |
|||
Save All Fields |
|||
Save will save all fields when performing the Updating SQL |
|||
|
|||
db.First(&user) |
|||
|
|||
user.Name = "jinzhu 2" |
|||
user.Age = 100 |
|||
db.Save(&user) |
|||
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; |
|||
Save is an upsert function: |
|||
|
|||
If the value contains no primary key, it performs Create |
|||
If the value has a primary key, it first executes Update (all fields, by Select(_)). |
|||
If rows affected = 0 after Update, it automatically falls back to Create. |
|||
💡 Note: Save guarantees either an update or insert will occur. |
|||
To prevent unintended creation when no rows match, use Select(_).Updates() . |
|||
|
|||
db.Save(&User{Name: "jinzhu", Age: 100}) |
|||
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00") |
|||
|
|||
db.Save(&User{ID: 1, Name: "jinzhu", Age: 100}) |
|||
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1 |
|||
NOTE Don’t use Save with Model, it’s an Undefined Behavior. |
|||
|
|||
Update single column |
|||
When updating a single column with Update, it needs to have any conditions or it will raise error ErrMissingWhereClause, checkout Block Global Updates for details. |
|||
When using the Model method and its value has a primary value, the primary key will be used to build the condition, for example: |
|||
|
|||
// Update with conditions |
|||
db.Model(&User{}).Where("active = ?", true).Update("name", "hello") |
|||
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true; |
|||
|
|||
// User's ID is `111`: |
|||
db.Model(&user).Update("name", "hello") |
|||
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; |
|||
|
|||
// Update with conditions and model value |
|||
db.Model(&user).Where("active = ?", true).Update("name", "hello") |
|||
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; |
|||
Updates multiple columns |
|||
Updates supports updating with struct or map[string]interface{}, when updating with struct it will only update non-zero fields by default |
|||
|
|||
// Update attributes with `struct`, will only update non-zero fields |
|||
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false}) |
|||
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; |
|||
|
|||
// Update attributes with `map` |
|||
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) |
|||
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; |
|||
NOTE When updating with struct, GORM will only update non-zero fields. You might want to use map to update attributes or use Select to specify fields to update |
|||
|
|||
Update Selected Fields |
|||
If you want to update selected fields or ignore some fields when updating, you can use Select, Omit |
|||
|
|||
// Select with Map |
|||
// User's ID is `111`: |
|||
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) |
|||
// UPDATE users SET name='hello' WHERE id=111; |
|||
|
|||
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) |
|||
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; |
|||
|
|||
// Select with Struct (select zero value fields) |
|||
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0}) |
|||
// UPDATE users SET name='new_name', age=0 WHERE id=111; |
|||
|
|||
// Select all fields (select all fields include zero value fields) |
|||
db.Model(&user).Select("\*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0}) |
|||
|
|||
// Select all fields but omit Role (select all fields include zero value fields) |
|||
db.Model(&user).Select("\*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0}) |
|||
Update Hooks |
|||
GORM allows the hooks BeforeSave, BeforeUpdate, AfterSave, AfterUpdate. Those methods will be called when updating a record, refer Hooks for details |
|||
|
|||
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { |
|||
if u.Role == "admin" { |
|||
return errors.New("admin user not allowed to update") |
|||
} |
|||
return |
|||
} |
|||
Batch Updates |
|||
If we haven’t specified a record having a primary key value with Model, GORM will perform a batch update |
|||
|
|||
// Update with struct |
|||
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18}) |
|||
// UPDATE users SET name='hello', age=18 WHERE role = 'admin'; |
|||
|
|||
// Update with map |
|||
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) |
|||
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); |
|||
Block Global Updates |
|||
If you perform a batch update without any conditions, GORM WON’T run it and will return ErrMissingWhereClause error by default |
|||
|
|||
You have to use some conditions or use raw SQL or enable the AllowGlobalUpdate mode, for example: |
|||
|
|||
db.Model(&User{}).Update("name", "jinzhu").Error // gorm.ErrMissingWhereClause |
|||
|
|||
db.Model(&User{}).Where("1 = 1").Update("name", "jinzhu") |
|||
// UPDATE users SET `name` = "jinzhu" WHERE 1=1 |
|||
|
|||
db.Exec("UPDATE users SET name = ?", "jinzhu") |
|||
// UPDATE users SET name = "jinzhu" |
|||
|
|||
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "jinzhu") |
|||
// UPDATE users SET `name` = "jinzhu" |
|||
Updated Records Count |
|||
Get the number of rows affected by a update |
|||
|
|||
// Get updated records count with `RowsAffected` |
|||
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18}) |
|||
// UPDATE users SET name='hello', age=18 WHERE role = 'admin'; |
|||
|
|||
result.RowsAffected // returns updated records count |
|||
result.Error // returns updating error |
|||
Advanced |
|||
Update with SQL Expression |
|||
GORM allows updating a column with a SQL expression, e.g: |
|||
|
|||
// product's ID is `3` |
|||
db.Model(&product).Update("price", gorm.Expr("price _ ? + ?", 2, 100)) |
|||
// UPDATE "products" SET "price" = price _ 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3; |
|||
|
|||
db.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price _ ? + ?", 2, 100)}) |
|||
// UPDATE "products" SET "price" = price _ 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3; |
|||
|
|||
db.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) |
|||
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3; |
|||
|
|||
db.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) |
|||
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1; |
|||
And GORM also allows updating with SQL Expression/Context Valuer with Customized Data Types, e.g: |
|||
|
|||
// Create from customized data type |
|||
type Location struct { |
|||
X, Y int |
|||
} |
|||
|
|||
func (loc Location) GormValue(ctx context.Context, db \*gorm.DB) clause.Expr { |
|||
return clause.Expr{ |
|||
SQL: "ST_PointFromText(?)", |
|||
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)}, |
|||
} |
|||
} |
|||
|
|||
db.Model(&User{ID: 1}).Updates(User{ |
|||
Name: "jinzhu", |
|||
Location: Location{X: 100, Y: 100}, |
|||
}) |
|||
// UPDATE `user_with_points` SET `name`="jinzhu",`location`=ST_PointFromText("POINT(100 100)") WHERE `id` = 1 |
|||
Update from SubQuery |
|||
Update a table by using SubQuery |
|||
|
|||
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id")) |
|||
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id); |
|||
|
|||
db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id")) |
|||
|
|||
db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")}) |
|||
Without Hooks/Time Tracking |
|||
If you want to skip Hooks methods and don’t track the update time when updating, you can use UpdateColumn, UpdateColumns, it works like Update, Updates |
|||
|
|||
// Update single column |
|||
db.Model(&user).UpdateColumn("name", "hello") |
|||
// UPDATE users SET name='hello' WHERE id = 111; |
|||
|
|||
// Update multiple columns |
|||
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) |
|||
// UPDATE users SET name='hello', age=18 WHERE id = 111; |
|||
|
|||
// Update selected columns |
|||
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello", Age: 0}) |
|||
// UPDATE users SET name='hello', age=0 WHERE id = 111; |
|||
Returning Data From Modified Rows |
|||
Returning changed data only works for databases which support Returning, for example: |
|||
|
|||
// return all columns |
|||
var users []User |
|||
db.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary _ ?", 2)) |
|||
// UPDATE `users` SET `salary`=salary _ 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING \* |
|||
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}} |
|||
|
|||
// return specified columns |
|||
db.Model(&users).Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary _ ?", 2)) |
|||
// UPDATE `users` SET `salary`=salary _ 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING `name`, `salary` |
|||
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}} |
|||
Check Field has changed? |
|||
GORM provides the Changed method which could be used in Before Update Hooks, it will return whether the field has changed or not. |
|||
|
|||
The Changed method only works with methods Update, Updates, and it only checks if the updating value from Update / Updates equals the model value. It will return true if it is changed and not omitted |
|||
|
|||
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { |
|||
// if Role changed |
|||
if tx.Statement.Changed("Role") { |
|||
return errors.New("role not allowed to change") |
|||
} |
|||
|
|||
if tx.Statement.Changed("Name", "Admin") { // if Name or Role changed |
|||
tx.Statement.SetColumn("Age", 18) |
|||
} |
|||
|
|||
// if any fields changed |
|||
if tx.Statement.Changed() { |
|||
tx.Statement.SetColumn("RefreshedAt", time.Now()) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"}) |
|||
// Changed("Name") => true |
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"}) |
|||
// Changed("Name") => false, `Name` not changed |
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{ |
|||
"name": "jinzhu2", "admin": false, |
|||
}) |
|||
// Changed("Name") => false, `Name` not selected to update |
|||
|
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"}) |
|||
// Changed("Name") => true |
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"}) |
|||
// Changed("Name") => false, `Name` not changed |
|||
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"}) |
|||
// Changed("Name") => false, `Name` not selected to update |
|||
Change Updating Values |
|||
To change updating values in Before Hooks, you should use SetColumn unless it is a full update with Save, for example: |
|||
|
|||
func (user *User) BeforeSave(tx *gorm.DB) (err error) { |
|||
if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { |
|||
tx.Statement.SetColumn("EncryptedPassword", pw) |
|||
} |
|||
|
|||
if tx.Statement.Changed("Code") { |
|||
user.Age += 20 |
|||
tx.Statement.SetColumn("Age", user.Age) |
|||
} |
|||
} |
|||
|
|||
db.Model(&user).Update("Name", "jinzhu") |
|||
GitHub tag (latest SemVer) |
@ -0,0 +1,251 @@ |
|||
Create |
|||
Create Record |
|||
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} |
|||
|
|||
result := db.Create(&user) // pass pointer of data to Create |
|||
|
|||
user.ID // returns inserted data's primary key |
|||
result.Error // returns error |
|||
result.RowsAffected // returns inserted records count |
|||
We can also create multiple records with Create(): |
|||
|
|||
users := []\*User{ |
|||
{Name: "Jinzhu", Age: 18, Birthday: time.Now()}, |
|||
{Name: "Jackson", Age: 19, Birthday: time.Now()}, |
|||
} |
|||
|
|||
result := db.Create(users) // pass a slice to insert multiple row |
|||
|
|||
result.Error // returns error |
|||
result.RowsAffected // returns inserted records count |
|||
NOTE You cannot pass a struct to ‘create’, so you should pass a pointer to the data. |
|||
|
|||
Create Record With Selected Fields |
|||
Create a record and assign a value to the fields specified. |
|||
|
|||
db.Select("Name", "Age", "CreatedAt").Create(&user) |
|||
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775") |
|||
Create a record and ignore the values for fields passed to omit. |
|||
|
|||
db.Omit("Name", "Age", "CreatedAt").Create(&user) |
|||
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775") |
|||
Batch Insert |
|||
To efficiently insert large number of records, pass a slice to the Create method. GORM will generate a single SQL statement to insert all the data and backfill primary key values, hook methods will be invoked too. It will begin a transaction when records can be split into multiple batches. |
|||
|
|||
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}} |
|||
db.Create(&users) |
|||
|
|||
for \_, user := range users { |
|||
user.ID // 1,2,3 |
|||
} |
|||
You can specify batch size when creating with CreateInBatches, e.g: |
|||
|
|||
var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}} |
|||
|
|||
// batch size 100 |
|||
db.CreateInBatches(users, 100) |
|||
Batch Insert is also supported when using Upsert and Create With Associations |
|||
|
|||
NOTE initialize GORM with CreateBatchSize option, all INSERT will respect this option when creating record & associations |
|||
|
|||
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ |
|||
CreateBatchSize: 1000, |
|||
}) |
|||
|
|||
db := db.Session(&gorm.Session{CreateBatchSize: 1000}) |
|||
|
|||
users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...} |
|||
|
|||
db.Create(&users) |
|||
// INSERT INTO users xxx (5 batches) |
|||
// INSERT INTO pets xxx (15 batches) |
|||
Create Hooks |
|||
GORM allows user defined hooks to be implemented for BeforeSave, BeforeCreate, AfterSave, AfterCreate. These hook method will be called when creating a record, refer Hooks for details on the lifecycle |
|||
|
|||
func (u *User) BeforeCreate(tx *gorm.DB) (err error) { |
|||
u.UUID = uuid.New() |
|||
|
|||
if u.Role == "admin" { |
|||
return errors.New("invalid role") |
|||
} |
|||
return |
|||
} |
|||
If you want to skip Hooks methods, you can use the SkipHooks session mode, for example: |
|||
|
|||
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user) |
|||
|
|||
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users) |
|||
|
|||
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100) |
|||
Create From Map |
|||
GORM supports create from map[string]interface{} and []map[string]interface{}{}, e.g: |
|||
|
|||
db.Model(&User{}).Create(map[string]interface{}{ |
|||
"Name": "jinzhu", "Age": 18, |
|||
}) |
|||
|
|||
// batch insert from `[]map[string]interface{}{}` |
|||
db.Model(&User{}).Create([]map[string]interface{}{ |
|||
{"Name": "jinzhu_1", "Age": 18}, |
|||
{"Name": "jinzhu_2", "Age": 20}, |
|||
}) |
|||
NOTE When creating from map, hooks won’t be invoked, associations won’t be saved and primary key values won’t be back filled |
|||
|
|||
Create From SQL Expression/Context Valuer |
|||
GORM allows insert data with SQL expression, there are two ways to achieve this goal, create from map[string]interface{} or Customized Data Types, for example: |
|||
|
|||
// Create from map |
|||
db.Model(User{}).Create(map[string]interface{}{ |
|||
"Name": "jinzhu", |
|||
"Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}}, |
|||
}) |
|||
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)")); |
|||
|
|||
// Create from customized data type |
|||
type Location struct { |
|||
X, Y int |
|||
} |
|||
|
|||
// Scan implements the sql.Scanner interface |
|||
func (loc \*Location) Scan(v interface{}) error { |
|||
// Scan a value into struct from database driver |
|||
} |
|||
|
|||
func (loc Location) GormDataType() string { |
|||
return "geometry" |
|||
} |
|||
|
|||
func (loc Location) GormValue(ctx context.Context, db \*gorm.DB) clause.Expr { |
|||
return clause.Expr{ |
|||
SQL: "ST_PointFromText(?)", |
|||
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)}, |
|||
} |
|||
} |
|||
|
|||
type User struct { |
|||
Name string |
|||
Location Location |
|||
} |
|||
|
|||
db.Create(&User{ |
|||
Name: "jinzhu", |
|||
Location: Location{X: 100, Y: 100}, |
|||
}) |
|||
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)")) |
|||
Advanced |
|||
Create With Associations |
|||
When creating some data with associations, if its associations value is not zero-value, those associations will be upserted, and its Hooks methods will be invoked. |
|||
|
|||
type CreditCard struct { |
|||
gorm.Model |
|||
Number string |
|||
UserID uint |
|||
} |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
CreditCard CreditCard |
|||
} |
|||
|
|||
db.Create(&User{ |
|||
Name: "jinzhu", |
|||
CreditCard: CreditCard{Number: "411111111111"} |
|||
}) |
|||
// INSERT INTO `users` ... |
|||
// INSERT INTO `credit_cards` ... |
|||
You can skip saving associations with Select, Omit, for example: |
|||
|
|||
db.Omit("CreditCard").Create(&user) |
|||
|
|||
// skip all associations |
|||
db.Omit(clause.Associations).Create(&user) |
|||
Default Values |
|||
You can define default values for fields with tag default, for example: |
|||
|
|||
type User struct { |
|||
ID int64 |
|||
Name string `gorm:"default:galeone"` |
|||
Age int64 `gorm:"default:18"` |
|||
} |
|||
Then the default value will be used when inserting into the database for zero-value fields |
|||
|
|||
NOTE Any zero value like 0, '', false won’t be saved into the database for those fields defined default value, you might want to use pointer type or Scanner/Valuer to avoid this, for example: |
|||
|
|||
type User struct { |
|||
gorm.Model |
|||
Name string |
|||
Age \*int `gorm:"default:18"` |
|||
Active sql.NullBool `gorm:"default:true"` |
|||
} |
|||
NOTE You have to setup the default tag for fields having default or virtual/generated value in database, if you want to skip a default value definition when migrating, you could use default:(-), for example: |
|||
|
|||
type User struct { |
|||
ID string `gorm:"default:uuid_generate_v3()"` // db func |
|||
FirstName string |
|||
LastName string |
|||
Age uint8 |
|||
FullName string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"` |
|||
} |
|||
NOTE SQLite doesn’t support some records are default values when batch insert. |
|||
See SQLite Insert stmt. For example: |
|||
|
|||
type Pet struct { |
|||
Name string `gorm:"default:cat"` |
|||
} |
|||
|
|||
// In SQLite, this is not supported, so GORM will build a wrong SQL to raise error: |
|||
// INSERT INTO `pets` (`name`) VALUES ("dog"),(DEFAULT) RETURNING `name` |
|||
db.Create(&[]Pet{{Name: "dog"}, {}}) |
|||
A viable alternative is to assign default value to fields in the hook, e.g. |
|||
|
|||
func (p *Pet) BeforeCreate(tx *gorm.DB) (err error) { |
|||
if p.Name == "" { |
|||
p.Name = "cat" |
|||
} |
|||
} |
|||
You can see more info in issues#6335 |
|||
|
|||
When using virtual/generated value, you might need to disable its creating/updating permission, check out Field-Level Permission |
|||
|
|||
Upsert / On Conflict |
|||
GORM provides compatible Upsert support for different databases |
|||
|
|||
import "gorm.io/gorm/clause" |
|||
|
|||
// Do nothing on conflict |
|||
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) |
|||
|
|||
// Update columns to default value on `id` conflict |
|||
db.Clauses(clause.OnConflict{ |
|||
Columns: []clause.Column{{Name: "id"}}, |
|||
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}), |
|||
}).Create(&users) |
|||
// MERGE INTO "users" USING **_ WHEN NOT MATCHED THEN INSERT _** WHEN MATCHED THEN UPDATE SET **_; SQL Server |
|||
// INSERT INTO `users` _** ON DUPLICATE KEY UPDATE \*\*\*; MySQL |
|||
|
|||
// Use SQL expression |
|||
db.Clauses(clause.OnConflict{ |
|||
Columns: []clause.Column{{Name: "id"}}, |
|||
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}), |
|||
}).Create(&users) |
|||
// INSERT INTO `users` \*\*\* ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count)); |
|||
|
|||
// Update columns to new value on `id` conflict |
|||
db.Clauses(clause.OnConflict{ |
|||
Columns: []clause.Column{{Name: "id"}}, |
|||
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), |
|||
}).Create(&users) |
|||
// MERGE INTO "users" USING **_ WHEN NOT MATCHED THEN INSERT _** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server |
|||
// INSERT INTO "users" **_ ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL |
|||
// INSERT INTO `users` _** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age); MySQL |
|||
|
|||
// Update all columns to new value on conflict except primary keys and those columns having default values from sql func |
|||
db.Clauses(clause.OnConflict{ |
|||
UpdateAll: true, |
|||
}).Create(&users) |
|||
// INSERT INTO "users" **_ ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...; |
|||
// INSERT INTO `users` _** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL |
|||
Also checkout FirstOrInit, FirstOrCreate on Advanced Query |
|||
|
|||
Checkout Raw SQL and SQL Builder for more details |
@ -0,0 +1,177 @@ |
|||
Declaring Models |
|||
GORM simplifies database interactions by mapping Go structs to database tables. Understanding how to declare models in GORM is fundamental for leveraging its full capabilities. |
|||
|
|||
Declaring Models |
|||
Models are defined using normal structs. These structs can contain fields with basic Go types, pointers or aliases of these types, or even custom types, as long as they implement the Scanner and Valuer interfaces from the database/sql package |
|||
|
|||
Consider the following example of a User model: |
|||
|
|||
type User struct { |
|||
ID uint // Standard field for the primary key |
|||
Name string // A regular string field |
|||
Email *string // A pointer to a string, allowing for null values |
|||
Age uint8 // An unsigned 8-bit integer |
|||
Birthday *time.Time // A pointer to time.Time, can be null |
|||
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings |
|||
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields |
|||
CreatedAt time.Time // Automatically managed by GORM for creation time |
|||
UpdatedAt time.Time // Automatically managed by GORM for update time |
|||
ignored string // fields that aren't exported are ignored |
|||
} |
|||
In this model: |
|||
|
|||
Basic data types like uint, string, and uint8 are used directly. |
|||
Pointers to types like *string and *time.Time indicate nullable fields. |
|||
sql.NullString and sql.NullTime from the database/sql package are used for nullable fields with more control. |
|||
CreatedAt and UpdatedAt are special fields that GORM automatically populates with the current time when a record is created or updated. |
|||
Non-exported fields (starting with a small letter) are not mapped |
|||
In addition to the fundamental features of model declaration in GORM, it’s important to highlight the support for serialization through the serializer tag. This feature enhances the flexibility of how data is stored and retrieved from the database, especially for fields that require custom serialization logic, See Serializer for a detailed explanation |
|||
|
|||
Conventions |
|||
Primary Key: GORM uses a field named ID as the default primary key for each model. |
|||
|
|||
Table Names: By default, GORM converts struct names to snake_case and pluralizes them for table names. For instance, a User struct becomes users in the database, and a GormUserName becomes gorm_user_names. |
|||
|
|||
Column Names: GORM automatically converts struct field names to snake_case for column names in the database. |
|||
|
|||
Timestamp Fields: GORM uses fields named CreatedAt and UpdatedAt to automatically track the creation and update times of records. |
|||
|
|||
Following these conventions can greatly reduce the amount of configuration or code you need to write. However, GORM is also flexible, allowing you to customize these settings if the default conventions don’t fit your requirements. You can learn more about customizing these conventions in GORM’s documentation on conventions. |
|||
|
|||
gorm.Model |
|||
GORM provides a predefined struct named gorm.Model, which includes commonly used fields: |
|||
|
|||
// gorm.Model definition |
|||
type Model struct { |
|||
ID uint `gorm:"primaryKey"` |
|||
CreatedAt time.Time |
|||
UpdatedAt time.Time |
|||
DeletedAt gorm.DeletedAt `gorm:"index"` |
|||
} |
|||
Embedding in Your Struct: You can embed gorm.Model directly in your structs to include these fields automatically. This is useful for maintaining consistency across different models and leveraging GORM’s built-in conventions, refer Embedded Struct |
|||
|
|||
Fields Included: |
|||
|
|||
ID: A unique identifier for each record (primary key). |
|||
CreatedAt: Automatically set to the current time when a record is created. |
|||
UpdatedAt: Automatically updated to the current time whenever a record is updated. |
|||
DeletedAt: Used for soft deletes (marking records as deleted without actually removing them from the database). |
|||
Advanced |
|||
Field-Level Permission |
|||
Exported fields have all permissions when doing CRUD with GORM, and GORM allows you to change the field-level permission with tag, so you can make a field to be read-only, write-only, create-only, update-only or ignored |
|||
|
|||
NOTE ignored fields won’t be created when using GORM Migrator to create table |
|||
|
|||
type User struct { |
|||
Name string `gorm:"<-:create"` // allow read and create |
|||
Name string `gorm:"<-:update"` // allow read and update |
|||
Name string `gorm:"<-"` // allow read and write (create and update) |
|||
Name string `gorm:"<-:false"` // allow read, disable write permission |
|||
Name string `gorm:"->"` // readonly (disable write permission unless it configured) |
|||
Name string `gorm:"->;<-:create"` // allow read and create |
|||
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db) |
|||
Name string `gorm:"-"` // ignore this field when write and read with struct |
|||
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct |
|||
Name string `gorm:"-:migration"` // ignore this field when migrate with struct |
|||
} |
|||
Creating/Updating Time/Unix (Milli/Nano) Seconds Tracking |
|||
GORM use CreatedAt, UpdatedAt to track creating/updating time by convention, and GORM will set the current time when creating/updating if the fields are defined |
|||
|
|||
To use fields with a different name, you can configure those fields with tag autoCreateTime, autoUpdateTime |
|||
|
|||
If you prefer to save UNIX (milli/nano) seconds instead of time, you can simply change the field’s data type from time.Time to int |
|||
|
|||
type User struct { |
|||
CreatedAt time.Time // Set to current time if it is zero on creating |
|||
UpdatedAt int // Set to current unix seconds on updating or if it is zero on creating |
|||
Updated int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time |
|||
Updated int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time |
|||
Created int64 `gorm:"autoCreateTime"` // Use unix seconds as creating time |
|||
} |
|||
Embedded Struct |
|||
For anonymous fields, GORM will include its fields into its parent struct, for example: |
|||
|
|||
type Author struct { |
|||
Name string |
|||
Email string |
|||
} |
|||
|
|||
type Blog struct { |
|||
Author |
|||
ID int |
|||
Upvotes int32 |
|||
} |
|||
// equals |
|||
type Blog struct { |
|||
ID int64 |
|||
Name string |
|||
Email string |
|||
Upvotes int32 |
|||
} |
|||
For a normal struct field, you can embed it with the tag embedded, for example: |
|||
|
|||
type Author struct { |
|||
Name string |
|||
Email string |
|||
} |
|||
|
|||
type Blog struct { |
|||
ID int |
|||
Author Author `gorm:"embedded"` |
|||
Upvotes int32 |
|||
} |
|||
// equals |
|||
type Blog struct { |
|||
ID int64 |
|||
Name string |
|||
Email string |
|||
Upvotes int32 |
|||
} |
|||
And you can use tag embeddedPrefix to add prefix to embedded fields’ db name, for example: |
|||
|
|||
type Blog struct { |
|||
ID int |
|||
Author Author `gorm:"embedded;embeddedPrefix:author_"` |
|||
Upvotes int32 |
|||
} |
|||
// equals |
|||
type Blog struct { |
|||
ID int64 |
|||
AuthorName string |
|||
AuthorEmail string |
|||
Upvotes int32 |
|||
} |
|||
Fields Tags |
|||
Tags are optional to use when declaring models, GORM supports the following tags: |
|||
Tags are case insensitive, however camelCase is preferred. If multiple tags are |
|||
used they should be separated by a semicolon (;). Characters that have special |
|||
meaning to the parser can be escaped with a backslash (\) allowing them to be |
|||
used as parameter values. |
|||
|
|||
Tag Name Description |
|||
column column db name |
|||
type column data type, prefer to use compatible general type, e.g: bool, int, uint, float, string, time, bytes, which works for all databases, and can be used with other tags together, like not null, size, autoIncrement… specified database data type like varbinary(8) also supported, when using specified database data type, it needs to be a full database data type, for example: MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT |
|||
serializer specifies serializer for how to serialize and deserialize data into db, e.g: serializer:json/gob/unixtime |
|||
size specifies column data size/length, e.g: size:256 |
|||
primaryKey specifies column as primary key |
|||
unique specifies column as unique |
|||
default specifies column default value |
|||
precision specifies column precision |
|||
scale specifies column scale |
|||
not null specifies column as NOT NULL |
|||
autoIncrement specifies column auto incrementable |
|||
autoIncrementIncrement auto increment step, controls the interval between successive column values |
|||
embedded embed the field |
|||
embeddedPrefix column name prefix for embedded fields |
|||
autoCreateTime track current time when creating, for int fields, it will track unix seconds, use value nano/milli to track unix nano/milli seconds, e.g: autoCreateTime:nano |
|||
autoUpdateTime track current time when creating/updating, for int fields, it will track unix seconds, use value nano/milli to track unix nano/milli seconds, e.g: autoUpdateTime:milli |
|||
index create index with options, use same name for multiple fields creates composite indexes, refer Indexes for details |
|||
uniqueIndex same as index, but create uniqued index |
|||
check creates check constraint, eg: check:age > 13, refer Constraints |
|||
<- set field’s write permission, <-:create create-only field, <-:update update-only field, <-:false no write permission, <- create and update permission |
|||
-> set field’s read permission, ->:false no read permission |
|||
|
|||
- ignore this field, - no read/write permission, -:migration no migrate permission, -:all no read/write/migrate permission |
|||
comment add comment for field when migration |
|||
Associations Tags |
|||
GORM allows configure foreign keys, constraints, many2many table through tags for Associations, check out |
@ -0,0 +1,184 @@ |
|||
Delete |
|||
Delete a Record |
|||
When deleting a record, the deleted value needs to have primary key or it will trigger a Batch Delete, for example: |
|||
|
|||
// Email's ID is `10` |
|||
db.Delete(&email) |
|||
// DELETE from emails where id = 10; |
|||
|
|||
// Delete with additional conditions |
|||
db.Where("name = ?", "jinzhu").Delete(&email) |
|||
// DELETE from emails where id = 10 AND name = "jinzhu"; |
|||
Delete with primary key |
|||
GORM allows to delete objects using primary key(s) with inline condition, it works with numbers, check out Query Inline Conditions for details |
|||
|
|||
db.Delete(&User{}, 10) |
|||
// DELETE FROM users WHERE id = 10; |
|||
|
|||
db.Delete(&User{}, "10") |
|||
// DELETE FROM users WHERE id = 10; |
|||
|
|||
db.Delete(&users, []int{1,2,3}) |
|||
// DELETE FROM users WHERE id IN (1,2,3); |
|||
Delete Hooks |
|||
GORM allows hooks BeforeDelete, AfterDelete, those methods will be called when deleting a record, refer Hooks for details |
|||
|
|||
func (u *User) BeforeDelete(tx *gorm.DB) (err error) { |
|||
if u.Role == "admin" { |
|||
return errors.New("admin user not allowed to delete") |
|||
} |
|||
return |
|||
} |
|||
Batch Delete |
|||
The specified value has no primary value, GORM will perform a batch delete, it will delete all matched records |
|||
|
|||
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{}) |
|||
// DELETE from emails where email LIKE "%jinzhu%"; |
|||
|
|||
db.Delete(&Email{}, "email LIKE ?", "%jinzhu%") |
|||
// DELETE from emails where email LIKE "%jinzhu%"; |
|||
To efficiently delete large number of records, pass a slice with primary keys to the Delete method. |
|||
|
|||
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}} |
|||
db.Delete(&users) |
|||
// DELETE FROM users WHERE id IN (1,2,3); |
|||
|
|||
db.Delete(&users, "name LIKE ?", "%jinzhu%") |
|||
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3); |
|||
Block Global Delete |
|||
If you perform a batch delete without any conditions, GORM WON’T run it, and will return ErrMissingWhereClause error |
|||
|
|||
You have to use some conditions or use raw SQL or enable AllowGlobalUpdate mode, for example: |
|||
|
|||
db.Delete(&User{}).Error // gorm.ErrMissingWhereClause |
|||
|
|||
db.Delete(&[]User{{Name: "jinzhu1"}, {Name: "jinzhu2"}}).Error // gorm.ErrMissingWhereClause |
|||
|
|||
db.Where("1 = 1").Delete(&User{}) |
|||
// DELETE FROM `users` WHERE 1=1 |
|||
|
|||
db.Exec("DELETE FROM users") |
|||
// DELETE FROM users |
|||
|
|||
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{}) |
|||
// DELETE FROM users |
|||
Returning Data From Deleted Rows |
|||
Return deleted data, only works for database support Returning, for example: |
|||
|
|||
// return all columns |
|||
var users []User |
|||
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users) |
|||
// DELETE FROM `users` WHERE role = "admin" RETURNING \* |
|||
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}} |
|||
|
|||
// return specified columns |
|||
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users) |
|||
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary` |
|||
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}} |
|||
Soft Delete |
|||
If your model includes a gorm.DeletedAt field (which is included in gorm.Model), it will get soft delete ability automatically! |
|||
|
|||
When calling Delete, the record WON’T be removed from the database, but GORM will set the DeletedAt‘s value to the current time, and the data is not findable with normal Query methods anymore. |
|||
|
|||
// user's ID is `111` |
|||
db.Delete(&user) |
|||
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; |
|||
|
|||
// Batch Delete |
|||
db.Where("age = ?", 20).Delete(&User{}) |
|||
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; |
|||
|
|||
// Soft deleted records will be ignored when querying |
|||
db.Where("age = 20").Find(&user) |
|||
// SELECT \* FROM users WHERE age = 20 AND deleted_at IS NULL; |
|||
If you don’t want to include gorm.Model, you can enable the soft delete feature like: |
|||
|
|||
type User struct { |
|||
ID int |
|||
Deleted gorm.DeletedAt |
|||
Name string |
|||
} |
|||
Find soft deleted records |
|||
You can find soft deleted records with Unscoped |
|||
|
|||
db.Unscoped().Where("age = 20").Find(&users) |
|||
// SELECT \* FROM users WHERE age = 20; |
|||
Delete permanently |
|||
You can delete matched records permanently with Unscoped |
|||
|
|||
db.Unscoped().Delete(&order) |
|||
// DELETE FROM orders WHERE id=10; |
|||
Delete Flag |
|||
By default, gorm.Model uses \*time.Time as the value for the DeletedAt field, and it provides other data formats support with plugin gorm.io/plugin/soft_delete |
|||
|
|||
INFO when creating unique composite index for the DeletedAt field, you must use other data format like unix second/flag with plugin gorm.io/plugin/soft_delete‘s help, e.g: |
|||
|
|||
import "gorm.io/plugin/soft_delete" |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string `gorm:"uniqueIndex:udx_name"` |
|||
DeletedAt soft_delete.DeletedAt `gorm:"uniqueIndex:udx_name"` |
|||
} |
|||
Unix Second |
|||
Use unix second as delete flag |
|||
|
|||
import "gorm.io/plugin/soft_delete" |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string |
|||
DeletedAt soft_delete.DeletedAt |
|||
} |
|||
|
|||
// Query |
|||
SELECT \* FROM users WHERE deleted_at = 0; |
|||
|
|||
// Delete |
|||
UPDATE users SET deleted_at = /_ current unix second _/ WHERE ID = 1; |
|||
You can also specify to use milli or nano seconds as the value, for example: |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string |
|||
DeletedAt soft_delete.DeletedAt `gorm:"softDelete:milli"` |
|||
// DeletedAt soft_delete.DeletedAt `gorm:"softDelete:nano"` |
|||
} |
|||
|
|||
// Query |
|||
SELECT \* FROM users WHERE deleted_at = 0; |
|||
|
|||
// Delete |
|||
UPDATE users SET deleted_at = /_ current unix milli second or nano second _/ WHERE ID = 1; |
|||
Use 1 / 0 AS Delete Flag |
|||
import "gorm.io/plugin/soft_delete" |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string |
|||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` |
|||
} |
|||
|
|||
// Query |
|||
SELECT \* FROM users WHERE is_del = 0; |
|||
|
|||
// Delete |
|||
UPDATE users SET is_del = 1 WHERE ID = 1; |
|||
Mixed Mode |
|||
Mixed mode can use 0, 1 or unix seconds to mark data as deleted or not, and save the deleted time at the same time. |
|||
|
|||
type User struct { |
|||
ID uint |
|||
Name string |
|||
DeletedAt time.Time |
|||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt"` // use `1` `0` |
|||
// IsDel soft_delete.DeletedAt `gorm:"softDelete:,DeletedAtField:DeletedAt"` // use `unix second` |
|||
// IsDel soft_delete.DeletedAt `gorm:"softDelete:nano,DeletedAtField:DeletedAt"` // use `unix nano second` |
|||
} |
|||
|
|||
// Query |
|||
SELECT \* FROM users WHERE is_del = 0; |
|||
|
|||
// Delete |
|||
UPDATE users SET is_del = 1, deleted_at = /_ current unix second _/ WHERE ID = 1; |
|||
GitHub tag (latest SemVer) |
@ -0,0 +1,62 @@ |
|||
https://gorm.io/zh_CN/docs |
|||
GORM Guides |
|||
The fantastic ORM library for Golang aims to be developer friendly. |
|||
|
|||
Overview |
|||
Full-Featured ORM |
|||
Associations (Has One, Has Many, Belongs To, Many To Many, Polymorphism, Single-table inheritance) |
|||
Hooks (Before/After Create/Save/Update/Delete/Find) |
|||
Eager loading with Preload, Joins |
|||
Transactions, Nested Transactions, Save Point, RollbackTo to Saved Point |
|||
Context, Prepared Statement Mode, DryRun Mode |
|||
Batch Insert, FindInBatches, Find/Create with Map, CRUD with SQL Expr and Context Valuer |
|||
SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints, Named Argument, SubQuery |
|||
Composite Primary Key, Indexes, Constraints |
|||
Auto Migrations |
|||
Logger |
|||
Extendable, flexible plugin API: Database Resolver (Multiple Databases, Read/Write Splitting) / Prometheus… |
|||
Every feature comes with tests |
|||
Developer Friendly |
|||
Install |
|||
go get -u gorm.io/gorm |
|||
go get -u gorm.io/driver/sqlite |
|||
Quick Start |
|||
package main |
|||
|
|||
import ( |
|||
"gorm.io/gorm" |
|||
"gorm.io/driver/sqlite" |
|||
) |
|||
|
|||
type Product struct { |
|||
gorm.Model |
|||
Code string |
|||
Price uint |
|||
} |
|||
|
|||
func main() { |
|||
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{}) |
|||
if err != nil { |
|||
panic("failed to connect database") |
|||
} |
|||
|
|||
// Migrate the schema |
|||
db.AutoMigrate(&Product{}) |
|||
|
|||
// Create |
|||
db.Create(&Product{Code: "D42", Price: 100}) |
|||
|
|||
// Read |
|||
var product Product |
|||
db.First(&product, 1) // find product with integer primary key |
|||
db.First(&product, "code = ?", "D42") // find product with code D42 |
|||
|
|||
// Update - update product's price to 200 |
|||
db.Model(&product).Update("Price", 200) |
|||
// Update - update multiple fields |
|||
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields |
|||
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) |
|||
|
|||
// Delete - delete product |
|||
db.Delete(&product, 1) |
|||
} |
@ -0,0 +1,364 @@ |
|||
Query |
|||
Retrieving a single object |
|||
GORM provides First, Take, Last methods to retrieve a single object from the database, it adds LIMIT 1 condition when querying the database, and it will return the error ErrRecordNotFound if no record is found. |
|||
|
|||
// Get the first record ordered by primary key |
|||
db.First(&user) |
|||
// SELECT \* FROM users ORDER BY id LIMIT 1; |
|||
|
|||
// Get one record, no specified order |
|||
db.Take(&user) |
|||
// SELECT \* FROM users LIMIT 1; |
|||
|
|||
// Get last record, ordered by primary key desc |
|||
db.Last(&user) |
|||
// SELECT \* FROM users ORDER BY id DESC LIMIT 1; |
|||
|
|||
result := db.First(&user) |
|||
result.RowsAffected // returns count of records found |
|||
result.Error // returns error or nil |
|||
|
|||
// check error ErrRecordNotFound |
|||
errors.Is(result.Error, gorm.ErrRecordNotFound) |
|||
If you want to avoid the ErrRecordNotFound error, you could use Find like db.Limit(1).Find(&user), the Find method accepts both struct and slice data |
|||
|
|||
Using Find without a limit for single object db.Find(&user) will query the full table and return only the first object which is not performant and nondeterministic |
|||
|
|||
The First and Last methods will find the first and last record (respectively) as ordered by primary key. They only work when a pointer to the destination struct is passed to the methods as argument or when the model is specified using db.Model(). Additionally, if no primary key is defined for relevant model, then the model will be ordered by the first field. For example: |
|||
|
|||
var user User |
|||
var users []User |
|||
|
|||
// works because destination struct is passed in |
|||
db.First(&user) |
|||
// SELECT \* FROM `users` ORDER BY `users`.`id` LIMIT 1 |
|||
|
|||
// works because model is specified using `db.Model()` |
|||
result := map[string]interface{}{} |
|||
db.Model(&User{}).First(&result) |
|||
// SELECT \* FROM `users` ORDER BY `users`.`id` LIMIT 1 |
|||
|
|||
// doesn't work |
|||
result := map[string]interface{}{} |
|||
db.Table("users").First(&result) |
|||
|
|||
// works with Take |
|||
result := map[string]interface{}{} |
|||
db.Table("users").Take(&result) |
|||
|
|||
// no primary key defined, results will be ordered by first field (i.e., `Code`) |
|||
type Language struct { |
|||
Code string |
|||
Name string |
|||
} |
|||
db.First(&Language{}) |
|||
// SELECT \* FROM `languages` ORDER BY `languages`.`code` LIMIT 1 |
|||
Retrieving objects with primary key |
|||
Objects can be retrieved using primary key by using Inline Conditions if the primary key is a number. When working with strings, extra care needs to be taken to avoid SQL Injection; check out Security section for details. |
|||
|
|||
db.First(&user, 10) |
|||
// SELECT \* FROM users WHERE id = 10; |
|||
|
|||
db.First(&user, "10") |
|||
// SELECT \* FROM users WHERE id = 10; |
|||
|
|||
db.Find(&users, []int{1,2,3}) |
|||
// SELECT \* FROM users WHERE id IN (1,2,3); |
|||
If the primary key is a string (for example, like a uuid), the query will be written as follows: |
|||
|
|||
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a") |
|||
// SELECT \* FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a"; |
|||
When the destination object has a primary value, the primary key will be used to build the condition, for example: |
|||
|
|||
var user = User{ID: 10} |
|||
db.First(&user) |
|||
// SELECT \* FROM users WHERE id = 10; |
|||
|
|||
var result User |
|||
db.Model(User{ID: 10}).First(&result) |
|||
// SELECT \* FROM users WHERE id = 10; |
|||
NOTE: If you use gorm’s specific field types like gorm.DeletedAt, it will run a different query for retrieving object/s. |
|||
|
|||
type User struct { |
|||
ID string `gorm:"primarykey;size:16"` |
|||
Name string `gorm:"size:24"` |
|||
DeletedAt gorm.DeletedAt `gorm:"index"` |
|||
} |
|||
|
|||
var user = User{ID: 15} |
|||
db.First(&user) |
|||
// SELECT _ FROM `users` WHERE `users`.`id` = '15' AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1 |
|||
Retrieving all objects |
|||
// Get all records |
|||
result := db.Find(&users) |
|||
// SELECT _ FROM users; |
|||
|
|||
result.RowsAffected // returns found records count, equals `len(users)` |
|||
result.Error // returns error |
|||
Conditions |
|||
String Conditions |
|||
// Get first matched record |
|||
db.Where("name = ?", "jinzhu").First(&user) |
|||
// SELECT \* FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1; |
|||
|
|||
// Get all matched records |
|||
db.Where("name <> ?", "jinzhu").Find(&users) |
|||
// SELECT \* FROM users WHERE name <> 'jinzhu'; |
|||
|
|||
// IN |
|||
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users) |
|||
// SELECT \* FROM users WHERE name IN ('jinzhu','jinzhu 2'); |
|||
|
|||
// LIKE |
|||
db.Where("name LIKE ?", "%jin%").Find(&users) |
|||
// SELECT \* FROM users WHERE name LIKE '%jin%'; |
|||
|
|||
// AND |
|||
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users) |
|||
// SELECT \* FROM users WHERE name = 'jinzhu' AND age >= 22; |
|||
|
|||
// Time |
|||
db.Where("updated_at > ?", lastWeek).Find(&users) |
|||
// SELECT \* FROM users WHERE updated_at > '2000-01-01 00:00:00'; |
|||
|
|||
// BETWEEN |
|||
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) |
|||
// SELECT \* FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00'; |
|||
If the object’s primary key has been set, then condition query wouldn’t cover the value of primary key but use it as a ‘and’ condition. For example: |
|||
|
|||
var user = User{ID: 10} |
|||
db.Where("id = ?", 20).First(&user) |
|||
// SELECT \* FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1 |
|||
This query would give record not found Error. So set the primary key attribute such as id to nil before you want to use the variable such as user to get new value from database. |
|||
|
|||
Struct & Map Conditions |
|||
// Struct |
|||
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) |
|||
// SELECT \* FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1; |
|||
|
|||
// Map |
|||
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) |
|||
// SELECT \* FROM users WHERE name = "jinzhu" AND age = 20; |
|||
|
|||
// Slice of primary keys |
|||
db.Where([]int64{20, 21, 22}).Find(&users) |
|||
// SELECT \* FROM users WHERE id IN (20, 21, 22); |
|||
NOTE When querying with struct, GORM will only query with non-zero fields, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions, for example: |
|||
|
|||
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users) |
|||
// SELECT \* FROM users WHERE name = "jinzhu"; |
|||
To include zero values in the query conditions, you can use a map, which will include all key-values as query conditions, for example: |
|||
|
|||
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users) |
|||
// SELECT \* FROM users WHERE name = "jinzhu" AND age = 0; |
|||
For more details, see Specify Struct search fields. |
|||
|
|||
Specify Struct search fields |
|||
When searching with struct, you can specify which particular values from the struct to use in the query conditions by passing in the relevant field name or the dbname to Where(), for example: |
|||
|
|||
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users) |
|||
// SELECT \* FROM users WHERE name = "jinzhu" AND age = 0; |
|||
|
|||
db.Where(&User{Name: "jinzhu"}, "Age").Find(&users) |
|||
// SELECT \* FROM users WHERE age = 0; |
|||
Inline Condition |
|||
Query conditions can be inlined into methods like First and Find in a similar way to Where. |
|||
|
|||
// Get by primary key if it were a non-integer type |
|||
db.First(&user, "id = ?", "string_primary_key") |
|||
// SELECT \* FROM users WHERE id = 'string_primary_key'; |
|||
|
|||
// Plain SQL |
|||
db.Find(&user, "name = ?", "jinzhu") |
|||
// SELECT \* FROM users WHERE name = "jinzhu"; |
|||
|
|||
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20) |
|||
// SELECT \* FROM users WHERE name <> "jinzhu" AND age > 20; |
|||
|
|||
// Struct |
|||
db.Find(&users, User{Age: 20}) |
|||
// SELECT \* FROM users WHERE age = 20; |
|||
|
|||
// Map |
|||
db.Find(&users, map[string]interface{}{"age": 20}) |
|||
// SELECT \* FROM users WHERE age = 20; |
|||
Not Conditions |
|||
Build NOT conditions, works similar to Where |
|||
|
|||
db.Not("name = ?", "jinzhu").First(&user) |
|||
// SELECT \* FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1; |
|||
|
|||
// Not In |
|||
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users) |
|||
// SELECT \* FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2"); |
|||
|
|||
// Struct |
|||
db.Not(User{Name: "jinzhu", Age: 18}).First(&user) |
|||
// SELECT \* FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1; |
|||
|
|||
// Not In slice of primary keys |
|||
db.Not([]int64{1,2,3}).First(&user) |
|||
// SELECT _ FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1; |
|||
Or Conditions |
|||
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) |
|||
// SELECT _ FROM users WHERE role = 'admin' OR role = 'super_admin'; |
|||
|
|||
// Struct |
|||
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users) |
|||
// SELECT \* FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18); |
|||
|
|||
// Map |
|||
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users) |
|||
// SELECT \* FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18); |
|||
For more complicated SQL queries. please also refer to Group Conditions in Advanced Query. |
|||
|
|||
Selecting Specific Fields |
|||
Select allows you to specify the fields that you want to retrieve from database. Otherwise, GORM will select all fields by default. |
|||
|
|||
db.Select("name", "age").Find(&users) |
|||
// SELECT name, age FROM users; |
|||
|
|||
db.Select([]string{"name", "age"}).Find(&users) |
|||
// SELECT name, age FROM users; |
|||
|
|||
db.Table("users").Select("COALESCE(age,?)", 42).Rows() |
|||
// SELECT COALESCE(age,'42') FROM users; |
|||
Also check out Smart Select Fields |
|||
|
|||
Order |
|||
Specify order when retrieving records from the database |
|||
|
|||
db.Order("age desc, name").Find(&users) |
|||
// SELECT \* FROM users ORDER BY age desc, name; |
|||
|
|||
// Multiple orders |
|||
db.Order("age desc").Order("name").Find(&users) |
|||
// SELECT \* FROM users ORDER BY age desc, name; |
|||
|
|||
db.Clauses(clause.OrderBy{ |
|||
Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true}, |
|||
}).Find(&User{}) |
|||
// SELECT \* FROM users ORDER BY FIELD(id,1,2,3) |
|||
Limit & Offset |
|||
Limit specify the max number of records to retrieve |
|||
Offset specify the number of records to skip before starting to return the records |
|||
|
|||
db.Limit(3).Find(&users) |
|||
// SELECT \* FROM users LIMIT 3; |
|||
|
|||
// Cancel limit condition with -1 |
|||
db.Limit(10).Find(&users1).Limit(-1).Find(&users2) |
|||
// SELECT _ FROM users LIMIT 10; (users1) |
|||
// SELECT _ FROM users; (users2) |
|||
|
|||
db.Offset(3).Find(&users) |
|||
// SELECT \* FROM users OFFSET 3; |
|||
|
|||
db.Limit(10).Offset(5).Find(&users) |
|||
// SELECT \* FROM users OFFSET 5 LIMIT 10; |
|||
|
|||
// Cancel offset condition with -1 |
|||
db.Offset(10).Find(&users1).Offset(-1).Find(&users2) |
|||
// SELECT _ FROM users OFFSET 10; (users1) |
|||
// SELECT _ FROM users; (users2) |
|||
Refer to Pagination for details on how to make a paginator |
|||
|
|||
Group By & Having |
|||
type result struct { |
|||
Date time.Time |
|||
Total int |
|||
} |
|||
|
|||
db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result) |
|||
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1 |
|||
|
|||
db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result) |
|||
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group" |
|||
|
|||
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() |
|||
defer rows.Close() |
|||
for rows.Next() { |
|||
... |
|||
} |
|||
|
|||
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() |
|||
defer rows.Close() |
|||
for rows.Next() { |
|||
... |
|||
} |
|||
|
|||
type Result struct { |
|||
Date time.Time |
|||
Total int64 |
|||
} |
|||
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) |
|||
Distinct |
|||
Selecting distinct values from the model |
|||
|
|||
db.Distinct("name", "age").Order("name, age desc").Find(&results) |
|||
Distinct works with Pluck and Count too |
|||
|
|||
Joins |
|||
Specify Joins conditions |
|||
|
|||
type result struct { |
|||
Name string |
|||
Email string |
|||
} |
|||
|
|||
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{}) |
|||
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id |
|||
|
|||
rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() |
|||
for rows.Next() { |
|||
... |
|||
} |
|||
|
|||
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) |
|||
|
|||
// multiple joins with parameter |
|||
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) |
|||
Joins Preloading |
|||
You can use Joins eager loading associations with a single SQL, for example: |
|||
|
|||
db.Joins("Company").Find(&users) |
|||
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`; |
|||
|
|||
// inner join |
|||
db.InnerJoins("Company").Find(&users) |
|||
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` INNER JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`; |
|||
Join with conditions |
|||
|
|||
db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users) |
|||
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true; |
|||
For more details, please refer to Preloading (Eager Loading). |
|||
|
|||
Joins a Derived Table |
|||
You can also use Joins to join a derived table. |
|||
|
|||
type User struct { |
|||
Id int |
|||
Age int |
|||
} |
|||
|
|||
type Order struct { |
|||
UserId int |
|||
FinishedAt \*time.Time |
|||
} |
|||
|
|||
query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id") |
|||
db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results) |
|||
// SELECT `order`.`user_id`,`order`.`finished_at` FROM `order` join (SELECT MAX(order.finished_at) as latest FROM `order` left join user user on order.user_id = user.id WHERE user.age > 18 GROUP BY `order`.`user_id`) q on order.finished_at = q.latest |
|||
Scan |
|||
Scanning results into a struct works similarly to the way we use Find |
|||
|
|||
type Result struct { |
|||
Name string |
|||
Age int |
|||
} |
|||
|
|||
var result Result |
|||
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result) |
|||
|
|||
// Raw SQL |
|||
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result) |
@ -0,0 +1,957 @@ |
|||
api 语法 |
|||
概述 |
|||
api 是 go-zero 自研的领域特性语言(下文称 api 语言 或 api 描述语言),旨在实现人性化的基础描述语言,作为生成 HTTP 服务最基本的描述语言。 |
|||
|
|||
api 领域特性语言包含语法版本,info 块,结构体声明,服务描述等几大块语法组成,其中结构体和 Golang 结构体 语法几乎一样,只是移出了 struct 关键字。 |
|||
|
|||
快速入门 |
|||
本次仅以 demo 形式快速介绍 api 文件的写法,更详细的写法示例可参考 《API 定义完整示例》,详细 api 语法规范可参考 《API 规范》。 |
|||
|
|||
示例 1. 编写最简单的 ping 路由服务 |
|||
syntax = "v1" |
|||
|
|||
// 定义 HTTP 服务 |
|||
service foo { |
|||
get /ping |
|||
} |
|||
示例 2. 编写一个登录接口 api 文件 |
|||
syntax = "v1" |
|||
|
|||
type ( |
|||
// 定义登录接口的请求体 |
|||
LoginReq { |
|||
Username string `json:"username"` |
|||
Password string `json:"password"` |
|||
} |
|||
// 定义登录接口的响应体 |
|||
LoginResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Token string `json:"token"` |
|||
ExpireAt string `json:"expireAt"` |
|||
} |
|||
) |
|||
|
|||
// 定义 HTTP 服务 |
|||
// 微服务名称为 user,生成的代码目录和配置文件将和 user 值相关 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法 |
|||
@handler Login |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/login |
|||
// 请求体为 LoginReq |
|||
// 响应体为 LoginResp,响应体必须有 returns 关键字修饰 |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
} |
|||
|
|||
示例 3. 编写简单的用户服务 api 文件 |
|||
syntax = "v1" |
|||
|
|||
type ( |
|||
// 定义登录接口的 json 请求体 |
|||
LoginReq { |
|||
Username string `json:"username"` |
|||
Password string `json:"password"` |
|||
} |
|||
// 定义登录接口的 json 响应体 |
|||
LoginResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Token string `json:"token"` |
|||
ExpireAt string `json:"expireAt"` |
|||
} |
|||
) |
|||
|
|||
type ( |
|||
// 定义获取用户信息的 json 请求体 |
|||
GetUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
} |
|||
// 定义获取用户信息的 json 响应体 |
|||
GetUserInfoResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
// 定义更新用户信息的 json 请求体 |
|||
UpdateUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
) |
|||
|
|||
// 定义 HTTP 服务 |
|||
// @server 语法块主要用于控制对 HTTP 服务生成时 meta 信息,目前支持功能有: |
|||
// 1. 路由分组 |
|||
// 2. 中间件声明 |
|||
// 3. 路由前缀 |
|||
// 4. 超时配置 |
|||
// 5. jwt 鉴权开关 |
|||
// 所有声明仅对当前 service 中的路由有效 |
|||
@server ( |
|||
// 代表当前 service 代码块下的路由生成代码时都会被放到 login 目录下 |
|||
group: login |
|||
// 定义路由前缀为 "/v1" |
|||
prefix: /v1 |
|||
) |
|||
// 微服务名称为 user,生成的代码目录和配置文件将和 user 值相关 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler login |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/login |
|||
// 请求体为 LoginReq |
|||
// 响应体为 LoginResp,响应体必须有 returns 关键字修饰 |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
} |
|||
|
|||
// @server 语法块主要用于控制对 HTTP 服务生成时 meta 信息,目前支持功能有: |
|||
// 1. 路由分组 |
|||
// 2. 中间件声明 |
|||
// 3. 路由前缀 |
|||
// 4. 超时配置 |
|||
// 5. jwt 鉴权开关 |
|||
// 所有声明仅对当前 service 中的路由有效 |
|||
@server ( |
|||
// 代表当前 service 代码块下的所有路由均需要 jwt 鉴权 |
|||
// goctl 生成代码时会将当前 service 代码块下的接口 |
|||
// 信息添加上 jwt 相关代码,Auth 值为 jwt 密钥,过期 |
|||
// 等信息配置的 golang 结构体名称 |
|||
jwt: Auth |
|||
// 代表当前 service 代码块下的路由生成代码时都会被放到 user 目录下 |
|||
group: user |
|||
// 定义路由前缀为 "/v1" |
|||
prefix: /v1 |
|||
) |
|||
// 注意,定义多个 service 代码块时,服务名称必须一致,因此这里的服务名称必须 |
|||
// 和上文的 service 名称一样,为 user 服务。 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler getUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info |
|||
// 请求体为 GetUserInfoReq |
|||
// 响应体为 GetUserInfoResp,响应体必须有 returns 关键字修饰 |
|||
post /user/info (GetUserInfoReq) returns (GetUserInfoResp) |
|||
|
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler updateUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info/update |
|||
// 请求体为 UpdateUserInfoReq |
|||
// 由于不需要响应体,因此可以忽略不写 |
|||
post /user/info/update (UpdateUserInfoReq) |
|||
|
|||
} |
|||
|
|||
示例 4. 编写带有中间件的 api 服务 |
|||
syntax = "v1" |
|||
|
|||
type GetUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
} |
|||
|
|||
type GetUserInfoResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
|
|||
// @server 语法块主要用于控制对 HTTP 服务生成时 meta 信息,目前支持功能有: |
|||
// 1. 路由分组 |
|||
// 2. 中间件声明 |
|||
// 3. 路由前缀 |
|||
// 4. 超时配置 |
|||
// 5. jwt 鉴权开关 |
|||
// 所有声明仅对当前 service 中的路由有效 |
|||
@server ( |
|||
// 定义一个鉴权控制的中间件,多个中间件以英文逗号,分割,如 Middleware1,Middleware2,中间件按声明顺序执行 |
|||
middleware: AuthInterceptor |
|||
) |
|||
// 定义一个名称为 user 的服务 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler getUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info |
|||
// 请求体为 GetUserInfoReq |
|||
// 响应体为 GetUserInfoResp,响应体必须有 returns 关键字修饰 |
|||
post /user/info (GetUserInfoReq) returns (GetUserInfoResp) |
|||
} |
|||
|
|||
示例 5. 编写带有超时配置的 api 服务 |
|||
syntax = "v1" |
|||
|
|||
type GetUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
} |
|||
|
|||
type GetUserInfoResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
|
|||
// @server 语法块主要用于控制对 HTTP 服务生成时 meta 信息,目前支持功能有: |
|||
// 1. 路由分组 |
|||
// 2. 中间件声明 |
|||
// 3. 路由前缀 |
|||
// 4. 超时配置 |
|||
// 5. jwt 鉴权开关 |
|||
// 所有声明仅对当前 service 中的路由有效 |
|||
@server ( |
|||
// 定义一个超时时长为 3 秒的超时配置,这里可填写为 time.Duration 的字符串形式,详情可参考 |
|||
// https://pkg.go.dev/time#Duration.String |
|||
timeout: 3s |
|||
) |
|||
// 定义一个名称为 user 的服务 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler getUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info |
|||
// 请求体为 GetUserInfoReq |
|||
// 响应体为 GetUserInfoResp,响应体必须有 returns 关键字修饰 |
|||
post /user/info (GetUserInfoReq) returns (GetUserInfoResp) |
|||
} |
|||
|
|||
示例 6. 结构体引用 |
|||
syntax = "v1" |
|||
|
|||
type Base { |
|||
Code int `json:"code"` |
|||
Msg string `json:"msg"` |
|||
} |
|||
|
|||
type UserInfo { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
|
|||
type GetUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
} |
|||
|
|||
type GetUserInfoResp { |
|||
// api 支持匿名结构体嵌套,也支持结构体引用 |
|||
Base |
|||
Data UserInfo `json:"data"` |
|||
// 匿名结构体 |
|||
Nested { |
|||
Foo string `json:"foo"` |
|||
} `json:"nested"` |
|||
} |
|||
|
|||
// 定义一个名称为 user 的服务 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler getUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info |
|||
// 请求体为 GetUserInfoReq |
|||
// 响应体为 GetUserInfoResp,响应体必须有 returns 关键字修饰 |
|||
post /user/info (GetUserInfoReq) returns (GetUserInfoResp) |
|||
} |
|||
|
|||
示例 7. 控制最大请求体控制的 api 服务 |
|||
syntax = "v1" |
|||
|
|||
type GetUserInfoReq { |
|||
Id int64 `json:"id"` |
|||
} |
|||
|
|||
type GetUserInfoResp { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Desc string `json:"desc"` |
|||
} |
|||
|
|||
// @server 语法块主要用于控制对 HTTP 服务生成时 meta 信息,目前支持功能有: |
|||
// 1. 路由分组 |
|||
// 2. 中间件声明 |
|||
// 3. 路由前缀 |
|||
// 4. 超时配置 |
|||
// 5. jwt 鉴权开关 |
|||
// 所有声明仅对当前 service 中的路由有效 |
|||
@server ( |
|||
// 定义一个请求体限制在 1MB 以内的请求,goctl >= 1.5.0 版本支持 |
|||
maxBytes: 1048576 |
|||
) |
|||
// 定义一个名称为 user 的服务 |
|||
service user { |
|||
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler |
|||
@handler getUserInfo |
|||
// 定义接口 |
|||
// 请求方法为 post |
|||
// 路由为 /user/info |
|||
// 请求体为 GetUserInfoReq |
|||
// 响应体为 GetUserInfoResp,响应体必须有 returns 关键字修饰 |
|||
post /user/info (GetUserInfoReq) returns (GetUserInfoResp) |
|||
} |
|||
API 定义完整示例 |
|||
api 示例 |
|||
下文仅展示 api 文件的完整写法和对应语法块的功能说明,如需查看 api 规范定义,可参考 《API 规范》 |
|||
|
|||
syntax = "v1" |
|||
|
|||
info ( |
|||
title: "api 文件完整示例写法" |
|||
desc: "演示如何编写 api 文件" |
|||
author: "keson.an" |
|||
date: "2022 年 12 月 26 日" |
|||
version: "v1" |
|||
) |
|||
|
|||
type UpdateReq { |
|||
Arg1 string `json:"arg1"` |
|||
} |
|||
|
|||
type ListItem { |
|||
Value1 string `json:"value1"` |
|||
} |
|||
|
|||
type LoginReq { |
|||
Username string `json:"username"` |
|||
Password string `json:"password"` |
|||
} |
|||
|
|||
type LoginResp { |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
type FormExampleReq { |
|||
Name string `form:"name"` |
|||
} |
|||
|
|||
type PathExampleReq { |
|||
// path 标签修饰的 id 必须与请求路由中的片段对应,如 |
|||
// id 在 service 语法块的请求路径上一定会有 :id 对应,见下文。 |
|||
ID string `path:"id"` |
|||
} |
|||
|
|||
type PathExampleResp { |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
@server ( |
|||
jwt: Auth // 对当前 Foo 语法块下的所有路由,开启 jwt 认证,不需要则请删除此行 |
|||
prefix: /v1 // 对当前 Foo 语法块下的所有路由,新增 /v1 路由前缀,不需要则请删除此行 |
|||
group: g1 // 对当前 Foo 语法块下的所有路由,路由归并到 g1 目录下,不需要则请删除此行 |
|||
timeout: 3s // 对当前 Foo 语法块下的所有路由进行超时配置,不需要则请删除此行 |
|||
middleware: AuthInterceptor // 对当前 Foo 语法块下的所有路由添加中间件,不需要则请删除此行 |
|||
maxBytes: 1048576 // 对当前 Foo 语法块下的所有路由添加请求体大小控制,单位为 byte,goctl 版本 >= 1.5.0 才支持 |
|||
) |
|||
service Foo { |
|||
// 定义没有请求体和响应体的接口,如 ping |
|||
@handler ping |
|||
get /ping |
|||
|
|||
// 定义只有请求体的接口,如更新信息 |
|||
@handler update |
|||
post /update (UpdateReq) |
|||
|
|||
// 定义只有响应体的结构,如获取全部信息列表 |
|||
@handler list |
|||
get /list returns ([]ListItem) |
|||
|
|||
// 定义有结构体和响应体的接口,如登录 |
|||
@handler login |
|||
post /login (LoginReq) returns (LoginResp) |
|||
|
|||
// 定义表单请求 |
|||
@handler formExample |
|||
post /form/example (FormExampleReq) |
|||
|
|||
// 定义 path 参数 |
|||
@handler pathExample |
|||
get /path/example/:id (PathExampleReq) returns (PathExampleResp) |
|||
|
|||
} |
|||
|
|||
API 规范 |
|||
概述 |
|||
api 是 go-zero 自研的领域特性语言(下文称 api 语言 或 api 描述语言),旨在实现人性化的基础描述语言,作为生成 HTTP 服务最基本的描述语言。 |
|||
|
|||
api 领域特性语言包含语法版本,info 块,结构体声明,服务描述等几大块语法组成,其中结构体和 Golang 结构体 语法几乎一样,只是移除了 struct 关键字。 |
|||
|
|||
目标 |
|||
学习成本低 |
|||
可读性强 |
|||
扩展自由 |
|||
HTTP 服务一键生成 |
|||
编写一个 api 文件,生成多种语言代码服务 |
|||
语法标记符号 |
|||
api 语法是使用 扩展巴科斯范式(EBNF) 来描述的,在扩展巴科斯范式中指定 |
|||
|
|||
Syntax = { Production } . |
|||
Production = production_name "=" [ Expression ] "." . |
|||
Expression = Term { "|" Term } . |
|||
Term = Factor { Factor } . |
|||
Factor = production_name | token [ "…" token ] | Group | Option | Repetition . |
|||
Group = "(" Expression ")" . |
|||
Option = "[" Expression "]" . |
|||
Repetition = "{" Expression "}" . |
|||
Production 由 Term 和如下操作符组成,如下操作符优先级递增: |
|||
|
|||
| alternation |
|||
() grouping |
|||
[] option (0 or 1 times) |
|||
{} repetition (0 to n times) |
|||
形式 a...b 表示从 a 到 b 的一组字符作为替代,如 0...9 代表 0 到 9 的有效数值。 |
|||
|
|||
. 表示 ENBF 的终结符号。 |
|||
|
|||
注意 |
|||
产生式的名称如果为小写,则代表终结 token,驼峰式的产生式名称则为非终结符 token,如: |
|||
|
|||
// 终结 token |
|||
number = "0"..."9" . |
|||
lower_letter = "a"..."z" . |
|||
|
|||
// 非终结 token |
|||
DataType = TypeLit | TypeGroup . |
|||
TypeLit = TypeAlias | TypeStruct . |
|||
源代码表示 |
|||
源代码表示是用来描述 api 语法的最基础元素。 |
|||
|
|||
字符 |
|||
newline = /_ 代表换行符, Unicode 值为 U+000A _/ . |
|||
unicode*char = /* 除换行符 newline 外的其他 Unicode 字符 _/ . |
|||
unicode_letter = /_ 字母 a...z|A...Z Unicode _/ . |
|||
unicode_digit = /_ 数值 0...9 Unicode _/ . |
|||
字母和数字 |
|||
下划线字符 _ (U+005F) 被视为小写字母。 |
|||
|
|||
letter = "A"..."Z" | "a"..."z" | "\_" . |
|||
decimal_digit = "0" … "9" . |
|||
抽象语法树 |
|||
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。 |
|||
|
|||
抽象语法树是代码的树形表示。它们是编译器工作方式的基本组成部分。当编译器转换一些代码时,基本上有以下步骤: |
|||
|
|||
词法分析(Lexical Analysis) |
|||
语法分析(Syntax Analysis) |
|||
代码生成(Code Generation) |
|||
task-grpc-demo-grpcui |
|||
AST 分析过程 |
|||
词法分析 |
|||
词法分析(Lexical Analysis)是计算机科学中将字符序列转换为记号(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称 lexer),也叫扫描器(scanner)。词法分析器一般以函数的形式存在,供语法分析器调用。 |
|||
|
|||
在 api 语言中,词法分析是将字符转换为词法元素序列的过程,其中词法元素包括 注释 和 Token。 |
|||
|
|||
词法元素 |
|||
注释 |
|||
在 api 领域特性语言中有 2 种格式: |
|||
|
|||
单行注释以 // 开始,行尾结束。 |
|||
|
|||
// 这是一个单行注释示例 |
|||
多行注释(文档注释)以 /_ 开始,以第一个 _/ 结束。 |
|||
|
|||
/_这是在同意行内的文档注释_/ |
|||
/_ |
|||
这是在多行的文档注释 |
|||
_/ |
|||
Token |
|||
Token 是组成节点的最基本元素,由 标识符(identifier)、关键字(keyword)、运算符(operator)、标点符号(punctuation)、字面量(literal)组成,空白符(White space)一般由空格(U+0020)、水平制表符(U+0009)、回车符(U+000D)和 换行符(U+000A)组成,在 api 语言中,Token 不包含 运算符(operator)。 |
|||
|
|||
Token 的 Golang 结构体定义为: |
|||
|
|||
type Token struct { |
|||
Type Type |
|||
Text string |
|||
Position Position |
|||
} |
|||
|
|||
type Position struct { |
|||
Filename string |
|||
Line int |
|||
Column int |
|||
} |
|||
如 api 语句 syntax="v1",其词法化后的为: |
|||
|
|||
文本 类型 |
|||
syntax 标识符 |
|||
= 操作符 |
|||
"v1" 字符串 |
|||
ID 标识符 |
|||
ID 标识符一般是结构体,变量,类型等的名称实体,ID 标识符一般有 1 到 n 个字母和数字组成,且开头必须为字母(记住上文提到的 \_ 也被当做小写字母看待),其 EBNF 表示法为: |
|||
|
|||
identifier = letter { letter | unicode_digit } . |
|||
ID 标识符示例: |
|||
|
|||
a |
|||
\_a1 |
|||
GoZero |
|||
有些 ID 标识符是预先定义的,api 沿用了 Golang 预定义 ID 标识符 。 |
|||
|
|||
预定义类型: |
|||
any bool byte comparable |
|||
complex64 complex128 error float32 float64 |
|||
int int8 int16 int32 int64 rune string |
|||
uint uint8 uint16 uint32 uint64 uintptr |
|||
|
|||
预定义常量: |
|||
true false iota |
|||
|
|||
零值: |
|||
nil |
|||
|
|||
预定义函数: |
|||
append cap close complex copy delete imag len |
|||
make new panic print println real recover |
|||
关键字 |
|||
关键字是一些特殊的 ID 标识符,是系统保留字,api 的关键字沿用了 Golang 关键字,结构体中不得使用 Golang 关键字作为标识符。 |
|||
|
|||
Golang 关键字 |
|||
|
|||
break default func interface select |
|||
case defer go map struct |
|||
chan else goto package switch |
|||
const fallthrough if range type |
|||
continue for import return var |
|||
标点符号 |
|||
标点符号可以用于对 Token、表达式做分割、分组,以下是 api 语言中的标点符号: |
|||
|
|||
- , ( ) |
|||
|
|||
* . [ ] |
|||
/ ; { } |
|||
= : , ; |
|||
... |
|||
字符串 |
|||
字符串字面量是由一组字符序列组成的常量。在 api 中沿用了 Golang 的字符串,有 2 种形式: 原始字符串(raw string)和普通符串(双引号字符串)。 |
|||
|
|||
原始字符串的字符序列在两个反引号之间,除反引号外,任何字符都可以出现,如 `foo`; |
|||
|
|||
普通字符串的字符序列在两个双引号之间,除双引号外,任何字符都可以出现,如 "foo"。 |
|||
|
|||
注意 |
|||
在 api 语言中,双引号字符串不支持 \" 来实现字符串转义。 |
|||
|
|||
string_lit = raw_string_lit | interpreted_string_lit . |
|||
raw_string_lit = "`" { unicode_char | newline } "`" . |
|||
interpreted_string_lit = `"` { unicode_value | byte_value } `"` . |
|||
字符串示例: |
|||
|
|||
// 原始字符串 |
|||
`` |
|||
`foo` |
|||
`bar` |
|||
`json:"baz"` |
|||
|
|||
// 普通字符串 |
|||
"" |
|||
"foo" |
|||
"bar" |
|||
语法分析 |
|||
语法分析(Syntax Analysis)又叫语法解析,这个过程是将词法元素转换为树的过程,而语法树一般由节点(Node)、表达式(Expression)、语句(Statement)组成,语法解析的过程除了词汇转换成树外,还需要完成语义分析。 |
|||
|
|||
节点 |
|||
节点(Node)是 Token 的变体,是一个接口类型,是组成表达式、语句的基本元素,其在 Golang 中的结构体定义为: |
|||
|
|||
// Node represents a node in the AST. |
|||
type Node interface { |
|||
// Pos returns the position of the first character belonging to the node. |
|||
Pos() token.Position |
|||
// End returns the position of the first character immediately after the node. |
|||
End() token.Position |
|||
// Format returns the node's text after format. |
|||
Format(...string) string |
|||
// HasHeadCommentGroup returns true if the node has head comment group. |
|||
HasHeadCommentGroup() bool |
|||
// HasLeadingCommentGroup returns true if the node has leading comment group. |
|||
HasLeadingCommentGroup() bool |
|||
// CommentGroup returns the node's head comment group and leading comment group. |
|||
CommentGroup() (head, leading CommentGroup) |
|||
} |
|||
表达式 |
|||
表达式(Expression)是组成语句的基本元素,可以理解为一个句子中的 “短语”,在 api 语言中包含的表达式如下: |
|||
|
|||
数据类型表达式 |
|||
结构体中的 field 表达式 |
|||
key-value 表达式 |
|||
服务声明表达式 |
|||
HTTP 请求/响应体表达式 |
|||
HTTP 路由表达式 |
|||
在 api 中 Golang 的结构体定义为: |
|||
|
|||
// Expr represents an expression in the AST. |
|||
type Expr interface { |
|||
Node |
|||
exprNode() |
|||
} |
|||
语句 |
|||
语句(Statement)是组成抽象语法树的基本元素,抽象语法树可以理解成一篇文章,而语句是组成文章的多条句子,在 api 语言中包含语句如下: |
|||
|
|||
@doc 语句 |
|||
@handler 语句 |
|||
@server 语句 |
|||
HTTP 服务的请求/响应体语句 |
|||
注释语句 |
|||
import 语句 |
|||
info 语句 |
|||
HTTP 路由语句 |
|||
HTTP 服务声明语句 |
|||
syntax 语句 |
|||
结构体语句 |
|||
在 api 中 Golang 的结构体定义为: |
|||
|
|||
// Stmt represents a statement in the AST. |
|||
type Stmt interface { |
|||
Node |
|||
stmtNode() |
|||
} |
|||
代码生成 |
|||
我们一旦有了抽象语法树,就可以通过它来打印或者生成不同的代码了,在 api 抽象语法树行成后,可以支持: |
|||
|
|||
打印 AST |
|||
api 语言格式化 |
|||
Golang HTTP 服务生成 |
|||
Typescript 代码生成 |
|||
Dart 代码生成 |
|||
Kotlin 代码生成 |
|||
除此之外,还可以根据 AST 进行扩展,比如插件: |
|||
|
|||
goctl-go-compact |
|||
goctl-swagger |
|||
goctl-php |
|||
api 语法标记 |
|||
api = SyntaxStmt | InfoStmt | { ImportStmt } | { TypeStmt } | { ServiceStmt } . |
|||
syntax 语句 |
|||
syntax 语句用于标记 api 语言的版本,不同的版本可能语法结构有所不同,随着版本的提升会做不断的优化,当前版本为 v1。 |
|||
|
|||
syntax 的 EBNF 表示为: |
|||
|
|||
SyntaxStmt = "syntax" "=" "v1" . |
|||
syntax 语法写法示例: |
|||
|
|||
syntax = "v1" |
|||
info 语句 |
|||
info 语句是 api 语言的 meta 信息,其仅用于对当前 api 文件进行描述,暂不参与代码生成,其和注释还是有一些区别,注释一般是依附某个 syntax 语句存在,而 info 语句是用于描述整个 api 信息的,当然,不排除在将来会参与到代码生成里面来,info 语句的 EBNF 表示为: |
|||
|
|||
InfoStmt = "info" "(" { InfoKeyValueExpr } ")" . |
|||
InfoKeyValueExpr = InfoKeyLit [ interpreted_string_lit ] . |
|||
InfoKeyLit = identifier ":" . |
|||
info 语句写法示例: |
|||
|
|||
// 不包含 key-value 的 info 块 |
|||
info () |
|||
|
|||
// 包含 key-value 的 info 块 |
|||
info ( |
|||
foo: "bar" |
|||
bar: |
|||
) |
|||
import 语句 |
|||
import 语句是在 api 中引入其他 api 文件的语法块,其支持相对/绝对路径,不支持 package 的设计,其 EBNF 表示为: |
|||
|
|||
ImportStmt = ImportLiteralStmt | ImportGroupStmt . |
|||
ImportLiteralStmt = "import" interpreted_string_lit . |
|||
ImportGroupStmt = "import" "(" { interpreted_string_lit } ")" . |
|||
import 语句写法示例: |
|||
|
|||
// 单行 import |
|||
import "foo" |
|||
import "/path/to/file" |
|||
|
|||
// import 组 |
|||
import () |
|||
import ( |
|||
"bar" |
|||
"relative/to/file" |
|||
) |
|||
数据类型 |
|||
api 中的数据类型基本沿用了 Golang 的数据类型,用于对 rest 服务的请求/响应体结构的描述,其 EBNF 表示为: |
|||
|
|||
TypeStmt = TypeLiteralStmt | TypeGroupStmt . |
|||
TypeLiteralStmt = "type" TypeExpr . |
|||
TypeGroupStmt = "type" "(" { TypeExpr } ")" . |
|||
TypeExpr = identifier [ "=" ] DataType . |
|||
DataType = AnyDataType | ArrayDataType | BaseDataType | |
|||
InterfaceDataType | MapDataType | PointerDataType | |
|||
SliceDataType | StructDataType . |
|||
AnyDataType = "any" . |
|||
ArrayDataType = "[" { decimal_digit } "]" DataType . |
|||
BaseDataType = "bool" | "uint8" | "uint16" | "uint32" | "uint64" | |
|||
"int8" | "int16" | "int32" | "int64" | "float32" | |
|||
"float64" | "complex64" | "complex128" | "string" | "int" | |
|||
"uint" | "uintptr" | "byte" | "rune" | "any" | . |
|||
|
|||
InterfaceDataType = "interface{}" . |
|||
MapDataType = "map" "[" DataType "]" DataType . |
|||
PointerDataType = "\*" DataType . |
|||
SliceDataType = "[" "]" DataType . |
|||
StructDataType = "{" { ElemExpr } "}" . |
|||
ElemExpr = [ ElemNameExpr ] DataType [ Tag ]. |
|||
ElemNameExpr = identifier { "," identifier } . |
|||
Tag = raw_string_lit . |
|||
数据类型写法示例: |
|||
|
|||
// 空结构体 |
|||
type Foo {} |
|||
|
|||
// 单个结构体 |
|||
type Bar { |
|||
Foo int `json:"foo"` |
|||
Bar bool `json:"bar"` |
|||
Baz []string `json:"baz"` |
|||
Qux map[string]string `json:"qux"` |
|||
} |
|||
|
|||
type Baz { |
|||
Bar `json:"baz"` |
|||
Array [3]int `json:"array"` |
|||
// 结构体内嵌 goctl 1.6.8 版本支持 |
|||
Qux { |
|||
Foo string `json:"foo"` |
|||
Bar bool `json:"bar"` |
|||
} `json:"baz"` |
|||
} |
|||
|
|||
// 空结构体组 |
|||
type () |
|||
|
|||
// 结构体组 |
|||
type ( |
|||
Int int |
|||
Integer = int |
|||
Bar { |
|||
Foo int `json:"foo"` |
|||
Bar bool `json:"bar"` |
|||
Baz []string `json:"baz"` |
|||
Qux map[string]string `json:"qux"` |
|||
} |
|||
) |
|||
|
|||
注意 |
|||
不支持 package 设计,如 time.Time。 |
|||
service 语句 |
|||
service 语句是对 HTTP 服务的直观描述,包含请求 handler,请求方法,请求路由,请求体,响应体,jwt 开关,中间件声明等定义。 |
|||
|
|||
其 EBNF 表示为: |
|||
|
|||
ServiceStmt = [ AtServerStmt ] "service" ServiceNameExpr "(" |
|||
{ ServiceItemStmt } ")" . |
|||
ServiceNameExpr = identifier [ "-api" ] . |
|||
@server 语句 |
|||
@server 语句是对一个服务语句的 meta 信息描述,其对应特性包含但不限于: |
|||
|
|||
jwt 开关 |
|||
中间件 |
|||
路由分组 |
|||
路由前缀 |
|||
@server 的 EBNF 表示为: |
|||
|
|||
AtServerStmt = "@server" "(" { AtServerKVExpr } ")" . |
|||
AtServerKVExpr = AtServerKeyLit [ AtServerValueLit ] . |
|||
AtServerKeyLit = identifier ":" . |
|||
AtServerValueLit = PathLit | identifier { "," identifier } . |
|||
PathLit = `"` { "/" { identifier | "-" identifier} } `"` . |
|||
@server 写法示例: |
|||
|
|||
// 空内容 |
|||
@server() |
|||
|
|||
// 有内容 |
|||
@server ( |
|||
// jwt 声明 |
|||
// 如果 key 固定为 “jwt:”,则代表开启 jwt 鉴权声明 |
|||
// value 则为配置文件的结构体名称 |
|||
jwt: Auth |
|||
|
|||
// 路由前缀 |
|||
// 如果 key 固定为 “prefix:” |
|||
// 则代表路由前缀声明,value 则为具体的路由前缀值,字符串中没让必须以 / 开头 |
|||
prefix: /v1 |
|||
|
|||
// 路由分组 |
|||
// 如果 key 固定为 “group:”,则代表路由分组声明 |
|||
// value 则为具体分组名称,在 goctl生成代码后会根据此值进行文件夹分组 |
|||
group: Foo |
|||
|
|||
// 中间件 |
|||
// 如果 key 固定为 middleware:”,则代表中间件声明 |
|||
// value 则为具体中间件函数名称,在 goctl生成代码后会根据此值进生成对应的中间件函数 |
|||
middleware: AuthInterceptor |
|||
|
|||
// 超时控制 |
|||
// 如果 key 固定为 timeout:”,则代表超时配置 |
|||
// value 则为具体中duration,在 goctl生成代码后会根据此值进生成对应的超时配置 |
|||
timeout: 3s |
|||
|
|||
// 其他 key-value,除上述几个内置 key 外,其他 key-value |
|||
// 也可以在作为 annotation 信息传递给 goctl 及其插件,但就 |
|||
// 目前来看,goctl 并未使用。 |
|||
foo: bar |
|||
|
|||
) |
|||
服务条目 |
|||
服务条目(ServiceItemStmt)是对单个 HTTP 请求的描述,包括 @doc 语句,handler 语句,路由语句信息,其 EBNF 表示为: |
|||
|
|||
ServiceItemStmt = [ AtDocStmt ] AtHandlerStmt RouteStmt . |
|||
@doc 语句 |
|||
@doc 语句是对单个路由的 meta 信息描述,一般为 key-value 值,可以传递给 goctl 及其插件来进行扩展生成,其 EBNF 表示为: |
|||
|
|||
AtDocStmt = AtDocLiteralStmt | AtDocGroupStmt . |
|||
AtDocLiteralStmt = "@doc" interpreted_string_lit . |
|||
AtDocGroupStmt = "@doc" "(" { AtDocKVExpr } ")" . |
|||
AtDocKVExpr = AtServerKeyLit interpreted_string_lit . |
|||
AtServerKeyLit = identifier ":" . |
|||
@doc 写法示例: |
|||
|
|||
// 单行 @doc |
|||
@doc "foo" |
|||
|
|||
// 空 @doc 组 |
|||
@doc () |
|||
|
|||
// 有内容的 @doc 组 |
|||
@doc ( |
|||
foo: "bar" |
|||
bar: "baz" |
|||
) |
|||
@handler 语句 |
|||
@handler 语句是对单个路由的 handler 信息控制,主要用于生成 golang http.HandleFunc 的实现转换方法,其 EBNF 表示为: |
|||
|
|||
AtHandlerStmt = "@handler" identifier . |
|||
@handler 写法示例: |
|||
|
|||
@handler foo |
|||
路由语句 |
|||
路由语句是对单此 HTTP 请求的具体描述,包括请求方法,请求路径,请求体,响应体信息,其 EBNF 表示为: |
|||
|
|||
RouteStmt = Method PathExpr [ BodyStmt ] [ "returns" ] [ BodyStmt ]. |
|||
Method = "get" | "head" | "post" | "put" | "patch" | "delete" | |
|||
"connect" | "options" | "trace" . |
|||
PathExpr = "/" identifier { ( "-" identifier ) | ( ":" identifier) } . |
|||
BodyStmt = "(" identifier ")" . |
|||
路由语句写法示例: |
|||
|
|||
// 没有请求体和响应体的写法 |
|||
get /ping |
|||
|
|||
// 只有请求体的写法 |
|||
get /foo (foo) |
|||
|
|||
// 只有响应体的写法 |
|||
post /foo returns (foo) |
|||
|
|||
// 有请求体和响应体的写法 |
|||
post /foo (foo) returns (bar) |
|||
service 写法示例 |
|||
|
|||
// 带 @server 的写法 |
|||
@server ( |
|||
prefix: /v1 |
|||
group: Login |
|||
) |
|||
service user { |
|||
@doc "登录" |
|||
@handler login |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
|
|||
@handler getUserInfo |
|||
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp) |
|||
|
|||
} |
|||
@server ( |
|||
prefix: /v1 |
|||
middleware: AuthInterceptor |
|||
) |
|||
service user { |
|||
@doc "登录" |
|||
@handler login |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
|
|||
@handler getUserInfo |
|||
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp) |
|||
|
|||
} |
|||
|
|||
// 不带 @server 的写法 |
|||
service user { |
|||
@doc "登录" |
|||
@handler login |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
|
|||
@handler getUserInfo |
|||
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp) |
|||
|
|||
} |
|||
|
|||
参数规则 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 go-zero 中已经内置了一些参数校验的规则,接下来我们来看一下 go-zero 中的参数接收/校验规则吧。 |
|||
|
|||
参数接收规则 |
|||
在 api 描述语言中,我们可以通过在 tag 中来声明参数接收规则,目前 go-zero 支持的参数接收规则如下: |
|||
|
|||
接收规则 说明 生效范围 接收 tag 示例 请求示例 |
|||
json json 序列化 请求体&响应体 json:"foo" {"key":"vulue"} |
|||
path 路由参数 请求体 path:"id" /foo/:id |
|||
form post 请求的表单(支持 content-type 为 form-data 和 x-www-form-urlencoded) 参数请求接收标识,get 请求的 query 参数接收标识 请求体 form:"name" GET /search?key=vulue |
|||
header http 请求体接收标识 请求体 header:"Content-Length" origin: https://go-zero.dev |
|||
温馨提示 |
|||
go-zero 中不支持多 tag 来接收参数,即一个字段只能有一个 tag,如下写法可能会导致参数接收不到: |
|||
|
|||
type Foo { |
|||
Name string `json:"name" form:"name"` |
|||
} |
|||
参数校验规则 |
|||
在 api 描述语言中,我们可以通过在 tag 中来声明参数接收规则,除此之外,还支持参数的校验,参数校验的规则仅对 请求体 有效,参数校验的规则写在 tag value 中,目前 go-zero 支持的参数校验规则如下: |
|||
|
|||
接收规则 说明 示例 |
|||
optional 当前字段是可选参数,允许为零值(zero value) `json:"foo,optional"` |
|||
options 当前参数仅可接收的枚举值 `json:"gender,options=foo|bar"` |
|||
default 当前参数默认值 `json:"gender,default=male"` |
|||
range 当前参数数值有效范围,仅对数值有效,写法规则详情见下文温馨提示 `json:"age,range=[0:120]"` |
|||
range 表达式值规则 |
|||
左开右闭区间:(min:max],表示大于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
左闭右开区间:[min:max),表示大于等于 min 小于 max,当 max 缺省时,max 代表数值 0,当 min 缺省时,min 代表无穷大,min 和 max 不能同时缺省 |
|||
闭区间:[min:max],表示大于等于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
开区间:(min:max),表示大于 min 小于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
|
|||
API Import |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 HTTP 服务开发中,我们都是通过 api 描述语言来描述 HTTP 服务,随着业务量的增加,api 文件可能会越来越大,又或者我们有一些公共结构体,如果我们都写在同一个 api 文件中,那么 api 文件将变成非常巨大,不易阅读和维护,我们可以通过 api import 来引入其他 api 文件解决这类问题。 |
|||
|
|||
api 文件引入 |
|||
假设我们 HTTP 服务的响应格式统一为如下 json 格式: |
|||
|
|||
{ |
|||
"code": 0, |
|||
"msg": "success", |
|||
"data": {} |
|||
} |
|||
通过如上 json 可以看出,我们的响应格式中有 code、msg、data 三个字段,其中 code 和 msg 是固定的,data 是可变的,我们可以将其中 2 个字段 code,msg 抽象出来,定义为一个公共的结构体,然后在其他 api 文件中引入这个结构体。 |
|||
|
|||
示例,假设我们有一个用户服务来查询用户信息和修改用户信息,我们可以将 code 和 msg 抽象在 base.api 中,然后 user.api 中复用和定义具体的响应结构体即可。 |
|||
|
|||
base.api |
|||
user.api |
|||
syntax = "v1" |
|||
|
|||
type Base { |
|||
Code int `json:"code"` |
|||
Msg string `json:"msg"` |
|||
} |
|||
温馨提示 |
|||
在 api 描述语言中,没有 package 的概念,所以在引入其他 api 文件时,需要使用相对路径,如上面示例中的 import "base.api",如果是在同一个目录下,亦可以使用 import "./base.api"。 import 支持相对路径和绝对路径。 |
|||
|
|||
在 api 描述语言中,我们规定将所有 service 语法块声明的 HTTP 服务信息都放在 main api 文件中,抽象结构体放在其他 api 文件中,然后在 main api 文件中引入其他 api 文件,这样可以让 main api 文件更加简洁,易于维护,而被引入的 api 文件中不允许出现 service 语法块,否则会报错。 |
|||
|
|||
特别注意:api 引入不支持循环引入!!! |
@ -0,0 +1,46 @@ |
|||
# 常用命令 |
|||
|
|||
## api demo 代码生成 |
|||
|
|||
```bash |
|||
# 创建工作空间并进入该目录 |
|||
$ mkdir -p ~/workspace/api && cd ~/workspace/api |
|||
# 执行指令生成 demo 服务 |
|||
$ goctl api new demo |
|||
Done. |
|||
``` |
|||
|
|||
## gRPC demo 代码生成 |
|||
|
|||
```bash |
|||
# 创建工作空间并进入该目录 |
|||
$ mkdir -p ~/workspace/rpc && cd ~/workspace/rpc |
|||
# 执行指令生成 demo 服务 |
|||
$ goctl rpc new demo |
|||
Done. |
|||
``` |
|||
|
|||
## mysql 代码生成 |
|||
|
|||
- user.sql |
|||
|
|||
```bash |
|||
CREATE TABLE user ( |
|||
id bigint AUTO_INCREMENT, |
|||
name varchar(255) NULL COMMENT 'The username', |
|||
password varchar(255) NOT NULL DEFAULT '' COMMENT 'The user password', |
|||
mobile varchar(255) NOT NULL DEFAULT '' COMMENT 'The mobile phone number', |
|||
gender char(10) NOT NULL DEFAULT 'male' COMMENT 'gender,male|female|unknown', |
|||
nickname varchar(255) NULL DEFAULT '' COMMENT 'The nickname', |
|||
type tinyint(1) NULL DEFAULT 0 COMMENT 'The user type, 0:normal,1:vip, for test golang keyword', |
|||
create_at timestamp NULL, |
|||
update_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
|||
UNIQUE mobile_index (mobile), |
|||
UNIQUE name_index (name), |
|||
PRIMARY KEY (id) |
|||
) ENGINE = InnoDB COLLATE utf8mb4_general_ci COMMENT 'user table'; |
|||
``` |
|||
|
|||
```bash |
|||
goctl model mysql ddl --src user.sql --dir . |
|||
``` |
@ -0,0 +1,390 @@ |
|||
go-zero 配置概述 |
|||
概述 |
|||
go-zero 提供了一个强大的 conf 包用于加载配置。我们目前支持的 yaml, json, toml 3 种格式的配置文件,go-zero 通过文件后缀会自行加载对应的文件格式。 |
|||
|
|||
如何使用 |
|||
我们使用 github.com/zeromicro/go-zero/core/conf conf 包进行配置的加载。 |
|||
|
|||
第一步我们会定义我们的配置结构体,其中定义我们所有需要的依赖。 |
|||
|
|||
第二步接着根据配置编写我们对应格式的配置文件。 |
|||
|
|||
第三步通过 conf.MustLoad 加载配置。 |
|||
|
|||
具体使用例子: |
|||
|
|||
main.go |
|||
config.yaml |
|||
package main |
|||
|
|||
import ( |
|||
"flag" |
|||
|
|||
"github.com/zeromicro/go-zero/core/conf" |
|||
|
|||
) |
|||
|
|||
type Config struct { |
|||
Host string `json:",default=0.0.0.0"` |
|||
Port int |
|||
} |
|||
|
|||
var f = flag.String("f", "config.yaml", "config file") |
|||
|
|||
func main() { |
|||
flag.Parse() |
|||
var c Config |
|||
conf.MustLoad(\*f, &c) |
|||
println(c.Host) |
|||
} |
|||
我们一般会在程序启动的时候进行配置的加载,同时我们一般也需要定义我们配置所需要的结构体, 在 go-zero 中,我们推荐将所有服务依赖都定义在 config 中,这样以后配置根据 config 就可以查找出所有的依赖。 |
|||
|
|||
我们使用 func MustLoad(path string, v interface{}, opts ...Option) 进行加载配置,path 为配置的路径,v 为结构体。 这个方法会完成配置的加载,如果配置加载失败,整个程序会 fatal 停止掉。 |
|||
|
|||
当然我们也提供了其他的加载方式,例如: |
|||
|
|||
func Load(file string, v interface{}, opts ...Option) error |
|||
其他格式的配置文件 |
|||
我们目前支持的配置格式如下: |
|||
|
|||
json |
|||
yaml | yml |
|||
toml |
|||
我们程序会自动通过文件后缀进行对应格式的加载。 |
|||
|
|||
当然我们在 conf 包中也提供了对应格式二进制数据加载的方法: |
|||
|
|||
func LoadFromJsonBytes(content []byte, v interface{}) error |
|||
|
|||
func LoadFromTomlBytes(content []byte, v interface{}) error |
|||
|
|||
func LoadFromYamlBytes(content []byte, v interface{}) error |
|||
简单示例: |
|||
|
|||
text := []byte(`a: foo |
|||
B: bar`) |
|||
|
|||
var val struct { |
|||
A string |
|||
B string |
|||
} |
|||
\_ = LoadFromYamlBytes(text, &val) |
|||
注意 |
|||
对于有些需要自定义 tag 的,为了方便统一,我们目前所有 tag 均为 json tag。 |
|||
|
|||
大小写不敏感 |
|||
conf 目前已经默认自动支持 key 大小写不敏感,例如对应如下的配置我们都可以解析出来: |
|||
|
|||
Host: "127.0.0.1" |
|||
|
|||
host: "127.0.0.1" |
|||
环境变量 |
|||
目前 conf 配置支持环境变量注入,我们有 2 种方式实现 |
|||
|
|||
1. conf.UseEnv() |
|||
|
|||
var c struct { |
|||
Name string |
|||
} |
|||
|
|||
conf.MustLoad("config.yaml", &c, conf.UseEnv()) |
|||
|
|||
Name: ${SERVER_NAME} |
|||
如上,我们在 Load 时候传入 UseEnv, conf 会自动根据值替换字符串中的${var}或$var 当前环境变量。 |
|||
|
|||
2. env Tag |
|||
注意 env 需要 go-zero v1.4.3 以上版本才支持。 |
|||
|
|||
var c struct { |
|||
Name string `json:",env=SERVER_NAME"` |
|||
} |
|||
|
|||
conf.MustLoad("config.yaml", &c) |
|||
我们可以在 json tag 的后面加上 env=SERVER_NAME 的标签,conf 将会自动去加载对应的环境变量。 |
|||
|
|||
注意 |
|||
我们配置加载的顺序会优先级 env > 配置中的定义 > json tag 中的 default 定义 |
|||
|
|||
tag 校验规则 |
|||
我们可以通过在 tag 中来声明参数接收规则,除此之外,还支持参数的校验,参数校验的规则写在 tag value 中,简单示例如下: |
|||
|
|||
type Config struct { |
|||
Name string // 没有任何 tag,表示配置必填 |
|||
Port int64 `json:",default=8080"` // 如果配置中没有配置,将会初始成 8080 |
|||
Path string `json:",optional"` |
|||
} |
|||
如果我们在 conf 加载的时候,验证没有通过,将会报出来对应的错误。 |
|||
|
|||
目前 go-zero 支持的校验规则如下: |
|||
|
|||
接收规则 说明 示例 |
|||
optional 当前字段是可选参数,允许为零值(zero value) `json:"foo,optional"` |
|||
options 当前参数仅可接收的枚举值 写法 1:竖线\ |
|||
default 当前参数默认值 `json:"gender,default=male"` |
|||
range 当前参数数值有效范围,仅对数值有效,写法规则详情见下文温馨提示 `json:"age,range=[0:120]"` |
|||
env 当前参数从环境变量获取 `json:"mode,env=MODE"` |
|||
range 表达式值规则 |
|||
左开右闭区间:(min:max],表示大于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
左闭右开区间:[min:max),表示大于等于 min 小于 max,当 max 缺省时,max 代表数值 0,当 min 缺省时,min 代表无穷大,min 和 max 不能同时缺省 |
|||
闭区间:[min:max],表示大于等于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
开区间:(min:max),表示大于 min 小于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省 |
|||
更多可以参考 unmarshaler_test.go |
|||
|
|||
inherit 配置继承 |
|||
在我们日常的配置,会出现很多重复的配置,例如 rpcClientConf 中,每个 rpc 都有一个 etcd 的配置,但是我们大部分的情况下 etcd 的配置都是一样的,我们希望可以只用配置一次 etcd 就可以了。 如下的例子 |
|||
|
|||
type Config struct { |
|||
Etcd discov.EtcdConf |
|||
UserRpc zrpc.RpcClientConf |
|||
PortRpc zrpc.RpcClientConf |
|||
OtherRpc zrpc.RpcClientConf |
|||
} |
|||
|
|||
const str = ` |
|||
Etcd: |
|||
Key: rpcServer" |
|||
Hosts: - "127.0.0.1:6379" - "127.0.0.1:6377" - "127.0.0.1:6376" |
|||
|
|||
UserRpc: |
|||
Etcd: |
|||
Key: UserRpc |
|||
Hosts: - "127.0.0.1:6379" - "127.0.0.1:6377" - "127.0.0.1:6376" |
|||
|
|||
PortRpc: |
|||
Etcd: |
|||
Key: PortRpc |
|||
Hosts: - "127.0.0.1:6379" - "127.0.0.1:6377" - "127.0.0.1:6376" |
|||
|
|||
OtherRpc: |
|||
Etcd: |
|||
Key: OtherRpc |
|||
Hosts: - "127.0.0.1:6379" - "127.0.0.1:6377" - "127.0.0.1:6376" |
|||
` |
|||
|
|||
我们必须为每个 Etcd 都要加上 Hosts 等基础配置。 |
|||
|
|||
但是如果我们使用了 inherit 的 tag 定义,使用的方式在 tag 中加上 inherit。如下: |
|||
|
|||
// A RpcClientConf is a rpc client config. |
|||
RpcClientConf struct { |
|||
Etcd discov.EtcdConf `json:",optional,inherit"` |
|||
.... |
|||
} |
|||
这样我们就可以简化 Etcd 的配置,他会自动向上一层寻找配置。 |
|||
|
|||
const str = ` |
|||
Etcd: |
|||
Key: rpcServer" |
|||
Hosts: - "127.0.0.1:6379" - "127.0.0.1:6377" - "127.0.0.1:6376" |
|||
|
|||
UserRpc: |
|||
Etcd: |
|||
Key: UserRpc |
|||
|
|||
PortRpc: |
|||
Etcd: |
|||
Key: PortRpc |
|||
|
|||
OtherRpc: |
|||
Etcd: |
|||
Key: OtherRpc |
|||
` |
|||
|
|||
基础服务配置 |
|||
service 概述 |
|||
ServiceConf 这个配置是用来表示我们一个独立服务的配置。他被我们的 rest,zrpc 等引用,当然我们也可以自己简单定义自己的服务。 |
|||
|
|||
例如: |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"github.com/zeromicro/go-zero/core/conf" |
|||
"github.com/zeromicro/go-zero/core/service" |
|||
"github.com/zeromicro/go-zero/zrpc" |
|||
) |
|||
|
|||
type JobConfig struct { |
|||
service.ServiceConf |
|||
UserRpc zrpc.RpcClientConf |
|||
} |
|||
|
|||
func main() { |
|||
var c JobConfig |
|||
conf.MustLoad("config.yaml", &c) |
|||
|
|||
c.MustSetUp() |
|||
// do your job |
|||
|
|||
} |
|||
|
|||
如上,我们定义了一个 JobConfig,并且在启动的时候初始设置 MustSetup,这样我们就可以启动了一个服务 service,里面自动集成了 Metrics,Prometheus,Trace,DevServer,Log 等能力。 |
|||
|
|||
参数定义 |
|||
ServiceConf 配置定义如下: |
|||
|
|||
// A ServiceConf is a service config. |
|||
type ServiceConf struct { |
|||
Name string |
|||
Log logx.LogConf |
|||
Mode string `json:",default=pro,options=dev|test|rt|pre|pro"` |
|||
MetricsUrl string `json:",optional"` |
|||
// Deprecated: please use DevServer |
|||
Prometheus prometheus.Config `json:",optional"` |
|||
Telemetry trace.Config `json:",optional"` |
|||
DevServer devserver.Config `json:",optional"` |
|||
} |
|||
参数 类型 默认值 说明 枚举值 |
|||
Name string - 定义服务的名称,会出现在 log 和 tracer 中 |
|||
Log logx.LogConf - 参考 log |
|||
Mode string pro 服务的环境,目前我们预定义了 dev。在 dev 环境我们会开启反射 dev,test,rt,pre, pro |
|||
MetricsUrl string 空 打点上报,我们会将一些 metrics 上报到对应的地址,如果为空,则不上报 |
|||
Prometheus prometheus.Config - 参考 Prometheus.md |
|||
Telemetry trace.Config - 参考 trace.md |
|||
DevServer devserver.Config - go-zero 版本 v1.4.3 及以上支持 |
|||
|
|||
日志配置 |
|||
log 概述 |
|||
LogConf 用于我们 log 相关的配置,logx.MustSetup 提供了我们日志的基础配置能力,简单使用方式如下: |
|||
|
|||
var c logx.LogConf |
|||
logx.MustSetup(c) |
|||
logx.Info(context.Background(), "log") |
|||
// do your job |
|||
我们 log 被 serviceConf 引用,他会在服务启动的时候自动初始化完成。 |
|||
|
|||
参数定义 |
|||
LogConf 配置定义如下: |
|||
|
|||
package logx |
|||
|
|||
// A LogConf is a logging config. |
|||
type LogConf struct { |
|||
ServiceName string `json:",optional"` |
|||
Mode string `json:",default=console,options=[console,file,volume]"` |
|||
Encoding string `json:",default=json,options=[json,plain]"` |
|||
TimeFormat string `json:",optional"` |
|||
Path string `json:",default=logs"` |
|||
Level string `json:",default=info,options=[debug,info,error,severe]"` |
|||
MaxContentLength uint32 `json:",optional"` |
|||
Compress bool `json:",optional"` |
|||
Stat bool `json:",default=true"` // go-zero 版本 >= 1.5.0 才支持 |
|||
KeepDays int `json:",optional"` |
|||
StackCooldownMillis int `json:",default=100"` |
|||
// MaxBackups represents how many backup log files will be kept. 0 means all files will be kept forever. |
|||
// Only take effect when RotationRuleType is `size`. |
|||
// Even thougth `MaxBackups` sets 0, log files will still be removed |
|||
// if the `KeepDays` limitation is reached. |
|||
MaxBackups int `json:",default=0"` |
|||
// MaxSize represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. |
|||
// Only take effect when RotationRuleType is `size` |
|||
MaxSize int `json:",default=0"` |
|||
// RotationRuleType represents the type of log rotation rule. Default is `daily`. |
|||
// daily: daily rotation. |
|||
// size: size limited rotation. |
|||
Rotation string `json:",default=daily,options=[daily,size]"` |
|||
// FileTimeFormat represents the time format for file name, default is `2006-01-02T15:04:05.000Z07:00`. |
|||
FileTimeFormat string `json:",optional"` |
|||
} |
|||
|
|||
参数 类型 默认值 说明 枚举值 |
|||
ServiceName string 服务名称 |
|||
Mode string console 日志打印模式,console 控制台 file, console |
|||
Encoding string json 日志格式, json 格式 或者 plain 纯文本 json, plain |
|||
TimeFormat string 日期格式化 |
|||
Path string logs 日志在文件输出模式下,日志输出路径 |
|||
Level string info 日志输出级别 debug,info,error,severe |
|||
MaxContentLength uint32 0 日志长度限制,打印单个日志的时候会对日志进行裁剪,只有对 content 进行裁剪 |
|||
Compress bool false 是否压缩日志 |
|||
Stat bool true 是否开启 stat 日志,go-zero 版本大于等于 1.5.0 才支持 |
|||
KeepDays int 0 日志保留天数,只有在文件模式才会生效 |
|||
StackCooldownMillis int 100 堆栈打印冷却时间 |
|||
MaxBackups int 0 文件输出模式,按照大小分割时,最多文件保留个数 |
|||
MaxSize int 0 文件输出模式,按照大小分割时,单个文件大小 |
|||
Rotation string daily 文件分割模式, daily 按日期 daily,size |
|||
FileTimeFormat string 文件名日期格式 |
|||
|
|||
Prometheus 配置 |
|||
prometheus 配置 |
|||
Config Prometheus 相关配置,我们会在进程中启动启动一个 prometheus 端口。 该配置在 v1.4.3 后已不推荐使用,请使用 https://github.com/zeromicro/go-zero/blob/master/internal/devserver/config.go 替换,详情可参考 基础服务配置 |
|||
|
|||
参数定义 |
|||
// A Config is a prometheus config. |
|||
type Config struct { |
|||
Host string `json:",optional"` |
|||
Port int `json:",default=9101"` |
|||
Path string `json:",default=/metrics"` |
|||
} |
|||
|
|||
参数 类型 默认值 说明 |
|||
Host string 监听地址 |
|||
Port int 9101 监听端口 |
|||
Path string 监听的路径 |
|||
|
|||
Auto Validation |
|||
自动配置验证 |
|||
go-zero 支持通过简单的接口实现来进行自动配置验证。当您需要确保配置值在应用程序启动前满足特定条件时,这个功能特别有用。 |
|||
|
|||
工作原理 |
|||
这项新功能引入了一个在加载后自动检查配置的验证机制。以下是使用方法: |
|||
|
|||
在您的配置结构体中实现 Validator 接口: |
|||
type YourConfig struct { |
|||
Name string |
|||
MaxUsers int |
|||
} |
|||
|
|||
// 实现 Validator 接口 |
|||
func (c YourConfig) Validate() error { |
|||
if len(c.Name) == 0 { |
|||
return errors.New("name 不能为空") |
|||
} |
|||
if c.MaxUsers <= 0 { |
|||
return errors.New("max users 必须为正数") |
|||
} |
|||
return nil |
|||
} |
|||
像往常一样使用配置 - 验证会自动进行: |
|||
var config YourConfig |
|||
err := conf.Load("config.yaml", &config) |
|||
if err != nil { |
|||
// 这里会捕获加载错误和验证错误 |
|||
log.Fatal(err) |
|||
} |
|||
主要优势 |
|||
早期错误检测:在应用程序启动时立即发现配置错误 |
|||
自定义验证规则:根据应用程序需求定义专属的验证逻辑 |
|||
简洁集成:无需额外的函数调用 - 验证在加载后自动进行 |
|||
类型安全:验证与配置结构体紧密绑定 |
|||
使用示例 |
|||
这里是一个实际的使用示例: |
|||
|
|||
type DatabaseConfig struct { |
|||
Host string |
|||
Port int |
|||
MaxConns int |
|||
} |
|||
|
|||
func (c DatabaseConfig) Validate() error { |
|||
if len(c.Host) == 0 { |
|||
return errors.New("数据库主机地址不能为空") |
|||
} |
|||
if c.Port <= 0 || c.Port > 65535 { |
|||
return errors.New("端口号无效") |
|||
} |
|||
if c.MaxConns <= 0 { |
|||
return errors.New("最大连接数必须为正数") |
|||
} |
|||
return nil |
|||
} |
|||
实现细节 |
|||
该功能通过在加载配置值后检查配置类型是否实现了 Validator 接口来工作。如果实现了该接口,验证就会自动执行。这种方法保持了向后兼容性,同时为新代码提供了增强的功能。 |
|||
|
|||
快速开始 |
|||
要使用这个功能,只需更新到最新版本的 go-zero 即可。不需要额外的依赖。验证功能适用于所有现有的配置加载方式,包括 JSON、YAML 和 TOML 格式。 |
|||
|
|||
最佳实践 |
|||
保持验证规则简单,专注于配置有效性 |
|||
使用清晰的错误消息,准确指出问题所在 |
|||
考虑为所有关键配置值添加验证 |
|||
记住验证在启动时运行 - 避免耗时操作 |
@ -0,0 +1,41 @@ |
|||
# 环境 |
|||
|
|||
## golang 推荐 1.23.5 以上 |
|||
|
|||
```bash |
|||
go version |
|||
``` |
|||
|
|||
## 工具 goctl |
|||
|
|||
- 安装 goctl,goctl 版本关系到生成模版版本 |
|||
|
|||
```bash |
|||
go install github.com/zeromicro/go-zero/tools/goctl@latest |
|||
goctl --version |
|||
``` |
|||
|
|||
## protoc |
|||
|
|||
- 推荐一键安装 |
|||
|
|||
```bash |
|||
goctl env check --install --verbose --force |
|||
goctl env check --verbose |
|||
``` |
|||
|
|||
## go-zero 安装 |
|||
|
|||
```bash |
|||
mkdir <project name> && cd <project name> # project name 为具体值 |
|||
go mod init <module name> # module name 为具体值 |
|||
go get -u github.com/zeromicro/go-zero@latest |
|||
``` |
|||
|
|||
## 数据库 |
|||
|
|||
- mysql 推荐 8.0 以上 |
|||
- redis 推荐 7.0 以上 |
|||
- mongodb 推荐 4.4 以上 |
|||
|
|||
## 其他 |
@ -0,0 +1,508 @@ |
|||
文件风格 |
|||
概述 |
|||
goctl 生成代码支持对文件和文件夹的命名风格进行格式化,可以满足不同开发者平时的阅读习惯,不过在 Golang 中,文件夹和文件命名规范推荐使用全小写风格,详情可参考 Go Style。 |
|||
|
|||
格式化符号 |
|||
在 goctl 代码生成中,可以通过 go 和 zero 组成格式化符号来格式化文件和文件夹的命名风格,如常见的风格格式化符号如下: |
|||
|
|||
lowercase: gozero |
|||
camelcase: goZero |
|||
snakecase: go_zero |
|||
格式化符号表参考 |
|||
假设我们有一个源字符串 welcome_to_go_zero,其参考格式化符号表如下: |
|||
|
|||
格式化符号 格式化后的字符串 说明 |
|||
gozero welcometogozero lower case |
|||
goZero welcomeToGoZero camel case |
|||
go*zero welcome_to_go_zero snake case |
|||
Go#zero Welcome#to#go#zero 自定义分隔符,如分割符 # |
|||
GOZERO WELCOMETOGOZERO upper case |
|||
\_go#zero* _welcome#to#go#zero_ 前缀、后缀及自定义分割符,这里使用 \_ 作为前缀和后缀,使用 # 作为分割符 |
|||
非法格式化符号 |
|||
go |
|||
gOZero |
|||
zero |
|||
goZEro |
|||
goZERo |
|||
goZeRo |
|||
foo |
|||
使用 |
|||
格式化符号是在 goctl 代码生成时使用的,由 style 参数来对格式化符进行控制,如: |
|||
|
|||
# 生成 lower case 文件和目录示例 |
|||
|
|||
$ goctl api new demo --style gozero |
|||
|
|||
# 生成 snake case 文件和目录示例 |
|||
|
|||
$ goctl api new demo --style go_zero |
|||
|
|||
# 生成 camel case 文件和目录示例 |
|||
|
|||
$ goctl api new demo --style goZero |
|||
|
|||
goctl api |
|||
概述 |
|||
goctl api 是 goctl 中的核心模块之一,其可以通过 .api 文件一键快速生成一个 api 服务,如果仅仅是启动一个 go-zero 的 api 演示项目, 你甚至都不用编码,就可以完成一个 api 服务开发及正常运行。在传统的 api 项目中,我们要创建各级目录,编写结构体, 定义路由,添加 logic 文件,这一系列操作,如果按照一条协议的业务需求计算,整个编码下来大概需要 5 ~ 6 分钟才能真正进入业务逻辑的编写, 这还不考虑编写过程中可能产生的各种错误,而随着服务的增多,随着协议的增多,这部分准备工作的时间将成正比上升, 而 goctl api 则可以完全替代你去做这一部分工作,不管你的协议要定多少个,最终来说,只需要花费 10 秒不到即可完成。 |
|||
|
|||
goctl api 指令 |
|||
$ goctl api --help |
|||
Generate api related files |
|||
|
|||
Usage: |
|||
goctl api [flags] |
|||
goctl api [command] |
|||
|
|||
Available Commands: |
|||
dart Generate dart files for provided api in api file |
|||
doc Generate doc files |
|||
format Format api files |
|||
go Generate go files for provided api in api file |
|||
kt Generate kotlin code for provided api file |
|||
new Fast create api service |
|||
plugin Custom file generator |
|||
swagger Generate swagger file from api |
|||
ts Generate ts files for provided api in api file |
|||
validate Validate api file |
|||
|
|||
Flags: |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
-h, --help help for api |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--o string Output a sample api file |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
|
|||
Use "goctl api [command] --help" for more information about a command. |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 模板仓库分支,配合 --remote 使用 |
|||
home string NO ~/.goctl 模板仓库本地路径,优先级低于 --remote |
|||
o string NO 空字符串 输出 api 文件 |
|||
remote string NO 空字符串 模板仓库远程路径 |
|||
dart |
|||
根据 api 文件生成 dart 代码。 |
|||
|
|||
$ goctl api dart --help |
|||
Generate dart files for provided api in api file |
|||
|
|||
Usage: |
|||
goctl api dart [flags] |
|||
|
|||
Flags: |
|||
--api string The api file |
|||
--dir string The target dir |
|||
-h, --help help for dart |
|||
--hostname string hostname of the server |
|||
--legacy Legacy generator for flutter v1 |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件 |
|||
dir string YES 空字符串 生成代码输出目录 |
|||
hostname string NO go-zero.dev host 值 |
|||
legacy boolean NO false 是否旧版本 |
|||
doc |
|||
根据 api 文件生成 markdown 文档。 |
|||
|
|||
$ goctl api doc --help |
|||
Generate doc files |
|||
|
|||
Usage: |
|||
goctl api doc [flags] |
|||
|
|||
Flags: |
|||
--dir string The target dir |
|||
-h, --help help for doc |
|||
--o string The output markdown directory |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
dir string YES 空字符串 api 文件所在目录 |
|||
o string NO 当前 work dir 文档输出目录 |
|||
format |
|||
递归格式化目录下的 api 文件。 |
|||
|
|||
$ goctl api format --help |
|||
Format api files |
|||
|
|||
Usage: |
|||
goctl api format [flags] |
|||
|
|||
Flags: |
|||
--declare Use to skip check api types already declare |
|||
--dir string The format target dir |
|||
-h, --help help for format |
|||
--iu Ignore update |
|||
--stdin Use stdin to input api doc content, press "ctrl + d" to send EOF |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
declare boolean NO false 是否检测上下文 |
|||
dir string YES 空字符串 api 所在目录 |
|||
iu - - - 未使用字段,待移出 |
|||
stdin boolean NO false 是否格式化终端输入的 api 内容 |
|||
go |
|||
根据 api 文件生成 Go HTTP 代码。 |
|||
|
|||
$ goctl api go --help |
|||
Generate go files for provided api in api file |
|||
|
|||
Usage: |
|||
goctl api go [flags] |
|||
|
|||
Flags: |
|||
--api string The api file |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
--dir string The target dir |
|||
-h, --help help for go |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
--style string The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md] (default "gozero") |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件路径 |
|||
branch string NO 空字符串 远程模板所在 git 分支名称,仅当 remote 有值时使用 |
|||
dir string NO 当前工作目录 代码输出目录 |
|||
home string NO ${HOME}/.goctl 本地模板文件目录 |
|||
remote string NO 空字符串 远程模板所在 git 仓库地址,当此字段传值时,优先级高于 home 字段值 |
|||
style string NO gozero 输出文件和目录的命名风格格式化符号,详情见 文件风格 |
|||
new |
|||
快速生成 Go HTTP 服务,开发者需要在终端指定服务名称参数,输出目录为当前工作目录。 |
|||
|
|||
$ goctl api new --help |
|||
Fast create api service |
|||
|
|||
Usage: |
|||
goctl api new [flags] |
|||
|
|||
Examples: |
|||
goctl api new [options] service-name |
|||
|
|||
Flags: |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
-h, --help help for new |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
--style string The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md] (default "gozero") |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 远程模板所在 git 分支名称,仅当 remote 有值时使用 |
|||
home string NO ${HOME}/.goctl 本地模板文件目录 |
|||
remote string NO 空字符串 远程模板所在 git 仓库地址,当此字段传值时,优先级高于 home 字段值 |
|||
style string NO gozero 输出文件和目录的命名风格格式化符号,详情见 文件风格 |
|||
温馨提示 |
|||
goctl api new 需要一个终端参数来指定需要生成的服务名称,输出目录为当前工作目录,如 demo 服务生成的指令示例如下: |
|||
|
|||
$ goctl api new demo |
|||
plugin |
|||
goctl api plugin 命令用于引用插件生成代码,开发者需要在终端指定插件名称、参数等信息。 |
|||
|
|||
$ goctl api plugin --help |
|||
Custom file generator |
|||
|
|||
Usage: |
|||
goctl api plugin [flags] |
|||
|
|||
Flags: |
|||
--api string The api file |
|||
--dir string The target dir |
|||
-h, --help help for plugin |
|||
-p, --plugin string The plugin file |
|||
--style string The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件路径 |
|||
dir string NO 当前工作目录 api 文件路径 |
|||
plugin string YES 空字符串 插件可执行文件所在路径,支持本地和 http 文件 |
|||
style string NO gozero 输出文件和目录的命名风格格式化符号,详情见 文件风格 |
|||
插件资源请参考 goctl 插件资源 |
|||
|
|||
swagger |
|||
根据 api 文件生成 swagger 文档,详情可参考 swagger 生成 |
|||
|
|||
温馨提示 |
|||
要求 goctl 版本大等于 1.8.3 |
|||
|
|||
goctl api swagger -h |
|||
Generate swagger file from api |
|||
|
|||
Usage: |
|||
goctl api swagger [flags] |
|||
|
|||
Flags: |
|||
--api string The api file |
|||
--dir string The target dir |
|||
-h, --help help for swagger |
|||
--yaml Generate swagger yaml file, default to json |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件路径 |
|||
dir string NO 当前工作目录 输出目录 |
|||
yaml bool NO false 输出 swagger 为 yaml 格式 |
|||
ts |
|||
根据 api 文件生成 TypeScript 代码。 |
|||
|
|||
$ goctl api ts --help |
|||
Generate ts files for provided api in api file |
|||
|
|||
Usage: |
|||
goctl api ts [flags] |
|||
|
|||
Flags: |
|||
--api string The api file |
|||
--caller string The web api caller |
|||
--dir string The target dir |
|||
-h, --help help for ts |
|||
--unwrap Unwrap the webapi caller for import |
|||
--webapi string The web api file path |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件路径 |
|||
dir string NO 当前工作目录 api 文件路径 |
|||
caller string NO webapi web caller, |
|||
plugin string YES 空字符串 插件可执行文件所在路径,支持本地和 http 文件 |
|||
style string NO gozero 输出文件和目录的命名风格格式化符号,详情见 文件风格 |
|||
validate |
|||
校验 api 文件是否符合规范。 |
|||
|
|||
goctl api validate --help |
|||
Validate api file |
|||
|
|||
Usage: |
|||
goctl api validate [flags] |
|||
|
|||
Flags: |
|||
--api string Validate target api file |
|||
-h, --help help for validate |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
api string YES 空字符串 api 文件路径 |
|||
|
|||
goctl rpc |
|||
概述 |
|||
goctl rpc 是 goctl 中的核心模块之一,其可以通过 .proto 文件一键快速生成一个 rpc 服务,如果仅仅是启动一个 go-zero 的 rpc 演示项目, 你甚至都不用编码,就可以完成一个 rpc 服务开发及正常运行。在传统的 rpc 项目中,我们要创建各级目录,编写结构体, 定义路由,添加 logic 文件,这一系列操作,如果按照一条协议的业务需求计算,整个编码下来大概需要 5 ~ 6 分钟才能真正进入业务逻辑的编写, 这还不考虑编写过程中可能产生的各种错误,而随着服务的增多,随着协议的增多,这部分准备工作的时间将成正比上升, 而 goctl rpc 则可以完全替代你去做这一部分工作,不管你的协议要定多少个,最终来说,只需要花费 10 秒不到即可完成。 |
|||
|
|||
goctl rpc 指令 |
|||
$ goctl rpc --help |
|||
Generate rpc code |
|||
|
|||
Usage: |
|||
goctl rpc [flags] |
|||
goctl rpc [command] |
|||
|
|||
Available Commands: |
|||
new Generate rpc demo service |
|||
protoc Generate grpc code |
|||
template Generate proto template |
|||
|
|||
Flags: |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
-h, --help help for rpc |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--o string Output a sample proto file |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
|
|||
Use "goctl rpc [command] --help" for more information about a command. |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 模板仓库分支,配合 --remote 使用 |
|||
home string NO ~/.goctl 模板仓库本地路径,优先级低于 --remote |
|||
o string NO 空字符串 输出 api 文件 |
|||
remote string NO 空字符串 模板仓库远程路径 |
|||
示例:生成 proto 文件 |
|||
|
|||
$ goctl rpc --o greet.proto |
|||
goctl rpc new |
|||
快速生成一个 rpc 服务,其接收一个终端参数来指定服务名称。 |
|||
|
|||
$ goctl rpc new --help |
|||
Generate rpc demo service |
|||
|
|||
Usage: |
|||
goctl rpc new [flags] |
|||
|
|||
Flags: |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
-h, --help help for new |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--idea Whether the command execution environment is from idea plugin. |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
--style string The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] (default "gozero") |
|||
-v, --verbose Enable log output |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 模板仓库分支,配合 --remote 使用 |
|||
home string NO ~/.goctl 模板仓库本地路径,优先级低于 --remote |
|||
idea bool NO false 仅 idea 插件用,终端请忽略此字段 |
|||
remote string NO 空字符串 模板仓库远程路径 |
|||
style string NO gozero 文件命名风格,详情可参考 文件风格 |
|||
示例: |
|||
|
|||
$ goctl rpc new greet |
|||
goctl rpc protoc |
|||
根据 protobufer 文件生成 rpc 服务。 |
|||
|
|||
$ goctl rpc protoc --help |
|||
Generate grpc code |
|||
|
|||
Usage: |
|||
goctl rpc protoc [flags] |
|||
|
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
|
|||
-c, --client Whether to generate rpc client (default true) |
|||
-h, --help help for protoc |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher |
|||
priority |
|||
-m, --multiple Generated in multiple rpc service mode |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher |
|||
priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
--style string The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md] |
|||
-v, --verbose Enable log output |
|||
--zrpc_out string The zrpc output directory |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 模板仓库分支,配合 --remote 使用 |
|||
home string NO ~/.goctl 模板仓库本地路径,优先级低于 --remote |
|||
client bool NO true 是否生成客户端代码 |
|||
multiple bool NO false 是否生成多个 rpc 服务 |
|||
remote string NO 空字符串 模板仓库远程路径 |
|||
style string NO gozero 文件命名风格,详情可参考 文件风格 |
|||
zrpc_out string NO 空字符串 输出目录 |
|||
除了上述参数外,还有支持 protoc 指令的原生参数,详情可参考 Go Generated Code Guide。 |
|||
|
|||
示例: |
|||
|
|||
# 单个 rpc 服务生成示例指令 |
|||
|
|||
$ goctl rpc protoc greet.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=. --client=true |
|||
|
|||
# 多个 rpc 服务生成示例指令 |
|||
|
|||
$ goctl rpc protoc greet.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=. --client=true -m |
|||
tip |
|||
多个 rpc 服务生成示例(rpc 分组)生成效果可参考 服务分组 |
|||
|
|||
小技能 |
|||
goctl rpc protoc 指令比较长,参数很多,其实在理解 protoc 用法的前提下,你可以将指令理解成是如下的形式: |
|||
|
|||
goctl rpc ${protoc 用法} --zrpc_out=${output directory},比如指令 goctl rpc protoc greet.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.,其中 protoc greet.proto --go_out=./pb --go-grpc_out=./pb 完全是 protoc 指令的用法,而 --zrpc_out=. 则是 goctl rpc protoc 指令的的参数。 |
|||
|
|||
注意 |
|||
goctl rpc protoc 指令生成 rpc 服务对 proto 有一些事项须知: |
|||
|
|||
proto 文件中如果有 import 语句,goctl 不会对 import 的 proto 文件进行处理,需要自行手动处理。 |
|||
rpc service 中的请求体和响应体必须是当前 proto 文件中的 message,不能是 import 的 proto 文件中的 message。 |
|||
goctl rpc template |
|||
快速生成一个 proto 模板文件,其接收一个 proto 文件名称参数。 |
|||
|
|||
注意 |
|||
该指令已经废弃,推荐使用 goctl rpc -o 指令。 |
|||
|
|||
$ goctl rpc template --help |
|||
Generate proto template |
|||
|
|||
Usage: |
|||
goctl rpc template [flags] |
|||
|
|||
Flags: |
|||
--branch string The branch of the remote repo, it does work with --remote |
|||
-h, --help help for template |
|||
--home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
--o string Output a sample proto file |
|||
--remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority |
|||
The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
branch string NO 空字符串 模板仓库分支,配合 --remote 使用 |
|||
home string NO ~/.goctl 模板仓库本地路径,优先级低于 --remote |
|||
o string NO 空字符串 输出文件路径 |
|||
remote string NO 空字符串 模板仓库远程路径 |
|||
示例: |
|||
|
|||
$ goctl rpc template -o greet.proto |
|||
|
|||
goctl template |
|||
概述 |
|||
模板(Template)是数据驱动生成的基础,所有的代码(rest api、rpc、model、docker、kube)生成都会依赖模板, 默认情况下,模板生成器会选择内存中的模板进行生成,而对于有模板修改需求的开发者来讲,则需要将模板进行落盘, 从而进行模板修改,在下次代码生成时会加载指定路径下的模板进行生成。 |
|||
|
|||
goctl template 指令 |
|||
$ goctl template --help |
|||
Template operation |
|||
|
|||
Usage: |
|||
goctl template [command] |
|||
|
|||
Available Commands: |
|||
clean Clean the all cache templates |
|||
init Initialize the all templates(force update) |
|||
revert Revert the target template to the latest |
|||
update Update template of the target category to the latest |
|||
|
|||
Flags: |
|||
-h, --help help for template |
|||
|
|||
Use "goctl template [command] --help" for more information about a command. |
|||
goctl template clean 指令 |
|||
goctl template clean 用于删除持久化在本地的模板文件。 |
|||
|
|||
$ goctl template clean --help |
|||
Clean the all cache templates |
|||
|
|||
Usage: |
|||
goctl template clean [flags] |
|||
|
|||
Flags: |
|||
-h, --help help for clean |
|||
--home string The goctl home path of the template |
|||
goctl template init 指令 |
|||
goctl template init 用于初始化模板,会将模板文件存储到本地。 |
|||
|
|||
$ goctl template init --help |
|||
Initialize the all templates(force update) |
|||
|
|||
Usage: |
|||
goctl template init [flags] |
|||
|
|||
Flags: |
|||
-h, --help help for init |
|||
--home string The goctl home path of the template |
|||
goctl template revert 指令 |
|||
goctl template revert 用于回滚某个分类下的指定的模板文件。 |
|||
|
|||
$ goctl template revert --help |
|||
Revert the target template to the latest |
|||
|
|||
Usage: |
|||
goctl template revert [flags] |
|||
|
|||
Flags: |
|||
-c, --category string The category of template, enum [api,rpc,model,docker,kube] |
|||
-h, --help help for revert |
|||
--home string The goctl home path of the template |
|||
-n, --name string The target file name of template |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
category string YES 空字符串 模板分类,api|rpc|model|docker|kube |
|||
home string YES ${HOME}/.goctl 模板存储的文件位置 |
|||
name string YES 空字符串 模板文件名称 |
|||
goctl template update 指令 |
|||
goctl template update 用于更新某个分类下的所有模板文件。 |
|||
|
|||
$ goctl template update --help |
|||
Update template of the target category to the latest |
|||
|
|||
Usage: |
|||
goctl template update [flags] |
|||
|
|||
Flags: |
|||
-c, --category string The category of template, enum [api,rpc,model,docker,kube] |
|||
-h, --help help for update |
|||
--home string The goctl home path of the template |
|||
参数字段 参数类型 是否必填 默认值 参数说明 |
|||
category string YES 空字符串 模板分类,api|rpc|model|docker|kube |
|||
home string YES ${HOME}/.goctl 模板存储的文件位置 |
|||
|
|||
goctl upgrade |
|||
概述 |
|||
goctl upgrade 用于升级 goctl 工具。 |
|||
|
|||
goctl upgrade 指令 |
|||
goctl upgrade 会将当前 goctl 工具升级到最新版本。 |
|||
|
|||
$ goctl upgrade --help |
|||
Upgrade goctl to latest version |
|||
|
|||
Usage: |
|||
goctl upgrade [flags] |
|||
|
|||
Flags: |
|||
-h, --help help for upgrade |
@ -0,0 +1,320 @@ |
|||
服务分组 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 HTTP 服务开发中,随着业务的发展,我们的服务接口会越来越多,生成的代码文件(handler,logic,types 文件等)也会越来越多,这时候我们需要将一些生成的代码文件按照一定维度进行文件夹聚合,以便于开发和维护。 |
|||
|
|||
服务分组 |
|||
假设我们有一个用户服务,我们有多个接口如下: |
|||
|
|||
https://example.com/v1/user/login |
|||
https://example.com/v1/user/info |
|||
https://example.com/v1/user/info/update |
|||
https://example.com/v1/user/list |
|||
|
|||
https://example.com/v1/user/role/list |
|||
https://example.com/v1/user/role/update |
|||
https://example.com/v1/user/role/info |
|||
https://example.com/v1/user/role/add |
|||
https://example.com/v1/user/role/delete |
|||
|
|||
https://example.com/v1/user/class/list |
|||
https://example.com/v1/user/class/update |
|||
https://example.com/v1/user/class/info |
|||
https://example.com/v1/user/class/add |
|||
https://example.com/v1/user/class/delete |
|||
我们首先来看一下在不进行分组的情况下 api 语言的写法: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type ( |
|||
UserLoginReq{} |
|||
UserInfoReq{} |
|||
UserLoginResp{} |
|||
UserInfoResp{} |
|||
UserInfoUpdateReq{} |
|||
UserInfoUpdateResp{} |
|||
) |
|||
|
|||
type ( |
|||
UserRoleReq{} |
|||
UserRoleResp{} |
|||
UserRoleUpdateReq{} |
|||
UserRoleUpdateResp{} |
|||
UserRoleAddReq{} |
|||
UserRoleAddResp{} |
|||
UserRoleDeleteReq{} |
|||
UserRoleDeleteResp{} |
|||
) |
|||
|
|||
type ( |
|||
UserClassReq{} |
|||
UserClassResp{} |
|||
UserClassUpdateReq{} |
|||
UserClassUpdateResp{} |
|||
UserClassAddReq{} |
|||
UserClassAddResp{} |
|||
UserClassDeleteReq{} |
|||
UserClassDeleteResp{} |
|||
) |
|||
@server( |
|||
prefix: /v1 |
|||
) |
|||
service user-api { |
|||
@handler UserLogin |
|||
post /user/login (UserLoginReq) returns (UserLoginResp) |
|||
|
|||
@handler UserInfo |
|||
post /user/info (UserInfoReq) returns (UserInfoResp) |
|||
|
|||
@handler UserInfoUpdate |
|||
post /user/info/update (UserInfoUpdateReq) returns (UserInfoUpdateResp) |
|||
|
|||
@handler UserList |
|||
get /user/list returns ([]UserInfoResp) |
|||
|
|||
@handler UserRoleList |
|||
get /user/role/list returns ([]UserRoleResp) |
|||
|
|||
@handler UserRoleUpdate |
|||
get /user/role/update (UserRoleUpdateReq) returns (UserRoleUpdateResp) |
|||
|
|||
@handler UserRoleInfo |
|||
get /user/role/info (UserRoleReq) returns (UserRoleResp) |
|||
|
|||
@handler UserRoleAdd |
|||
get /user/role/add (UserRoleAddReq) returns (UserRoleAddResp) |
|||
|
|||
@handler UserRoleDelete |
|||
get /user/role/delete (UserRoleDeleteReq) returns (UserRoleDeleteResp) |
|||
|
|||
@handler UserClassList |
|||
get /user/class/list returns ([]UserClassResp) |
|||
|
|||
@handler UserClassUpdate |
|||
get /user/class/update (UserClassUpdateReq) returns (UserClassUpdateResp) |
|||
|
|||
@handler UserClassInfo |
|||
get /user/class/info (UserClassReq) returns (UserClassResp) |
|||
|
|||
@handler UserClassAdd |
|||
get /user/class/add (UserClassAddReq) returns (UserClassAddResp) |
|||
|
|||
@handler UserClassDelete |
|||
get /user/class/delete (UserClassDeleteReq) returns (UserClassDeleteResp) |
|||
|
|||
} |
|||
在不分组的情况下生成的代码目录结构如下: |
|||
|
|||
. |
|||
├── etc |
|||
│ └── user-api.yaml |
|||
├── internal |
|||
│ ├── config |
|||
│ │ └── config.go |
|||
│ ├── handler |
|||
│ │ ├── routes.go |
|||
│ │ ├── userclassaddhandler.go |
|||
│ │ ├── userclassdeletehandler.go |
|||
│ │ ├── userclassinfohandler.go |
|||
│ │ ├── userclasslisthandler.go |
|||
│ │ ├── userclassupdatehandler.go |
|||
│ │ ├── userinfohandler.go |
|||
│ │ ├── userinfoupdatehandler.go |
|||
│ │ ├── userlisthandler.go |
|||
│ │ ├── userloginhandler.go |
|||
│ │ ├── userroleaddhandler.go |
|||
│ │ ├── userroledeletehandler.go |
|||
│ │ ├── userroleinfohandler.go |
|||
│ │ ├── userrolelisthandler.go |
|||
│ │ └── userroleupdatehandler.go |
|||
│ ├── logic |
|||
│ │ ├── userclassaddlogic.go |
|||
│ │ ├── userclassdeletelogic.go |
|||
│ │ ├── userclassinfologic.go |
|||
│ │ ├── serclasslistlogic.go |
|||
│ │ ├── userclassupdatelogic.go |
|||
│ │ ├── userinfologic.go |
|||
│ │ ├── userinfoupdatelogic.go |
|||
│ │ ├── userlistlogic.go |
|||
│ │ ├── userloginlogic.go |
|||
│ │ ├── userroleaddlogic.go |
|||
│ │ ├── userroledeletelogic.go |
|||
│ │ ├── userroleinfologic.go |
|||
│ │ ├── userrolelistlogic.go |
|||
│ │ └── userroleupdatelogic.go |
|||
│ ├── svc |
|||
│ │ └── servicecontext.go |
|||
│ └── types |
|||
│ └── types.go |
|||
├── user.api |
|||
└── user.go |
|||
|
|||
7 directories, 35 files |
|||
|
|||
由于我们没有进行分组,所以生成的代码中 handler 和 logic 结构体目录下的文件是全部揉在一起的,这样的目录结构在项目中不太好管理和阅读,接下来我们按照 user,role,class 来进行分组,在 api 语言中,我们可以通过在 @server 语句块中使用 group 关键字来进行分组,分组的语法如下: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type ( |
|||
UserLoginReq {} |
|||
UserInfoReq {} |
|||
UserLoginResp {} |
|||
UserInfoResp {} |
|||
UserInfoUpdateReq {} |
|||
UserInfoUpdateResp {} |
|||
) |
|||
|
|||
type ( |
|||
UserRoleReq {} |
|||
UserRoleResp {} |
|||
UserRoleUpdateReq {} |
|||
UserRoleUpdateResp {} |
|||
UserRoleAddReq {} |
|||
UserRoleAddResp {} |
|||
UserRoleDeleteReq {} |
|||
UserRoleDeleteResp {} |
|||
) |
|||
|
|||
type ( |
|||
UserClassReq {} |
|||
UserClassResp {} |
|||
UserClassUpdateReq {} |
|||
UserClassUpdateResp {} |
|||
UserClassAddReq {} |
|||
UserClassAddResp {} |
|||
UserClassDeleteReq {} |
|||
UserClassDeleteResp {} |
|||
) |
|||
|
|||
@server ( |
|||
prefix: /v1 |
|||
group: user |
|||
) |
|||
service user-api { |
|||
@handler UserLogin |
|||
post /user/login (UserLoginReq) returns (UserLoginResp) |
|||
|
|||
@handler UserInfo |
|||
post /user/info (UserInfoReq) returns (UserInfoResp) |
|||
|
|||
@handler UserInfoUpdate |
|||
post /user/info/update (UserInfoUpdateReq) returns (UserInfoUpdateResp) |
|||
|
|||
@handler UserList |
|||
get /user/list returns ([]UserInfoResp) |
|||
|
|||
} |
|||
|
|||
@server ( |
|||
prefix: /v1 |
|||
group: role |
|||
) |
|||
service user-api { |
|||
@handler UserRoleList |
|||
get /user/role/list returns ([]UserRoleResp) |
|||
|
|||
@handler UserRoleUpdate |
|||
get /user/role/update (UserRoleUpdateReq) returns (UserRoleUpdateResp) |
|||
|
|||
@handler UserRoleInfo |
|||
get /user/role/info (UserRoleReq) returns (UserRoleResp) |
|||
|
|||
@handler UserRoleAdd |
|||
get /user/role/add (UserRoleAddReq) returns (UserRoleAddResp) |
|||
|
|||
@handler UserRoleDelete |
|||
get /user/role/delete (UserRoleDeleteReq) returns (UserRoleDeleteResp) |
|||
|
|||
} |
|||
|
|||
@server ( |
|||
prefix: /v1 |
|||
group: class |
|||
) |
|||
service user-api { |
|||
@handler UserClassList |
|||
get /user/class/list returns ([]UserClassResp) |
|||
|
|||
@handler UserClassUpdate |
|||
get /user/class/update (UserClassUpdateReq) returns (UserClassUpdateResp) |
|||
|
|||
@handler UserClassInfo |
|||
get /user/class/info (UserClassReq) returns (UserClassResp) |
|||
|
|||
@handler UserClassAdd |
|||
get /user/class/add (UserClassAddReq) returns (UserClassAddResp) |
|||
|
|||
@handler UserClassDelete |
|||
get /user/class/delete (UserClassDeleteReq) returns (UserClassDeleteResp) |
|||
|
|||
} |
|||
|
|||
我们再来看一下分组后的代码生成目录结构: |
|||
|
|||
. |
|||
├── etc |
|||
│ └── user-api.yaml |
|||
├── internal |
|||
│ ├── config |
|||
│ │ └── config.go |
|||
│ ├── handler |
|||
│ │ ├── class |
|||
│ │ │ ├── userclassaddhandler.go |
|||
│ │ │ ├── userclassdeletehandler.go |
|||
│ │ │ ├── userclassinfohandler.go |
|||
│ │ │ ├── userclasslisthandler.go |
|||
│ │ │ └── userclassupdatehandler.go |
|||
│ │ ├── role |
|||
│ │ │ ├── userroleaddhandler.go |
|||
│ │ │ ├── userroledeletehandler.go |
|||
│ │ │ ├── userroleinfohandler.go |
|||
│ │ │ ├── userrolelisthandler.go |
|||
│ │ │ └── userroleupdatehandler.go |
|||
│ │ ├── routes.go |
|||
│ │ └── user |
|||
│ │ ├── userinfohandler.go |
|||
│ │ ├── userinfoupdatehandler.go |
|||
│ │ ├── userlisthandler.go |
|||
│ │ └── userloginhandler.go |
|||
│ ├── logic |
|||
│ │ ├── class |
|||
│ │ │ ├── userclassaddlogic.go |
|||
│ │ │ ├── userclassdeletelogic.go |
|||
│ │ │ ├── userclassinfologic.go |
|||
│ │ │ ├── userclasslistlogic.go |
|||
│ │ │ └── userclassupdatelogic.go |
|||
│ │ ├── role |
|||
│ │ │ ├── userroleaddlogic.go |
|||
│ │ │ ├── userroledeletelogic.go |
|||
│ │ │ ├── userroleinfologic.go |
|||
│ │ │ ├── userrolelistlogic.go |
|||
│ │ │ └── userroleupdatelogic.go |
|||
│ │ └── user |
|||
│ │ ├── userinfologic.go |
|||
│ │ ├── userinfoupdatelogic.go |
|||
│ │ ├── userlistlogic.go |
|||
│ │ └── userloginlogic.go |
|||
│ ├── svc |
|||
│ │ └── servicecontext.go |
|||
│ └── types |
|||
│ ├── class.go |
|||
│ ├── role.go |
|||
│ └── user.go |
|||
└── user.go |
|||
|
|||
14 directories, 36 files |
|||
|
|||
通过分组,我们可以很方便的将不同的业务逻辑分组到不同的目录下,这样可以很方便的管理不同的业务逻辑。 |
|||
|
|||
注意 |
|||
通过命令行参数 --types-group 可开启 types 分组,types 分组会按照 group 名称生成不同的文件,而不是按照目录分组,生成示例如下 |
|||
|
|||
goctl api go --api $api --dir $output --type-group |
|||
types 分组需要 goctl 大于等于 1.8.3 版本 该功能处于实验性阶段,如果兼容问题请执行命令 goctl env -w GOCTL_EXPERIMENTAL=off 关闭实验性功能即可 请注意,实验功能非稳定版本,有存在调整的可能。 |
|||
|
|||
Edit this page |
|||
Previous |
|||
« 路由前缀 |
|||
Next |
|||
签名开关 » |
@ -0,0 +1,85 @@ |
|||
JWT |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 HTTP 服务开发中,服务认证也是经常会用到的一个功能,本文档将介绍如何在 api 文件中声明中间件。 |
|||
|
|||
JWT |
|||
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用间传递声明式信息。它是一种基于 JSON 的轻量级的身份验证和授权机制,用于在客户端和服务器之间安全地传输信息。 |
|||
|
|||
更多关于 jwt 的文档请参考 |
|||
|
|||
《JSON Web Tokens》 |
|||
《JWT 认证》 |
|||
我们来看一下在 api 文件中如何声明开启 jwt 认证 |
|||
|
|||
syntax = "v1" |
|||
|
|||
type LoginReq { |
|||
Username string `json:"username"` |
|||
Password string `json:"password"` |
|||
} |
|||
|
|||
type LoginResp { |
|||
ID string `json:"id"` |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
type UserInfoReq { |
|||
ID string `json:"id"` |
|||
} |
|||
|
|||
type UserInfoResp { |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
service user-api { |
|||
@handler login |
|||
post /user/login (LoginReq) returns (LoginResp) |
|||
} |
|||
|
|||
@server ( |
|||
jwt: Auth // 开启 jwt 认证 |
|||
) |
|||
service user-api { |
|||
@handler userInfo |
|||
post /user/info (UserInfoReq) returns (UserInfoResp) |
|||
} |
|||
|
|||
在上文中,我们通过在 @server 中来通过 jwt 关键字声明了开启 jwt 认证,且该 jwt 认证仅对其对应的路由有用,如上文中 jwt 仅对 /user/info 生效,对 /user/login 是不生效的,我们使用 Auth 来作为 jwt 的值,其在经过 goctl 进行代码生成后会转成 对应 jwt 配置。 |
|||
|
|||
下面简单看一下生成的 jwt 代码: |
|||
|
|||
config.go |
|||
routes.go |
|||
package config |
|||
|
|||
import "github.com/zeromicro/go-zero/rest" |
|||
|
|||
type Config struct { |
|||
rest.RestConf |
|||
Auth struct {// JWT 认证需要的密钥和过期时间配置 |
|||
AccessSecret string |
|||
AccessExpire int64 |
|||
} |
|||
} |
|||
Config 结构体中的 Auth 字段就是我们通过在 api 语法文件中声明的值,这是代码生成后的结果。 |
|||
|
|||
在上文中,我们可以看到,我们声明的 jwt 其实在生成代码后通过 rest.WithJwt 来声明进行 jwt 认证了。 |
|||
|
|||
注意 |
|||
代码生成后的 jwt 认证,框架只做了服务端逻辑,对于 jwt token 的生成及 refresh token 仍需要开发者自行实现。 |
|||
|
|||
载体信息获取 |
|||
jwt 通常可以携带一些自定义信息,比如 server 端生成 jwt key 时添加了 custom-key 值,go-zero 在解析后会将所有载体放到 context 中,开发者可以 通过如下示例获取载体信息。 |
|||
|
|||
func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp \*types.UserInfoResp, err error) { |
|||
// 获取 jwt 载体信息 |
|||
value:=l.ctx.Value("custom-key") |
|||
return |
|||
} |
|||
Edit this page |
|||
Previous |
|||
« 签名开关 |
|||
Next |
|||
路由规则 » |
@ -0,0 +1,319 @@ |
|||
logx |
|||
类型与方法说明 |
|||
|
|||
1. LogConf |
|||
LogConf 是日志配置结构体,用于设置日志相关的各项参数。 |
|||
|
|||
type LogConf struct { |
|||
ServiceName string `json:",optional"` |
|||
Mode string `json:",default=console,options=[console,file,volume]"` |
|||
Encoding string `json:",default=json,options=[json,plain]"` |
|||
TimeFormat string `json:",optional"` |
|||
Path string `json:",default=logs"` |
|||
Level string `json:",default=info,options=[debug,info,error,severe]"` |
|||
MaxContentLength uint32 `json:",optional"` |
|||
Compress bool `json:",optional"` |
|||
Stat bool `json:",default=true"` |
|||
KeepDays int `json:",optional"` |
|||
StackCooldownMillis int `json:",default=100"` |
|||
MaxBackups int `json:",default=0"` |
|||
MaxSize int `json:",default=0"` |
|||
Rotation string `json:",default=daily,options=[daily,size]"` |
|||
} 2. WithColor |
|||
在纯文本编码时,给字符串添加颜色。 |
|||
|
|||
func WithColor(text string, colour color.Color) string |
|||
参数: |
|||
|
|||
text: 要添加颜色的文本。 |
|||
colour: 颜色对象。 |
|||
返回值: 返回带颜色的字符串。 |
|||
|
|||
示例代码 |
|||
import "github.com/fatih/color" |
|||
|
|||
text := "Hello, World!" |
|||
coloredText := logx.WithColor(text, color.FgRed) |
|||
fmt.Println(coloredText) 3. AddGlobalFields |
|||
添加全局字段,这些字段将被添加到所有日志条目中。 |
|||
|
|||
func AddGlobalFields(fields ...LogField) |
|||
参数: |
|||
fields: 要添加的全局字段。 |
|||
示例代码 |
|||
logx.AddGlobalFields(logx.Field("service", "my-service")) 4. ContextWithFields |
|||
返回包含给定字段的上下文。 |
|||
|
|||
func ContextWithFields(ctx context.Context, fields ...LogField) context.Context |
|||
参数: |
|||
|
|||
ctx: 上下文对象。 |
|||
fields: 要添加到上下文中的字段。 |
|||
返回值: 返回新的上下文对象。 |
|||
|
|||
示例代码 |
|||
ctx := context.Background() |
|||
ctx = logx.ContextWithFields(ctx, logx.Field("request_id", "12345")) 5. Logger 接口 |
|||
Logger 接口定义了日志记录的方法。 |
|||
|
|||
type Logger interface { |
|||
Debug(...any) |
|||
Debugf(string, ...any) |
|||
Debugv(any) |
|||
Debugw(string, ...LogField) |
|||
Error(...any) |
|||
Errorf(string, ...any) |
|||
Errorv(any) |
|||
Errorw(string, ...LogField) |
|||
Info(...any) |
|||
Infof(string, ...any) |
|||
Infov(any) |
|||
Infow(string, ...LogField) |
|||
Slow(...any) |
|||
Slowf(string, ...any) |
|||
Slowv(any) |
|||
Sloww(string, ...LogField) |
|||
WithCallerSkip(skip int) Logger |
|||
WithContext(ctx context.Context) Logger |
|||
WithDuration(d time.Duration) Logger |
|||
WithFields(fields ...LogField) Logger |
|||
} |
|||
示例代码 |
|||
var logger logx.Logger = logx.WithContext(context.Background()) |
|||
|
|||
logger.Info("This is an info log") |
|||
logger.Debugf("Debug log with value: %d", 42) |
|||
logger.Errorw("Error occurred", logx.Field("error_code", 500)) 6. NewLessLogger |
|||
创建一个间隔一定时间内只记录一次日志的 LessLogger。 |
|||
|
|||
func NewLessLogger(milliseconds int) \*LessLogger |
|||
参数: |
|||
|
|||
milliseconds: 时间间隔(毫秒)。 |
|||
返回值: 返回 LessLogger 对象。 |
|||
|
|||
示例代码 |
|||
lessLogger := logx.NewLessLogger(1000) |
|||
|
|||
lessLogger.Error("This error will be logged at most once per second") 7. NewWriter |
|||
创建一个新的 Writer 实例。 |
|||
|
|||
func NewWriter(w io.Writer) Writer |
|||
参数: |
|||
|
|||
w: 一个实现了 io.Writer 接口的实例。 |
|||
返回值: 返回 Writer 接口的实现。 |
|||
|
|||
示例代码 |
|||
file, err := os.Create("app.log") |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
writer := logx.NewWriter(file) |
|||
logx.SetWriter(writer) |
|||
日志配置示例 |
|||
logConf := logx.LogConf{ |
|||
ServiceName: "example-service", |
|||
Mode: "file", |
|||
Encoding: "json", |
|||
Path: "/var/logs", |
|||
Level: "debug", |
|||
KeepDays: 7, |
|||
MaxContentLength: 1024, |
|||
Compress: true, |
|||
} |
|||
|
|||
err := logx.SetUp(logConf) |
|||
if err != nil { |
|||
log.Fatalf("Failed to set up logging: %v", err) |
|||
} |
|||
|
|||
logc |
|||
logc 包封装了 go-zero 中 logx 包的日志功能,提供了一些便捷的方法来记录不同级别的日志。 |
|||
|
|||
类型定义 |
|||
type ( |
|||
LogConf = logx.LogConf |
|||
LogField = logx.LogField |
|||
) |
|||
函数列表 |
|||
AddGlobalFields |
|||
添加全局字段,这些字段会出现在所有的日志中。 |
|||
|
|||
func AddGlobalFields(fields ...LogField) |
|||
示例: |
|||
|
|||
logc.AddGlobalFields(logc.Field("app", "exampleApp")) |
|||
Alert |
|||
以警告级别记录日志信息,该信息会写入错误日志。 |
|||
|
|||
func Alert(\_ context.Context, v string) |
|||
示例: |
|||
|
|||
logc.Alert(context.Background(), "This is an alert message") |
|||
Close |
|||
关闭日志记录系统。 |
|||
|
|||
func Close() error |
|||
示例: |
|||
|
|||
if err := logc.Close(); err != nil { |
|||
fmt.Println("Error closing log system:", err) |
|||
} |
|||
Debug |
|||
记录调试级别的日志信息。 |
|||
|
|||
func Debug(ctx context.Context, v ...interface{}) |
|||
示例: |
|||
|
|||
logc.Debug(context.Background(), "This is a debug message") |
|||
Debugf |
|||
格式化记录调试级别的日志信息。 |
|||
|
|||
func Debugf(ctx context.Context, format string, v ...interface{}) |
|||
示例: |
|||
|
|||
logc.Debugf(context.Background(), "This is a %s message", "formatted debug") |
|||
Debugv |
|||
以 JSON 格式记录调试级别的日志信息。 |
|||
|
|||
func Debugv(ctx context.Context, v interface{}) |
|||
示例: |
|||
|
|||
logc.Debugv(context.Background(), map[string]interface{}{"key": "value"}) |
|||
Debugw |
|||
记录带字段的调试级别的日志信息。 |
|||
|
|||
func Debugw(ctx context.Context, msg string, fields ...LogField) |
|||
示例: |
|||
|
|||
logc.Debugw(context.Background(), "Debug message with fields", logc.Field("key", "value")) |
|||
Error |
|||
记录错误级别的日志信息。 |
|||
|
|||
func Error(ctx context.Context, v ...any) |
|||
示例: |
|||
|
|||
logc.Error(context.Background(), "This is an error message") |
|||
Errorf |
|||
格式化记录错误级别的日志信息。 |
|||
|
|||
func Errorf(ctx context.Context, format string, v ...any) |
|||
示例: |
|||
|
|||
logc.Errorf(context.Background(), "This is a %s message", "formatted error") |
|||
Errorv |
|||
以 JSON 格式记录错误级别的日志信息。 |
|||
|
|||
func Errorv(ctx context.Context, v any) |
|||
示例: |
|||
|
|||
logc.Errorv(context.Background(), map[string]interface{}{"error": "something went wrong"}) |
|||
Errorw |
|||
记录带字段的错误级别的日志信息。 |
|||
|
|||
func Errorw(ctx context.Context, msg string, fields ...LogField) |
|||
示例: |
|||
|
|||
logc.Errorw(context.Background(), "Error message with fields", logc.Field("key", "value")) |
|||
Field |
|||
返回一个日志字段。 |
|||
|
|||
func Field(key string, value any) LogField |
|||
示例: |
|||
|
|||
field := logc.Field("key", "value") |
|||
Info |
|||
记录信息级别的日志信息。 |
|||
|
|||
func Info(ctx context.Context, v ...any) |
|||
示例: |
|||
|
|||
logc.Info(context.Background(), "This is an info message") |
|||
Infof |
|||
格式化记录信息级别的日志信息。 |
|||
|
|||
func Infof(ctx context.Context, format string, v ...any) |
|||
示例: |
|||
|
|||
logc.Infof(context.Background(), "This is a %s message", "formatted info") |
|||
Infov |
|||
以 JSON 格式记录信息级别的日志信息。 |
|||
|
|||
func Infov(ctx context.Context, v any) |
|||
示例: |
|||
|
|||
logc.Infov(context.Background(), map[string]interface{}{"info": "some information"}) |
|||
Infow |
|||
记录带字段的信息级别的日志信息。 |
|||
|
|||
func Infow(ctx context.Context, msg string, fields ...LogField) |
|||
示例: |
|||
|
|||
logc.Infow(context.Background(), "Info message with fields", logc.Field("key", "value")) |
|||
Must |
|||
检查错误,如果发生错误则记录错误并退出程序。 |
|||
|
|||
func Must(err error) |
|||
示例: |
|||
|
|||
logc.Must(errors.New("fatal error")) |
|||
MustSetup |
|||
根据给定的配置初始化日志系统,如有错误则退出程序。 |
|||
|
|||
func MustSetup(c logx.LogConf) |
|||
示例: |
|||
|
|||
config := logx.LogConf{ |
|||
ServiceName: "exampleService", |
|||
Mode: "console", |
|||
} |
|||
logc.MustSetup(config) |
|||
SetLevel |
|||
设置日志级别,可以用来抑制某些日志。 |
|||
|
|||
func SetLevel(level uint32) |
|||
示例: |
|||
|
|||
logc.SetLevel(logx.LevelInfo) |
|||
SetUp |
|||
根据给定的配置初始化日志系统。如果已经初始化,将不再重复初始化。 |
|||
|
|||
func SetUp(c LogConf) error |
|||
示例: |
|||
|
|||
config := logc.LogConf{ |
|||
ServiceName: "exampleService", |
|||
Mode: "console", |
|||
} |
|||
if err := logc.SetUp(config); err != nil { |
|||
fmt.Println("Error setting up log system:", err) |
|||
} |
|||
Slow |
|||
记录慢日志。 |
|||
|
|||
func Slow(ctx context.Context, v ...any) |
|||
示例: |
|||
|
|||
logc.Slow(context.Background(), "This is a slow log message") |
|||
Slowf |
|||
格式化记录慢日志。 |
|||
|
|||
func Slowf(ctx context.Context, format string, v ...any) |
|||
示例: |
|||
|
|||
logc.Slowf(context.Background(), "This is a %s message", "formatted slow log") |
|||
Slowv |
|||
以 JSON 格式记录慢日志。 |
|||
|
|||
func Slowv(ctx context.Context, v any) |
|||
示例: |
|||
|
|||
logc.Slowv(context.Background(), map[string]interface{}{"slow": "operation details"}) |
|||
Sloww |
|||
记录带字段的慢日志。 |
|||
|
|||
func Sloww(ctx context.Context, msg string, fields ...LogField) |
|||
示例: |
|||
|
|||
logc.Sloww(context.Background(), "Slow log message with fields", logc.Field("key", "value")) |
@ -0,0 +1,107 @@ |
|||
中间件声明 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 HTTP 开发中,中间件是非常常见的需求,比如我们需要对请求进行鉴权,或者对请求进行日志记录,这些都是非常常见的需求。 |
|||
|
|||
中间件声明 |
|||
假设我们有一个用户服务,我们需要将 user-agent 信息存入到 context 信息中,然后在 logic 层根据 user-agent 做业务处理,我们可以通过 api 语言来声明中间件, 在 api 语言中,我们可以通过 middleware 关键字来声明中间件,中间件的声明格式如下: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type UserInfoRequest { |
|||
Id int64 `path:"id"` |
|||
} |
|||
type UserInfoResponse { |
|||
Id int64 `json:"id"` |
|||
Name string `json:"name"` |
|||
Age int32 `json:"age"` |
|||
} |
|||
|
|||
@server( |
|||
// 通过 middileware 关键字声明中间件,多个中间件以英文逗号分割,如 UserAgentMiddleware,LogMiddleware |
|||
middleware: UserAgentMiddleware |
|||
) |
|||
service user { |
|||
@handler userinfo |
|||
get /user/info/:id (UserInfoRequest) returns (UserInfoResponse) |
|||
} |
|||
在上面的例子中,我们声明了一个中间件 UserAgentMiddleware,然后在 @server 中通过 middileware 关键字来声明中间件。 我们来看一下生成的中间件代码: |
|||
|
|||
目录结构 |
|||
|
|||
. |
|||
├── etc |
|||
│ └── user.yaml |
|||
├── internal |
|||
│ ├── config |
|||
│ │ └── config.go |
|||
│ ├── handler |
|||
│ │ ├── routes.go |
|||
│ │ └── userinfohandler.go |
|||
│ ├── logic |
|||
│ │ └── userinfologic.go |
|||
│ ├── middleware # 中间件目录 |
|||
│ │ └── useragentmiddleware.go |
|||
│ ├── svc |
|||
│ │ └── servicecontext.go |
|||
│ └── types |
|||
│ └── types.go |
|||
├── user.api |
|||
└── user.go |
|||
|
|||
8 directories, 10 files |
|||
中间件代码(未填充逻辑) |
|||
|
|||
useragentmiddleware.go |
|||
servicecontext.go |
|||
routes.go |
|||
package middleware |
|||
|
|||
import "net/http" |
|||
|
|||
type UserAgentMiddleware struct { |
|||
} |
|||
|
|||
func NewUserAgentMiddleware() \*UserAgentMiddleware { |
|||
return &UserAgentMiddleware{} |
|||
} |
|||
|
|||
func (m *UserAgentMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
// TODO generate middleware implement function, delete after code implementation |
|||
|
|||
// Passthrough to next handler if need |
|||
next(w, r) |
|||
} |
|||
|
|||
} |
|||
你可以看到,中间件的代码是通过 goctl 自动生成的,中间件的代码是一个结构体,结构体中有一个 Handle 方法,这个方法是中间件的核心方法,这个方法接收一个 http.HandlerFunc 类型的参数,然后返回一个 http.HandlerFunc 类型的参数,这个方法的作用是对请求进行处理,然后将请求传递给下一个中间件或者 handler。 |
|||
|
|||
你可以在 Handle 方法中对请求进行处理,比如鉴权,日志记录等等,然后将请求传递给下一个中间件或者 handler。 |
|||
|
|||
如上需求例子,我们可以在中间件中将 header 中的 User-Agent 信息存到 context 中,中间件实现如下: |
|||
|
|||
package middleware |
|||
|
|||
import ( |
|||
"context" |
|||
"net/http" |
|||
) |
|||
|
|||
type UserAgentMiddleware struct { |
|||
} |
|||
|
|||
func NewUserAgentMiddleware() \*UserAgentMiddleware { |
|||
return &UserAgentMiddleware{} |
|||
} |
|||
|
|||
func (m *UserAgentMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
val := r.Header.Get("User-Agent") |
|||
reqCtx := r.Context() |
|||
ctx := context.WithValue(reqCtx, "User-Agent", val) |
|||
newReq := r.WithContext(ctx) |
|||
next(w, newReq) |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
内存缓存使用 |
|||
概述 |
|||
本章节主要介绍 cache 的使用。 |
|||
|
|||
准备条件 |
|||
完成 golang 安装 |
|||
创建 |
|||
NewCache |
|||
函数签名: |
|||
NewCache func(expire time.Duration, opts ...CacheOption) (*Cache, error) |
|||
说明: |
|||
创建 cache 对象。 |
|||
入参: 1. expire: 过期时间 2. opts: 操作选项 |
|||
2.1 WithLimit: 设置 cache 存储数据数量上限 |
|||
2.2 WithName: 设置 cache 名称,输出日志时会打印 |
|||
返回值: 1. *Cache: cache 对象 2. error: 创建结果 |
|||
方法说明 |
|||
Set |
|||
函数签名: |
|||
Set func(key string, value interface{}) |
|||
说明: |
|||
添加值到缓存。 |
|||
入参: 1. key: key 2. value: 值 |
|||
|
|||
示例: |
|||
cache, err := NewCache(time.Second\*2, WithName("any")) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
cache.Set("first", "first element") |
|||
SetWithExpire |
|||
函数签名: |
|||
SetWithExpire func(key string, value interface{}, expire time.Duration) |
|||
说明: |
|||
添加值到缓存, 同时指定过期时间 |
|||
入参: 1. key: key 2. value: 值 3. expire: 过期时间 |
|||
|
|||
示例: |
|||
cache, err := NewCache(time.Second\*2, WithName("any")) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
cache.SetWithExpire("first", "first element", time.Second) |
|||
Get |
|||
函数签名: |
|||
Get func(key string) (interface{}, bool) |
|||
说明: |
|||
查询缓存 |
|||
入参: 1. key: key |
|||
|
|||
返回值: 1. interface{}: value 2. bool: 是否存在 |
|||
|
|||
示例: |
|||
cache, err := NewCache(time.Second\*2, WithName("any")) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
cache.Set("first", "first element") |
|||
|
|||
v, exist := cache.Get("first") |
|||
if !exist { |
|||
// deal with not exist |
|||
} |
|||
value, ok := v.(string) |
|||
if !ok { |
|||
// deal with type error |
|||
} |
|||
// use value |
|||
|
|||
Del |
|||
函数签名: |
|||
Del func(key string) |
|||
说明: |
|||
删除缓存。 |
|||
入参: 1. key: key |
|||
|
|||
示例: |
|||
cache, err := NewCache(time.Second\*2, WithName("any")) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
cache.Del("first") |
|||
Take |
|||
函数签名: |
|||
Take funcTake(key string, fetch func() (interface{}, error)) (interface{}, error) |
|||
说明: |
|||
获取缓存,如果缓存中存在,则返回缓存中的值,如果缓存不存在,则执行 fetch 函数的返回结果。 |
|||
入参: 1. key: key 2. fetch: 自定义返回结果 |
|||
|
|||
示例: |
|||
cache, err := NewCache(time.Second\*2, WithName("any")) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
|
|||
v, err := cache.Take("first", func() (interface{}, error) { |
|||
return "first element", nil |
|||
}) |
|||
println(v) // output: first element |
|||
|
|||
cache.Set("first", "first element 2") |
|||
|
|||
v, err = cache.Take("first", func() (interface{}, error) { |
|||
return "first element", nil |
|||
}) |
|||
println(v) // // output: first element 2 |
@ -0,0 +1,266 @@ |
|||
Protobuf 规范 |
|||
概述 |
|||
本教程使用 Protocol Buffers 语言的 proto3 版本,为 Go 程序员提供了使用 Protocol Buffers 的基本介绍。通过创建一个简单的示例应用程序,它向您展示了如何 |
|||
|
|||
在 .proto 文件中定义消息格式。 |
|||
使用协议缓冲区编译器。 |
|||
使用 Go protocol buffer API 来写入和读取消息。 |
|||
|
|||
服务分组 |
|||
概述 |
|||
go-zero 采用 gRPC 进行服务间的通信,我们通过 proto 文件来定义服务的接口,但是在实际的开发中,我们可能会有多个服务,如果不对服务进行文件分组,那么 goctl 生成的代码将会是一个大的文件夹,这样会导致代码的可维护性变差,因此服务分组可以提高代码的可读性和可维护性。 |
|||
|
|||
服务分组 |
|||
在 go-zero 中,我们通过在 proto 文件中以 service 为维度来进行文件分组,我们可以在 proto 文件中定义多个 service,每个 service 都会生成一个独立的文件夹,这样就可以将不同的服务进行分组,从而提高代码的可读性和可维护性。 |
|||
|
|||
除了 proto 文件中定义了 service 外,分组与否还需要在 goctl 中控制,生成带分组或者不带分组的代码取决于开发者,我们通过示例来演示一下。 |
|||
|
|||
不带分组 |
|||
假设我们有一个 proto 文件,如下: |
|||
|
|||
syntax = "proto3"; |
|||
|
|||
package user; |
|||
|
|||
option go_package = "github.com/example/user"; |
|||
|
|||
message LoginReq{} |
|||
message LoginResp{} |
|||
message UserInfoReq{} |
|||
message UserInfoResp{} |
|||
message UserInfoUpdateReq{} |
|||
message UserInfoUpdateResp{} |
|||
message UserListReq{} |
|||
message UserListResp{} |
|||
|
|||
message UserRoleListReq{} |
|||
message UserRoleListResp{} |
|||
message UserRoleUpdateReq{} |
|||
message UserRoleUpdateResp{} |
|||
message UserRoleInfoReq{} |
|||
message UserRoleInfoResp{} |
|||
message UserRoleAddReq{} |
|||
message UserRoleAddResp{} |
|||
message UserRoleDeleteReq{} |
|||
message UserRoleDeleteResp{} |
|||
|
|||
message UserClassListReq{} |
|||
message UserClassListResp{} |
|||
message UserClassUpdateReq{} |
|||
message UserClassUpdateResp{} |
|||
message UserClassInfoReq{} |
|||
message UserClassInfoResp{} |
|||
message UserClassAddReq{} |
|||
message UserClassAddResp{} |
|||
message UserClassDeleteReq{} |
|||
message UserClassDeleteResp{} |
|||
|
|||
service UserService{ |
|||
rpc Login (LoginReq) returns (LoginResp); |
|||
rpc UserInfo (UserInfoReq) returns (UserInfoResp); |
|||
rpc UserInfoUpdate (UserInfoUpdateReq) returns (UserInfoUpdateResp); |
|||
rpc UserList (UserListReq) returns (UserListResp); |
|||
|
|||
rpc UserRoleList (UserRoleListReq) returns (UserRoleListResp); |
|||
rpc UserRoleUpdate (UserRoleUpdateReq) returns (UserRoleUpdateResp); |
|||
rpc UserRoleInfo (UserRoleInfoReq) returns (UserRoleInfoResp); |
|||
rpc UserRoleAdd (UserRoleAddReq) returns (UserRoleAddResp); |
|||
rpc UserRoleDelete (UserRoleDeleteReq) returns (UserRoleDeleteResp); |
|||
|
|||
rpc UserClassList (UserClassListReq) returns (UserClassListResp); |
|||
rpc UserClassUpdate (UserClassUpdateReq) returns (UserClassUpdateResp); |
|||
rpc UserClassInfo (UserClassInfoReq) returns (UserClassInfoResp); |
|||
rpc UserClassAdd (UserClassAddReq) returns (UserClassAddResp); |
|||
rpc UserClassDelete (UserClassDeleteReq) returns (UserClassDeleteResp); |
|||
} |
|||
我们来看一下不分组的情况下,goctl 生成的代码结构: |
|||
|
|||
$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. |
|||
$ tree |
|||
. |
|||
├── etc |
|||
│ └── user.yaml |
|||
├── github.com |
|||
│ └── example |
|||
│ └── user |
|||
│ ├── user.pb.go |
|||
│ └── user_grpc.pb.go |
|||
├── go.mod |
|||
├── internal |
|||
│ ├── config |
|||
│ │ └── config.go |
|||
│ ├── logic |
|||
│ │ ├── loginlogic.go |
|||
│ │ ├── userclassaddlogic.go |
|||
│ │ ├── userclassdeletelogic.go |
|||
│ │ ├── userclassinfologic.go |
|||
│ │ ├── userclasslistlogic.go |
|||
│ │ ├── userclassupdatelogic.go |
|||
│ │ ├── userinfologic.go |
|||
│ │ ├── userinfoupdatelogic.go |
|||
│ │ ├── userlistlogic.go |
|||
│ │ ├── userroleaddlogic.go |
|||
│ │ ├── userroledeletelogic.go |
|||
│ │ ├── userroleinfologic.go |
|||
│ │ ├── userrolelistlogic.go |
|||
│ │ └── userroleupdatelogic.go |
|||
│ ├── server |
|||
│ │ └── userserviceserver.go |
|||
│ └── svc |
|||
│ └── servicecontext.go |
|||
├── user.go |
|||
├── user.proto |
|||
└── userservice |
|||
└── userservice.go |
|||
|
|||
10 directories, 24 files |
|||
温馨提示 |
|||
在不进行分组的情况下,不支持在 proto 文件中定义多个 service,否则会报错。 |
|||
|
|||
带分组 |
|||
首先,我们需要在 proto 文件中定义多个 service,如下: |
|||
|
|||
syntax = "proto3"; |
|||
|
|||
package user; |
|||
|
|||
option go_package = "github.com/example/user"; |
|||
|
|||
message LoginReq{} |
|||
message LoginResp{} |
|||
message UserInfoReq{} |
|||
message UserInfoResp{} |
|||
message UserInfoUpdateReq{} |
|||
message UserInfoUpdateResp{} |
|||
message UserListReq{} |
|||
message UserListResp{} |
|||
service UserService{ |
|||
rpc Login (LoginReq) returns (LoginResp); |
|||
rpc UserInfo (UserInfoReq) returns (UserInfoResp); |
|||
rpc UserInfoUpdate (UserInfoUpdateReq) returns (UserInfoUpdateResp); |
|||
rpc UserList (UserListReq) returns (UserListResp); |
|||
} |
|||
|
|||
message UserRoleListReq{} |
|||
message UserRoleListResp{} |
|||
message UserRoleUpdateReq{} |
|||
message UserRoleUpdateResp{} |
|||
message UserRoleInfoReq{} |
|||
message UserRoleInfoResp{} |
|||
message UserRoleAddReq{} |
|||
message UserRoleAddResp{} |
|||
message UserRoleDeleteReq{} |
|||
message UserRoleDeleteResp{} |
|||
service UserRoleService{ |
|||
rpc UserRoleList (UserRoleListReq) returns (UserRoleListResp); |
|||
rpc UserRoleUpdate (UserRoleUpdateReq) returns (UserRoleUpdateResp); |
|||
rpc UserRoleInfo (UserRoleInfoReq) returns (UserRoleInfoResp); |
|||
rpc UserRoleAdd (UserRoleAddReq) returns (UserRoleAddResp); |
|||
rpc UserRoleDelete (UserRoleDeleteReq) returns (UserRoleDeleteResp); |
|||
} |
|||
|
|||
message UserClassListReq{} |
|||
message UserClassListResp{} |
|||
message UserClassUpdateReq{} |
|||
message UserClassUpdateResp{} |
|||
message UserClassInfoReq{} |
|||
message UserClassInfoResp{} |
|||
message UserClassAddReq{} |
|||
message UserClassAddResp{} |
|||
message UserClassDeleteReq{} |
|||
message UserClassDeleteResp{} |
|||
service UserClassService{ |
|||
rpc UserClassList (UserClassListReq) returns (UserClassListResp); |
|||
rpc UserClassUpdate (UserClassUpdateReq) returns (UserClassUpdateResp); |
|||
rpc UserClassInfo (UserClassInfoReq) returns (UserClassInfoResp); |
|||
rpc UserClassAdd (UserClassAddReq) returns (UserClassAddResp); |
|||
rpc UserClassDelete (UserClassDeleteReq) returns (UserClassDeleteResp); |
|||
} |
|||
我们来看一下带分组的情况下,goctl 生成的代码结构: |
|||
|
|||
# 通过 -m 指定 goctl 生成分组的代码 |
|||
|
|||
$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. -m |
|||
$ tree |
|||
. |
|||
├── client |
|||
│ ├── userclassservice |
|||
│ │ └── userclassservice.go |
|||
│ ├── userroleservice |
|||
│ │ └── userroleservice.go |
|||
│ └── userservice |
|||
│ └── userservice.go |
|||
├── etc |
|||
│ └── user.yaml |
|||
├── github.com |
|||
│ └── example |
|||
│ └── user |
|||
│ ├── user.pb.go |
|||
│ └── user_grpc.pb.go |
|||
├── go.mod |
|||
├── internal |
|||
│ ├── config |
|||
│ │ └── config.go |
|||
│ ├── logic |
|||
│ │ ├── userclassservice |
|||
│ │ │ ├── userclassaddlogic.go |
|||
│ │ │ ├── userclassdeletelogic.go |
|||
│ │ │ ├── userclassinfologic.go |
|||
│ │ │ ├── userclasslistlogic.go |
|||
│ │ │ └── userclassupdatelogic.go |
|||
│ │ ├── userroleservice |
|||
│ │ │ ├── userroleaddlogic.go |
|||
│ │ │ ├── userroledeletelogic.go |
|||
│ │ │ ├── userroleinfologic.go |
|||
│ │ │ ├── userrolelistlogic.go |
|||
│ │ │ └── userroleupdatelogic.go |
|||
│ │ └── userservice |
|||
│ │ ├── loginlogic.go |
|||
│ │ ├── userinfologic.go |
|||
│ │ ├── userinfoupdatelogic.go |
|||
│ │ └── userlistlogic.go |
|||
│ ├── server |
|||
│ │ ├── userclassservice |
|||
│ │ │ └── userclassserviceserver.go |
|||
│ │ ├── userroleservice |
|||
│ │ │ └── userroleserviceserver.go |
|||
│ │ └── userservice |
|||
│ │ └── userserviceserver.go |
|||
│ └── svc |
|||
│ └── servicecontext.go |
|||
├── user.go |
|||
└── user.proto |
|||
|
|||
19 directories, 28 files |
|||
通过目录结构我们可以看出,logic、server、client 目录都会根据 service 进行分组。 |
|||
|
|||
1. goctl 生成 gRPC 代码时 proto 使用规范 |
|||
在使用 goctl 生成 gRPC 代码时,编写的所有 rpc 方法的请求体和响应体必须在主 proto 中声明 message,即不支持从外包外 message,以下是 不支持 的 import 示例: |
|||
syntax = "proto3"; |
|||
|
|||
package greet; |
|||
|
|||
import "base.proto" |
|||
|
|||
service demo{ |
|||
rpc (base.DemoReq) returns (base.DemoResp); |
|||
} |
|||
正确写法 |
|||
|
|||
syntax = "proto3"; |
|||
|
|||
package greet; |
|||
|
|||
message DemoReq{} |
|||
message DemoResp{} |
|||
|
|||
service demo{ |
|||
rpc (DemoReq) returns (DemoResp); |
|||
} |
|||
在满足 1 的情况,proto import 只支持 message 引入,不支持 service 引入。 2. 为什么使用 goctl 生成 gRPC 代码时 proto 不支持使用包外 proto 和 service? |
|||
对于包外的 proto,goctl 没法完全控制,有几个问题 goctl 没法解决: |
|||
|
|||
包外的 proto 如果层级很深,亦或在其他公共仓库中,goctl 没法确定包外 proto 是否已经生成了 pb.go |
|||
如果包外的 proto 生成了 pb.go,那些 pb.go 生成在哪些目录下,是否在同一个工程中,如果在当前工程外,pb.go 所在工程是否使用了 go module 等等,goctl 没法得知,因此没法定位到其真正的 go package。 |
|||
如果包外的 proto 没有生成,既然其属于公共 proto,那么 goctl 是否需要针对每次引入都要生成,这个也无从解决 3. goctl 生成 gRPC 不支持 google/protobuf/empty.proto 包的 import |
|||
答案,同 1,2 |
@ -0,0 +1,135 @@ |
|||
路由前缀 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 HTTP 服务开发中,路由前缀需求是非常常见的,比如我们通过路由来区分版本,或者通过路由来区分不同的服务,这些都是非常常见的需求。 |
|||
|
|||
路由前缀 |
|||
假设我们有一个用户服务,我们需要通过路由来区分不同的版本,我们可以通过 api 语言来声明路由前缀: |
|||
|
|||
https://example.com/v1/users |
|||
https://example.com/v2/users |
|||
在上文路由中,我们通过版本 v1 和 v2 来区分了 /users 路由,我们可以通过 api 语言来声明路由前缀: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type UserV1 { |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
type UserV2 { |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
@server ( |
|||
prefix: /v1 |
|||
) |
|||
service user-api { |
|||
@handler usersv1 |
|||
get /users returns ([]UserV1) |
|||
} |
|||
|
|||
@server ( |
|||
prefix: /v2 |
|||
) |
|||
service user-api { |
|||
@handler usersv2 |
|||
get /users returns ([]UserV2) |
|||
} |
|||
|
|||
在上文中,我们通过在 @server 中来通过 prefix 关键字声明了路由前缀,然后通过 @handler 来声明了路由处理函数,这样我们就可以通过路由前缀来区分不同的版本了。 |
|||
|
|||
下面简单看一下生成的路由代码: |
|||
|
|||
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { |
|||
server.AddRoutes( |
|||
[]rest.Route{ |
|||
{ |
|||
Method: http.MethodGet, |
|||
Path: "/users", |
|||
Handler: usersv1Handler(serverCtx), |
|||
}, |
|||
}, |
|||
rest.WithPrefix("/v1"), |
|||
) |
|||
|
|||
server.AddRoutes( |
|||
[]rest.Route{ |
|||
{ |
|||
Method: http.MethodGet, |
|||
Path: "/users", |
|||
Handler: usersv2Handler(serverCtx), |
|||
}, |
|||
}, |
|||
rest.WithPrefix("/v2"), |
|||
) |
|||
|
|||
} |
|||
在上文中,我们可以看到,我们声明的 prefix 其实在生成代码后通过 rest.WithPrefix 来声明了路由前缀,这样我们就可以通过路由前缀来区分不同的版本了。 |
|||
|
|||
路由规则 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 api 描述语言中,有特定的路由规则,这些路由规则并非和 HTTP 的路由规则完全引用,接下来我们来看一下 api 描述语言中的路由规则吧。 |
|||
|
|||
路由规则 |
|||
在 api 描述语言中,路由需要满足如下规则 |
|||
|
|||
路由必须以 / 开头 |
|||
路由节点必须以 / 分隔 |
|||
路由节点中可以包含 :,但是 : 必须是路由节点的第一个字符,: 后面的节点值必须要在结请求体中有 path tag 声明,用于接收路由参数,详细规则可参考 路由参数。 |
|||
路由节点可以包含字母、数字(goctl 1.5.1 支持,可参考 新版 API 解析器使用)、下划线、中划线 |
|||
路由示例: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type DemoPath3Req { |
|||
Id int64 `path:"id"` |
|||
} |
|||
|
|||
type DemoPath4Req { |
|||
Id int64 `path:"id"` |
|||
Name string `path:"name"` |
|||
} |
|||
|
|||
type DemoPath5Req { |
|||
Id int64 `path:"id"` |
|||
Name string `path:"name"` |
|||
Age int `path:"age"` |
|||
} |
|||
|
|||
type DemoReq {} |
|||
|
|||
type DemoResp {} |
|||
|
|||
service Demo { |
|||
// 示例路由 /foo |
|||
@handler demoPath1 |
|||
get /foo (DemoReq) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar |
|||
@handler demoPath2 |
|||
get /foo/bar (DemoReq) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar/:id,其中 id 为请求体中的字段 |
|||
@handler demoPath3 |
|||
get /foo/bar/:id (DemoPath3Req) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar/:id/:name,其中 id,name 为请求体中的字段 |
|||
@handler demoPath4 |
|||
get /foo/bar/:id/:name (DemoPath4Req) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar/:id/:name/:age,其中 id,name,age 为请求体中的字段 |
|||
@handler demoPath5 |
|||
get /foo/bar/:id/:name/:age (DemoPath5Req) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar/baz-qux |
|||
@handler demoPath6 |
|||
get /foo/bar/baz-qux (DemoReq) returns (DemoResp) |
|||
|
|||
// 示例路由 /foo/bar_baz/123(goctl 1.5.1 支持) |
|||
@handler demoPath7 |
|||
get /foo/bar_baz/123 (DemoReq) returns (DemoResp) |
|||
|
|||
} |
@ -0,0 +1,46 @@ |
|||
签名开关 |
|||
概述 |
|||
在 go-zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码,在之前我们系统性的介绍了 API 规范。 |
|||
|
|||
在 go-zero 中,已经内置了签名功能,我们可以通过 api 语言来开启签名功能,然后通过 goctl 生成签名代码,这样我们就可以在 HTTP 服务中使用签名功能了。 |
|||
|
|||
签名开关 |
|||
在 api 语言中,我们可以通过 signature 关键字来开启签名功能,假设我们有一个 signdemo 服务,我们有一个接口如下: |
|||
|
|||
https://example.com/sign/demo |
|||
其对应的 api 语言如下: |
|||
|
|||
syntax = "v1" |
|||
|
|||
type ( |
|||
SignDemoReq { |
|||
Msg string `json:"msg"` |
|||
} |
|||
SignDemoResp { |
|||
Msg string `json:"msg"` |
|||
} |
|||
) |
|||
|
|||
@server ( |
|||
signature: true // 通过 signature 关键字开启签名功能 |
|||
) |
|||
service sign-api { |
|||
@handler SignDemo |
|||
post /sign/demo (SignDemoReq) returns (SignDemoResp) |
|||
} |
|||
|
|||
我们来看一下生成的路由代码,完整代码点击 这里下载 |
|||
|
|||
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { |
|||
server.AddRoutes( |
|||
[]rest.Route{ |
|||
{ |
|||
Method: http.MethodPost, |
|||
Path: "/sign/demo", |
|||
Handler: SignDemoHandler(serverCtx), |
|||
}, |
|||
}, |
|||
rest.WithSignature(serverCtx.Config.Signature), |
|||
) |
|||
} |
|||
可以看到,我们通过 rest.WithSignature 来开启签名功能,这样我们就可以在 HTTP 服务中使用签名功能了。 |
@ -0,0 +1,45 @@ |
|||
类型声明 |
|||
概述 |
|||
API 的类型声明和 Golang 的类型声明非常相似,但是有一些细微的差别,接下来我们来看一下 API 的类型声明吧。 |
|||
|
|||
类型声明 |
|||
在 API 描述语言中,类型声明需要满足如下规则: |
|||
|
|||
类型声明必须以 type 开头 |
|||
不需要声明 struct 关键字 |
|||
示例 |
|||
|
|||
type StructureExample { |
|||
// 基本数据类型示例 |
|||
BaseInt int `json:"base_int"` |
|||
BaseBool bool `json:"base_bool"` |
|||
BaseString string `json:"base_string"` |
|||
BaseByte byte `json:"base_byte"` |
|||
BaseFloat32 float32 `json:"base_float32"` |
|||
BaseFloat64 float64 `json:"base_float64"` |
|||
// 切片示例 |
|||
BaseIntSlice []int `json:"base_int_slice"` |
|||
BaseBoolSlice []bool `json:"base_bool_slice"` |
|||
BaseStringSlice []string `json:"base_string_slice"` |
|||
BaseByteSlice []byte `json:"base_byte_slice"` |
|||
BaseFloat32Slice []float32 `json:"base_float32_slice"` |
|||
BaseFloat64Slice []float64 `json:"base_float64_slice"` |
|||
// map 示例 |
|||
BaseMapIntString map[int]string `json:"base_map_int_string"` |
|||
BaseMapStringInt map[string]int `json:"base_map_string_int"` |
|||
BaseMapStringStruct map[string]*StructureExample `json:"base_map_string_struct"` |
|||
BaseMapStringIntArray map[string][]int `json:"base_map_string_int_array"` |
|||
// 匿名示例 |
|||
*Base |
|||
// 指针示例 |
|||
Base4 \*Base `json:"base4"` |
|||
|
|||
// 新的特性( goctl >= 1.5.1 版本支持 ) |
|||
// 标签忽略示例 |
|||
TagOmit string |
|||
|
|||
} |
|||
tip |
|||
API 新特性使用可参考 新版 API 解析器使用 |
|||
|
|||
我们暂时不支持泛型、弱类型,如 any 类型 |
@ -0,0 +1,100 @@ |
|||
module backend |
|||
|
|||
go 1.23.0 |
|||
|
|||
toolchain go1.23.11 |
|||
|
|||
require ( |
|||
github.com/zeromicro/go-zero v1.8.4 |
|||
google.golang.org/grpc v1.73.0 |
|||
google.golang.org/protobuf v1.36.6 |
|||
gorm.io/driver/mysql v1.6.0 |
|||
gorm.io/gorm v1.30.0 |
|||
) |
|||
|
|||
require ( |
|||
filippo.io/edwards25519 v1.1.0 // indirect |
|||
github.com/beorn7/perks v1.0.1 // indirect |
|||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect |
|||
github.com/cespare/xxhash/v2 v2.3.0 // indirect |
|||
github.com/coreos/go-semver v0.3.1 // indirect |
|||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect |
|||
github.com/davecgh/go-spew v1.1.1 // indirect |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect |
|||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect |
|||
github.com/fatih/color v1.18.0 // indirect |
|||
github.com/go-logr/logr v1.4.2 // indirect |
|||
github.com/go-logr/stdr v1.2.2 // indirect |
|||
github.com/go-openapi/jsonpointer v0.19.6 // indirect |
|||
github.com/go-openapi/jsonreference v0.20.2 // indirect |
|||
github.com/go-openapi/swag v0.22.4 // indirect |
|||
github.com/go-sql-driver/mysql v1.9.0 // indirect |
|||
github.com/gogo/protobuf v1.3.2 // indirect |
|||
github.com/golang/mock v1.6.0 // indirect |
|||
github.com/golang/protobuf v1.5.4 // indirect |
|||
github.com/google/gnostic-models v0.6.8 // indirect |
|||
github.com/google/go-cmp v0.7.0 // indirect |
|||
github.com/google/gofuzz v1.2.0 // indirect |
|||
github.com/google/uuid v1.6.0 // indirect |
|||
github.com/grafana/pyroscope-go v1.2.2 // indirect |
|||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect |
|||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect |
|||
github.com/jinzhu/inflection v1.0.0 // indirect |
|||
github.com/jinzhu/now v1.1.5 // indirect |
|||
github.com/josharian/intern v1.0.0 // indirect |
|||
github.com/json-iterator/go v1.1.12 // indirect |
|||
github.com/klauspost/compress v1.17.11 // indirect |
|||
github.com/mailru/easyjson v0.7.7 // indirect |
|||
github.com/mattn/go-colorable v0.1.13 // indirect |
|||
github.com/mattn/go-isatty v0.0.20 // indirect |
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect |
|||
github.com/modern-go/reflect2 v1.0.2 // indirect |
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect |
|||
github.com/openzipkin/zipkin-go v0.4.3 // indirect |
|||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect |
|||
github.com/prometheus/client_golang v1.21.1 // indirect |
|||
github.com/prometheus/client_model v0.6.1 // indirect |
|||
github.com/prometheus/common v0.62.0 // indirect |
|||
github.com/prometheus/procfs v0.15.1 // indirect |
|||
github.com/redis/go-redis/v9 v9.10.0 // indirect |
|||
github.com/spaolacci/murmur3 v1.1.0 // indirect |
|||
go.etcd.io/etcd/api/v3 v3.5.15 // indirect |
|||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect |
|||
go.etcd.io/etcd/client/v3 v3.5.15 // indirect |
|||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect |
|||
go.opentelemetry.io/otel v1.35.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect |
|||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect |
|||
go.opentelemetry.io/otel/metric v1.35.0 // indirect |
|||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect |
|||
go.opentelemetry.io/otel/trace v1.35.0 // indirect |
|||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect |
|||
go.uber.org/atomic v1.10.0 // indirect |
|||
go.uber.org/automaxprocs v1.6.0 // indirect |
|||
go.uber.org/multierr v1.9.0 // indirect |
|||
go.uber.org/zap v1.24.0 // indirect |
|||
golang.org/x/net v0.38.0 // indirect |
|||
golang.org/x/oauth2 v0.28.0 // indirect |
|||
golang.org/x/sys v0.31.0 // indirect |
|||
golang.org/x/term v0.30.0 // indirect |
|||
golang.org/x/text v0.27.0 // indirect |
|||
golang.org/x/time v0.10.0 // indirect |
|||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect |
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect |
|||
gopkg.in/inf.v0 v0.9.1 // indirect |
|||
gopkg.in/yaml.v2 v2.4.0 // indirect |
|||
gopkg.in/yaml.v3 v3.0.1 // indirect |
|||
k8s.io/api v0.29.3 // indirect |
|||
k8s.io/apimachinery v0.29.4 // indirect |
|||
k8s.io/client-go v0.29.3 // indirect |
|||
k8s.io/klog/v2 v2.110.1 // indirect |
|||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect |
|||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect |
|||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect |
|||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect |
|||
sigs.k8s.io/yaml v1.3.0 // indirect |
|||
) |
@ -0,0 +1,293 @@ |
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= |
|||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= |
|||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= |
|||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= |
|||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= |
|||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= |
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= |
|||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= |
|||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= |
|||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= |
|||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= |
|||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= |
|||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= |
|||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= |
|||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= |
|||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= |
|||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= |
|||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= |
|||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= |
|||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= |
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= |
|||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= |
|||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= |
|||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= |
|||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= |
|||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= |
|||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= |
|||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= |
|||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= |
|||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= |
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= |
|||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= |
|||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= |
|||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= |
|||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= |
|||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= |
|||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= |
|||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= |
|||
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= |
|||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= |
|||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= |
|||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= |
|||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= |
|||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= |
|||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= |
|||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= |
|||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= |
|||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= |
|||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= |
|||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= |
|||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= |
|||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= |
|||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= |
|||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= |
|||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
|||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= |
|||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
|||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= |
|||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= |
|||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= |
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
|||
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE= |
|||
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU= |
|||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= |
|||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= |
|||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= |
|||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= |
|||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= |
|||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= |
|||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= |
|||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= |
|||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= |
|||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
|||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= |
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= |
|||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= |
|||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= |
|||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= |
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= |
|||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= |
|||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= |
|||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= |
|||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= |
|||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= |
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= |
|||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= |
|||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= |
|||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= |
|||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= |
|||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= |
|||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= |
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
|||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= |
|||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= |
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
|||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= |
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= |
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= |
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= |
|||
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= |
|||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= |
|||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= |
|||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= |
|||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= |
|||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= |
|||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= |
|||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= |
|||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= |
|||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= |
|||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= |
|||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= |
|||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= |
|||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= |
|||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= |
|||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= |
|||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= |
|||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= |
|||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= |
|||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= |
|||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= |
|||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= |
|||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= |
|||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= |
|||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= |
|||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= |
|||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= |
|||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= |
|||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= |
|||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= |
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= |
|||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
|||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= |
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
|||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
|||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
|||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= |
|||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= |
|||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= |
|||
github.com/zeromicro/go-zero v1.8.4 h1:3s7kOoThCnkDoqCafsqSX58Y9osYTBIa5QEmomw07TE= |
|||
github.com/zeromicro/go-zero v1.8.4/go.mod h1:eM5f6If/RF+jG1wSCmlvfXD2h2l23vJwETI8oDpjYt4= |
|||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= |
|||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= |
|||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= |
|||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= |
|||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= |
|||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= |
|||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= |
|||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= |
|||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= |
|||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= |
|||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= |
|||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= |
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= |
|||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= |
|||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= |
|||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= |
|||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= |
|||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= |
|||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= |
|||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= |
|||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= |
|||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= |
|||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= |
|||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= |
|||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= |
|||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= |
|||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= |
|||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= |
|||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= |
|||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= |
|||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= |
|||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= |
|||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= |
|||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= |
|||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= |
|||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= |
|||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= |
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
|||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= |
|||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= |
|||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= |
|||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= |
|||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= |
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= |
|||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= |
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
|||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= |
|||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
|||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= |
|||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= |
|||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= |
|||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= |
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= |
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
|||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= |
|||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= |
|||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= |
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= |
|||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= |
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= |
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= |
|||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= |
|||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= |
|||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= |
|||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= |
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |
|||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= |
|||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= |
|||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= |
|||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= |
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= |
|||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= |
|||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= |
|||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= |
|||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= |
|||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= |
|||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= |
|||
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= |
|||
k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= |
|||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= |
|||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= |
|||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= |
|||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= |
|||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= |
|||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= |
|||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= |
|||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= |
|||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= |
|||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= |
|||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= |
|||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= |
|||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= |
|||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= |
@ -0,0 +1,24 @@ |
|||
# backend 目标 |
|||
|
|||
## 技术栈 |
|||
|
|||
- 框架:go-zero |
|||
- ORM:gorm |
|||
- 缓存:redis |
|||
- 日志:logx |
|||
- 工具:goctl |
|||
- 监控:prometheus |
|||
- 告警:alertmanager |
|||
- 文档:swagger |
|||
|
|||
## 相关服务 |
|||
|
|||
- 用户中心 |
|||
- 机构管理 |
|||
- 权限管理 |
|||
- 文件服务 |
|||
- 日志服务 |
|||
- 网关服务 |
|||
- 监控服务 |
|||
- 告警服务 |
|||
- 消息队列 |
@ -0,0 +1,192 @@ |
|||
# 用户中心 |
|||
|
|||
## 1. 功能模块 |
|||
|
|||
- 用户注册与登录 |
|||
|
|||
- 支持邮箱、手机号注册 |
|||
- 支持用户名/手机号/邮箱+密码登录 |
|||
- 支持验证码登录 |
|||
- 支持第三方登录(如微信、QQ、钉钉、GitHub 等) |
|||
|
|||
- 用户信息管理 |
|||
|
|||
- 个人信息查看与修改(昵称、头像、联系方式等) |
|||
- 密码修改 |
|||
- 绑定/解绑第三方账号 |
|||
- 实名认证(可选) |
|||
|
|||
- 权限与角色管理 |
|||
|
|||
- 用户分组与角色分配 |
|||
- 权限点管理 |
|||
- 角色权限分配 |
|||
- 支持多租户 |
|||
|
|||
- 安全与认证 |
|||
|
|||
- JWT Token 认证 |
|||
- Session 管理 |
|||
- 登录日志与安全审计 |
|||
- 登录设备管理 |
|||
- 密码找回与重置(邮箱/短信验证码) |
|||
|
|||
- 用户状态管理 |
|||
|
|||
- 启用/禁用用户 |
|||
- 注销/删除账号 |
|||
- 黑名单管理 |
|||
|
|||
- 通知与消息 |
|||
- 站内信 |
|||
- 邮件/短信通知 |
|||
- 消息推送 |
|||
|
|||
## 2. 数据库设计(简要) |
|||
|
|||
- 用户表(users) |
|||
- 用户扩展信息表(user_profiles) |
|||
- 角色表(roles) |
|||
- 权限表(permissions) |
|||
- 用户-角色关联表(user_roles) |
|||
- 角色-权限关联表(role_permissions) |
|||
- 第三方账号表(user_oauth) |
|||
- 登录日志表(login_logs) |
|||
|
|||
## 3. API 设计 |
|||
|
|||
### 用户注册与登录 |
|||
|
|||
- POST `/api/user/register` 用户注册 |
|||
- POST `/api/user/login` 用户登录 |
|||
- POST `/api/user/login/code` 验证码登录 |
|||
- POST `/api/user/login/oauth` 第三方登录 |
|||
- POST `/api/user/logout` 用户登出 |
|||
|
|||
### 用户信息管理 |
|||
|
|||
- GET `/api/user/profile` 获取个人信息 |
|||
- PUT `/api/user/profile` 修改个人信息 |
|||
- POST `/api/user/password/change` 修改密码 |
|||
- POST `/api/user/password/reset` 密码重置 |
|||
- POST `/api/user/bind_oauth` 绑定第三方账号 |
|||
- POST `/api/user/unbind_oauth` 解绑第三方账号 |
|||
- POST `/api/user/verify_identity` 实名认证 |
|||
|
|||
### 权限与角色管理 |
|||
|
|||
- GET `/api/user/roles` 获取当前用户角色 |
|||
- GET `/api/user/permissions` 获取当前用户权限点 |
|||
- GET `/api/roles` 获取所有角色列表 |
|||
- POST `/api/role` 新增角色 |
|||
- POST `/api/role/assign` 分配角色给用户 |
|||
- GET `/api/permissions` 获取所有权限点 |
|||
- POST `/api/permission` 新增权限点 |
|||
- POST `/api/role/permission/assign` 分配权限点给角色 |
|||
|
|||
### 安全与认证 |
|||
|
|||
- GET `/api/user/login_logs` 获取登录日志 |
|||
- GET `/api/user/devices` 获取登录设备列表 |
|||
- POST `/api/user/device/logout` 注销指定设备 |
|||
- POST `/api/user/blacklist/add` 加入黑名单 |
|||
- POST `/api/user/blacklist/remove` 移除黑名单 |
|||
|
|||
### 用户状态管理 |
|||
|
|||
- POST `/api/user/enable` 启用用户 |
|||
- POST `/api/user/disable` 禁用用户 |
|||
- POST `/api/user/delete` 注销/删除账号 |
|||
|
|||
### 通知与消息 |
|||
|
|||
- GET `/api/user/messages` 获取站内信列表 |
|||
- POST `/api/user/message/read` 标记消息为已读 |
|||
- GET `/api/user/notifications` 获取通知列表 |
|||
|
|||
--- |
|||
|
|||
### 多租户支持(Multi-Tenancy) |
|||
|
|||
#### 1. 租户管理 |
|||
|
|||
- GET `/api/tenants` |
|||
租户列表(分页、条件查询) |
|||
|
|||
- POST `/api/tenants` |
|||
新增租户 |
|||
|
|||
- GET `/api/tenants/{id}` |
|||
获取租户详情 |
|||
|
|||
- POST `/api/tenants/{id}/update` |
|||
修改租户信息 |
|||
|
|||
- POST `/api/tenants/{id}/delete` |
|||
删除租户 |
|||
|
|||
- POST `/api/tenants/import` |
|||
批量导入租户 |
|||
|
|||
- GET `/api/tenants/export` |
|||
租户数据导出 |
|||
|
|||
--- |
|||
|
|||
#### 2. 租户下的用户与资源 |
|||
|
|||
- GET `/api/tenants/{tenant_id}/users` |
|||
获取租户下用户列表(分页) |
|||
|
|||
- POST `/api/tenants/{tenant_id}/users/batch` |
|||
批量新增租户用户 |
|||
|
|||
- GET `/api/tenants/{tenant_id}/roles` |
|||
获取租户下角色列表 |
|||
|
|||
- POST `/api/tenants/{tenant_id}/roles/batch` |
|||
批量新增租户角色 |
|||
|
|||
- GET `/api/tenants/{tenant_id}/permissions` |
|||
获取租户下权限点列表 |
|||
|
|||
- 其他资源(如机构、部门、日志等)均可按 `/api/tenants/{tenant_id}/resource` 方式设计 |
|||
|
|||
--- |
|||
|
|||
#### 3. 用户与租户关系 |
|||
|
|||
- GET `/api/users/{id}/tenants` |
|||
获取用户所属租户列表 |
|||
|
|||
- POST `/api/users/{id}/tenants/assign` |
|||
分配用户到租户 |
|||
Body: 租户 ID 数组 |
|||
|
|||
--- |
|||
|
|||
#### 4. 说明 |
|||
|
|||
- 所有与租户相关的资源操作均以 `/api/tenants/{tenant_id}/...` 作为前缀,确保数据隔离。 |
|||
- 超级管理员可管理所有租户,租户管理员仅能管理本租户下资源。 |
|||
- 用户登录后,需带上当前租户 ID(如 Header: `X-Tenant-Id`)进行资源访问。 |
|||
|
|||
如需继续细化某一模块或添加其他接口,请随时告知! |
|||
|
|||
## 4. 安全与合规 |
|||
|
|||
- 密码加密存储(如 bcrypt) |
|||
- 防止暴力破解(登录限流、验证码) |
|||
- 敏感操作二次验证 |
|||
- 数据合规(如 GDPR,用户可导出/删除个人数据) |
|||
|
|||
## 5. 其他建议 |
|||
|
|||
- 支持多语言 |
|||
- 支持多终端(Web、App、小程序) |
|||
- 详细的接口文档(Swagger/OpenAPI) |
|||
- 单元测试与接口测试覆盖 |
|||
|
|||
--- |
|||
|
|||
如需详细到每个接口的参数、返回值、流程图或数据库表结构设计,请告知! |
@ -0,0 +1,7 @@ |
|||
package main |
|||
|
|||
import "backend/usercenter/orm" |
|||
|
|||
func main() { |
|||
orm.AutoMigrate() |
|||
} |
@ -0,0 +1,15 @@ |
|||
Name: usercenter.rpc |
|||
ListenOn: 0.0.0.0:8080 |
|||
Etcd: |
|||
Hosts: |
|||
- 127.0.0.1:2379 |
|||
Key: usercenter.rpc |
|||
MySQL: |
|||
Host: 127.0.0.1 |
|||
Port: 3306 |
|||
User: root |
|||
Password: root |
|||
DBName: usercenter |
|||
MaxIdleConns: 10 |
|||
MaxOpenConns: 100 |
|||
|
@ -0,0 +1,28 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
|
|||
"github.com/zeromicro/go-zero/zrpc" |
|||
) |
|||
|
|||
// MySQL 数据库配置
|
|||
type MySQLConf struct { |
|||
Host string |
|||
Port int |
|||
User string |
|||
Password string |
|||
DBName string |
|||
MaxIdleConns int |
|||
MaxOpenConns int |
|||
} |
|||
|
|||
func (m MySQLConf) DSN() string { |
|||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", |
|||
m.User, m.Password, m.Host, m.Port, m.DBName) |
|||
} |
|||
|
|||
type Config struct { |
|||
zrpc.RpcServerConf |
|||
MySQL MySQLConf |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type ChangePasswordLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewChangePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChangePasswordLogic { |
|||
return &ChangePasswordLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *ChangePasswordLogic) ChangePassword(in *usercenter.ChangePasswordRequest) (*usercenter.ChangePasswordResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.ChangePasswordResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type GetProfileLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewGetProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProfileLogic { |
|||
return &GetProfileLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *GetProfileLogic) GetProfile(in *usercenter.GetProfileRequest) (*usercenter.GetProfileResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.GetProfileResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type GetUserPermissionsLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewGetUserPermissionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserPermissionsLogic { |
|||
return &GetUserPermissionsLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *GetUserPermissionsLogic) GetUserPermissions(in *usercenter.Request) (*usercenter.GetUserPermissionsResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.GetUserPermissionsResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type GetUserRolesLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewGetUserRolesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserRolesLogic { |
|||
return &GetUserRolesLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *GetUserRolesLogic) GetUserRoles(in *usercenter.Request) (*usercenter.GetUserRolesResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.GetUserRolesResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type LoginLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { |
|||
return &LoginLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *LoginLogic) Login(in *usercenter.LoginRequest) (*usercenter.LoginResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.LoginResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type PingLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic { |
|||
return &PingLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *PingLogic) Ping(in *usercenter.Request) (*usercenter.Response, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.Response{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type RegisterLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { |
|||
return &RegisterLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *RegisterLogic) Register(in *usercenter.RegisterRequest) (*usercenter.RegisterResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.RegisterResponse{}, nil |
|||
} |
@ -0,0 +1,30 @@ |
|||
package logic |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type UpdateProfileLogic struct { |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
logx.Logger |
|||
} |
|||
|
|||
func NewUpdateProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProfileLogic { |
|||
return &UpdateProfileLogic{ |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
Logger: logx.WithContext(ctx), |
|||
} |
|||
} |
|||
|
|||
func (l *UpdateProfileLogic) UpdateProfile(in *usercenter.UpdateProfileRequest) (*usercenter.UpdateProfileResponse, error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return &usercenter.UpdateProfileResponse{}, nil |
|||
} |
@ -0,0 +1,64 @@ |
|||
// Code generated by goctl. DO NOT EDIT.
|
|||
// goctl 1.8.4
|
|||
// Source: usercenter.proto
|
|||
|
|||
package server |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/internal/logic" |
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
) |
|||
|
|||
type UsercenterServer struct { |
|||
svcCtx *svc.ServiceContext |
|||
usercenter.UnimplementedUsercenterServer |
|||
} |
|||
|
|||
func NewUsercenterServer(svcCtx *svc.ServiceContext) *UsercenterServer { |
|||
return &UsercenterServer{ |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (s *UsercenterServer) Ping(ctx context.Context, in *usercenter.Request) (*usercenter.Response, error) { |
|||
l := logic.NewPingLogic(ctx, s.svcCtx) |
|||
return l.Ping(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) Register(ctx context.Context, in *usercenter.RegisterRequest) (*usercenter.RegisterResponse, error) { |
|||
l := logic.NewRegisterLogic(ctx, s.svcCtx) |
|||
return l.Register(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) Login(ctx context.Context, in *usercenter.LoginRequest) (*usercenter.LoginResponse, error) { |
|||
l := logic.NewLoginLogic(ctx, s.svcCtx) |
|||
return l.Login(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) GetProfile(ctx context.Context, in *usercenter.GetProfileRequest) (*usercenter.GetProfileResponse, error) { |
|||
l := logic.NewGetProfileLogic(ctx, s.svcCtx) |
|||
return l.GetProfile(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) UpdateProfile(ctx context.Context, in *usercenter.UpdateProfileRequest) (*usercenter.UpdateProfileResponse, error) { |
|||
l := logic.NewUpdateProfileLogic(ctx, s.svcCtx) |
|||
return l.UpdateProfile(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) ChangePassword(ctx context.Context, in *usercenter.ChangePasswordRequest) (*usercenter.ChangePasswordResponse, error) { |
|||
l := logic.NewChangePasswordLogic(ctx, s.svcCtx) |
|||
return l.ChangePassword(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) GetUserRoles(ctx context.Context, in *usercenter.Request) (*usercenter.GetUserRolesResponse, error) { |
|||
l := logic.NewGetUserRolesLogic(ctx, s.svcCtx) |
|||
return l.GetUserRoles(in) |
|||
} |
|||
|
|||
func (s *UsercenterServer) GetUserPermissions(ctx context.Context, in *usercenter.Request) (*usercenter.GetUserPermissionsResponse, error) { |
|||
l := logic.NewGetUserPermissionsLogic(ctx, s.svcCtx) |
|||
return l.GetUserPermissions(in) |
|||
} |
@ -0,0 +1,13 @@ |
|||
package svc |
|||
|
|||
import "backend/usercenter/internal/config" |
|||
|
|||
type ServiceContext struct { |
|||
Config config.Config |
|||
} |
|||
|
|||
func NewServiceContext(c config.Config) *ServiceContext { |
|||
return &ServiceContext{ |
|||
Config: c, |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
// GORM 自动迁移, 创建表
|
|||
// 1.读取 etc/usercenter.yaml 文件, 获取配置,连接数据库
|
|||
// 2.遍历 orm 包下的所有文件, 获取所有表名,自动迁移
|
|||
|
|||
// 1.读取 etc/usercenter.yaml 文件, 获取配置,连接数据库
|
|||
package orm |
|||
|
|||
import ( |
|||
"backend/usercenter/internal/config" |
|||
"flag" |
|||
"fmt" |
|||
|
|||
"github.com/zeromicro/go-zero/core/conf" |
|||
"gorm.io/driver/mysql" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
var configFile = flag.String("f", "etc/usercenter.yaml", "the config file") |
|||
|
|||
func AutoMigrate() { |
|||
flag.Parse() |
|||
var c config.Config |
|||
conf.MustLoad(*configFile, &c) |
|||
db, err := gorm.Open(mysql.Open(c.MySQL.DSN()), &gorm.Config{}) |
|||
if err != nil { |
|||
panic(err) |
|||
} |
|||
// 自动迁移 User 表
|
|||
fmt.Println("迁移User表") |
|||
if err := db.AutoMigrate(&User{}); err != nil { |
|||
panic(err) |
|||
} |
|||
fmt.Println("迁移User表完成") |
|||
// 迁移完成,打印信息
|
|||
fmt.Println("全部迁移完成") |
|||
} |
@ -0,0 +1,12 @@ |
|||
package orm |
|||
|
|||
import "time" |
|||
|
|||
type BaseModel struct { |
|||
CreatedAt time.Time `gorm:"autoCreateTime:milli"` |
|||
UpdatedAt time.Time `gorm:"autoUpdateTime:milli"` |
|||
DeletedAt time.Time `gorm:"index"` |
|||
CreatedBy string |
|||
UpdatedBy string |
|||
DeletedBy string |
|||
} |
@ -0,0 +1,22 @@ |
|||
package orm |
|||
|
|||
// 用户表
|
|||
import ( |
|||
"time" |
|||
) |
|||
|
|||
type User struct { |
|||
ID uint `gorm:"primaryKey"` |
|||
Name string // 用户名
|
|||
Email string // 邮箱
|
|||
Age uint8 // 年龄
|
|||
Birthday time.Time // 生日
|
|||
Phone string // 手机号
|
|||
Password string // 密码
|
|||
Status int // 状态
|
|||
Role int // 角色
|
|||
Avatar string // 头像
|
|||
Introduction string // 简介
|
|||
Department string // 部门
|
|||
BaseModel // 基础模型 包含CreatedAt、UpdatedAt、DeletedAt、CreatedBy、UpdatedBy、DeletedBy
|
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,357 @@ |
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
|||
// versions:
|
|||
// - protoc-gen-go-grpc v1.2.0
|
|||
// - protoc v3.20.3
|
|||
// source: usercenter.proto
|
|||
|
|||
package usercenter |
|||
|
|||
import ( |
|||
context "context" |
|||
grpc "google.golang.org/grpc" |
|||
codes "google.golang.org/grpc/codes" |
|||
status "google.golang.org/grpc/status" |
|||
) |
|||
|
|||
// This is a compile-time assertion to ensure that this generated file
|
|||
// is compatible with the grpc package it is being compiled against.
|
|||
// Requires gRPC-Go v1.32.0 or later.
|
|||
const _ = grpc.SupportPackageIsVersion7 |
|||
|
|||
// UsercenterClient is the client API for Usercenter service.
|
|||
//
|
|||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
|||
type UsercenterClient interface { |
|||
Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) |
|||
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) |
|||
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) |
|||
GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) |
|||
UpdateProfile(ctx context.Context, in *UpdateProfileRequest, opts ...grpc.CallOption) (*UpdateProfileResponse, error) |
|||
ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*ChangePasswordResponse, error) |
|||
GetUserRoles(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserRolesResponse, error) |
|||
GetUserPermissions(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserPermissionsResponse, error) |
|||
} |
|||
|
|||
type usercenterClient struct { |
|||
cc grpc.ClientConnInterface |
|||
} |
|||
|
|||
func NewUsercenterClient(cc grpc.ClientConnInterface) UsercenterClient { |
|||
return &usercenterClient{cc} |
|||
} |
|||
|
|||
func (c *usercenterClient) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { |
|||
out := new(Response) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/Ping", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { |
|||
out := new(RegisterResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/Register", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { |
|||
out := new(LoginResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/Login", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) { |
|||
out := new(GetProfileResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/GetProfile", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) UpdateProfile(ctx context.Context, in *UpdateProfileRequest, opts ...grpc.CallOption) (*UpdateProfileResponse, error) { |
|||
out := new(UpdateProfileResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/UpdateProfile", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*ChangePasswordResponse, error) { |
|||
out := new(ChangePasswordResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/ChangePassword", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) GetUserRoles(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserRolesResponse, error) { |
|||
out := new(GetUserRolesResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/GetUserRoles", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
func (c *usercenterClient) GetUserPermissions(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserPermissionsResponse, error) { |
|||
out := new(GetUserPermissionsResponse) |
|||
err := c.cc.Invoke(ctx, "/usercenter.Usercenter/GetUserPermissions", in, out, opts...) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return out, nil |
|||
} |
|||
|
|||
// UsercenterServer is the server API for Usercenter service.
|
|||
// All implementations must embed UnimplementedUsercenterServer
|
|||
// for forward compatibility
|
|||
type UsercenterServer interface { |
|||
Ping(context.Context, *Request) (*Response, error) |
|||
Register(context.Context, *RegisterRequest) (*RegisterResponse, error) |
|||
Login(context.Context, *LoginRequest) (*LoginResponse, error) |
|||
GetProfile(context.Context, *GetProfileRequest) (*GetProfileResponse, error) |
|||
UpdateProfile(context.Context, *UpdateProfileRequest) (*UpdateProfileResponse, error) |
|||
ChangePassword(context.Context, *ChangePasswordRequest) (*ChangePasswordResponse, error) |
|||
GetUserRoles(context.Context, *Request) (*GetUserRolesResponse, error) |
|||
GetUserPermissions(context.Context, *Request) (*GetUserPermissionsResponse, error) |
|||
mustEmbedUnimplementedUsercenterServer() |
|||
} |
|||
|
|||
// UnimplementedUsercenterServer must be embedded to have forward compatible implementations.
|
|||
type UnimplementedUsercenterServer struct { |
|||
} |
|||
|
|||
func (UnimplementedUsercenterServer) Ping(context.Context, *Request) (*Response, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) GetProfile(context.Context, *GetProfileRequest) (*GetProfileResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method GetProfile not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) UpdateProfile(context.Context, *UpdateProfileRequest) (*UpdateProfileResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method UpdateProfile not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) ChangePassword(context.Context, *ChangePasswordRequest) (*ChangePasswordResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method ChangePassword not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) GetUserRoles(context.Context, *Request) (*GetUserRolesResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoles not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) GetUserPermissions(context.Context, *Request) (*GetUserPermissionsResponse, error) { |
|||
return nil, status.Errorf(codes.Unimplemented, "method GetUserPermissions not implemented") |
|||
} |
|||
func (UnimplementedUsercenterServer) mustEmbedUnimplementedUsercenterServer() {} |
|||
|
|||
// UnsafeUsercenterServer may be embedded to opt out of forward compatibility for this service.
|
|||
// Use of this interface is not recommended, as added methods to UsercenterServer will
|
|||
// result in compilation errors.
|
|||
type UnsafeUsercenterServer interface { |
|||
mustEmbedUnimplementedUsercenterServer() |
|||
} |
|||
|
|||
func RegisterUsercenterServer(s grpc.ServiceRegistrar, srv UsercenterServer) { |
|||
s.RegisterService(&Usercenter_ServiceDesc, srv) |
|||
} |
|||
|
|||
func _Usercenter_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(Request) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).Ping(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/Ping", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).Ping(ctx, req.(*Request)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(RegisterRequest) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).Register(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/Register", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).Register(ctx, req.(*RegisterRequest)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(LoginRequest) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).Login(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/Login", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).Login(ctx, req.(*LoginRequest)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_GetProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(GetProfileRequest) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).GetProfile(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/GetProfile", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).GetProfile(ctx, req.(*GetProfileRequest)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_UpdateProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(UpdateProfileRequest) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).UpdateProfile(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/UpdateProfile", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).UpdateProfile(ctx, req.(*UpdateProfileRequest)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_ChangePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(ChangePasswordRequest) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).ChangePassword(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/ChangePassword", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).ChangePassword(ctx, req.(*ChangePasswordRequest)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_GetUserRoles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(Request) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).GetUserRoles(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/GetUserRoles", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).GetUserRoles(ctx, req.(*Request)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
func _Usercenter_GetUserPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { |
|||
in := new(Request) |
|||
if err := dec(in); err != nil { |
|||
return nil, err |
|||
} |
|||
if interceptor == nil { |
|||
return srv.(UsercenterServer).GetUserPermissions(ctx, in) |
|||
} |
|||
info := &grpc.UnaryServerInfo{ |
|||
Server: srv, |
|||
FullMethod: "/usercenter.Usercenter/GetUserPermissions", |
|||
} |
|||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { |
|||
return srv.(UsercenterServer).GetUserPermissions(ctx, req.(*Request)) |
|||
} |
|||
return interceptor(ctx, in, info, handler) |
|||
} |
|||
|
|||
// Usercenter_ServiceDesc is the grpc.ServiceDesc for Usercenter service.
|
|||
// It's only intended for direct use with grpc.RegisterService,
|
|||
// and not to be introspected or modified (even as a copy)
|
|||
var Usercenter_ServiceDesc = grpc.ServiceDesc{ |
|||
ServiceName: "usercenter.Usercenter", |
|||
HandlerType: (*UsercenterServer)(nil), |
|||
Methods: []grpc.MethodDesc{ |
|||
{ |
|||
MethodName: "Ping", |
|||
Handler: _Usercenter_Ping_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "Register", |
|||
Handler: _Usercenter_Register_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "Login", |
|||
Handler: _Usercenter_Login_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "GetProfile", |
|||
Handler: _Usercenter_GetProfile_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "UpdateProfile", |
|||
Handler: _Usercenter_UpdateProfile_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "ChangePassword", |
|||
Handler: _Usercenter_ChangePassword_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "GetUserRoles", |
|||
Handler: _Usercenter_GetUserRoles_Handler, |
|||
}, |
|||
{ |
|||
MethodName: "GetUserPermissions", |
|||
Handler: _Usercenter_GetUserPermissions_Handler, |
|||
}, |
|||
}, |
|||
Streams: []grpc.StreamDesc{}, |
|||
Metadata: "usercenter.proto", |
|||
} |
Binary file not shown.
@ -0,0 +1,2 @@ |
|||
file appendonly.aof.1.base.rdb seq 1 type b |
|||
file appendonly.aof.1.incr.aof seq 1 type i startoffset 0 endoffset 0 |
Binary file not shown.
@ -0,0 +1,39 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"flag" |
|||
"fmt" |
|||
|
|||
"backend/usercenter/internal/config" |
|||
"backend/usercenter/internal/server" |
|||
"backend/usercenter/internal/svc" |
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/core/conf" |
|||
"github.com/zeromicro/go-zero/core/service" |
|||
"github.com/zeromicro/go-zero/zrpc" |
|||
"google.golang.org/grpc" |
|||
"google.golang.org/grpc/reflection" |
|||
) |
|||
|
|||
var configFile = flag.String("f", "etc/usercenter.yaml", "the config file") |
|||
|
|||
func main() { |
|||
flag.Parse() |
|||
|
|||
var c config.Config |
|||
conf.MustLoad(*configFile, &c) |
|||
ctx := svc.NewServiceContext(c) |
|||
|
|||
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { |
|||
usercenter.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx)) |
|||
|
|||
if c.Mode == service.DevMode || c.Mode == service.TestMode { |
|||
reflection.Register(grpcServer) |
|||
} |
|||
}) |
|||
defer s.Stop() |
|||
|
|||
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) |
|||
s.Start() |
|||
} |
@ -0,0 +1,109 @@ |
|||
syntax = "proto3"; |
|||
|
|||
package usercenter; |
|||
option go_package = "backend/usercenter"; |
|||
|
|||
message Request { string ping = 1; } |
|||
|
|||
message Response { string pong = 1; } |
|||
|
|||
// 用户注册请求 |
|||
message RegisterRequest { |
|||
string username = 1; |
|||
string password = 2; |
|||
string email = 3; |
|||
string mobile = 4; |
|||
} |
|||
|
|||
// 用户注册响应 |
|||
message RegisterResponse { |
|||
string user_id = 1; |
|||
string message = 2; |
|||
} |
|||
|
|||
// 用户登录请求 |
|||
message LoginRequest { |
|||
string identity = 1; // 用户名/邮箱/手机号 |
|||
string password = 2; |
|||
} |
|||
|
|||
// 用户登录响应 |
|||
message LoginResponse { |
|||
string user_id = 1; |
|||
string token = 2; |
|||
string refresh_token = 3; |
|||
string message = 4; |
|||
} |
|||
|
|||
// 获取用户信息请求 |
|||
message GetProfileRequest { string user_id = 1; } |
|||
|
|||
// 用户信息 |
|||
message UserProfile { |
|||
string user_id = 1; |
|||
string username = 2; |
|||
string nickname = 3; |
|||
string email = 4; |
|||
string mobile = 5; |
|||
string avatar = 6; |
|||
string gender = 7; |
|||
string status = 8; |
|||
} |
|||
|
|||
// 获取用户信息响应 |
|||
message GetProfileResponse { UserProfile profile = 1; } |
|||
|
|||
// 修改用户信息请求 |
|||
message UpdateProfileRequest { |
|||
string user_id = 1; |
|||
string nickname = 2; |
|||
string avatar = 3; |
|||
string email = 4; |
|||
string mobile = 5; |
|||
string gender = 6; |
|||
} |
|||
|
|||
// 修改用户信息响应 |
|||
message UpdateProfileResponse { string message = 1; } |
|||
|
|||
// 修改密码请求 |
|||
message ChangePasswordRequest { |
|||
string user_id = 1; |
|||
string old_password = 2; |
|||
string new_password = 3; |
|||
} |
|||
|
|||
// 修改密码响应 |
|||
message ChangePasswordResponse { string message = 1; } |
|||
|
|||
// 角色信息 |
|||
message Role { |
|||
string role_id = 1; |
|||
string name = 2; |
|||
string desc = 3; |
|||
} |
|||
|
|||
// 权限信息 |
|||
message Permission { |
|||
string permission_id = 1; |
|||
string name = 2; |
|||
string desc = 3; |
|||
} |
|||
|
|||
// 获取当前用户角色响应 |
|||
message GetUserRolesResponse { repeated Role roles = 1; } |
|||
|
|||
// 获取当前用户权限响应 |
|||
message GetUserPermissionsResponse { repeated Permission permissions = 1; } |
|||
|
|||
// 用户中心服务 |
|||
service Usercenter { |
|||
rpc Ping(Request) returns (Response); |
|||
rpc Register(RegisterRequest) returns (RegisterResponse); |
|||
rpc Login(LoginRequest) returns (LoginResponse); |
|||
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse); |
|||
rpc UpdateProfile(UpdateProfileRequest) returns (UpdateProfileResponse); |
|||
rpc ChangePassword(ChangePasswordRequest) returns (ChangePasswordResponse); |
|||
rpc GetUserRoles(Request) returns (GetUserRolesResponse); |
|||
rpc GetUserPermissions(Request) returns (GetUserPermissionsResponse); |
|||
} |
@ -0,0 +1,95 @@ |
|||
// Code generated by goctl. DO NOT EDIT.
|
|||
// goctl 1.8.4
|
|||
// Source: usercenter.proto
|
|||
|
|||
package usercenterclient |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"backend/usercenter/pb/usercenter" |
|||
|
|||
"github.com/zeromicro/go-zero/zrpc" |
|||
"google.golang.org/grpc" |
|||
) |
|||
|
|||
type ( |
|||
ChangePasswordRequest = usercenter.ChangePasswordRequest |
|||
ChangePasswordResponse = usercenter.ChangePasswordResponse |
|||
GetProfileRequest = usercenter.GetProfileRequest |
|||
GetProfileResponse = usercenter.GetProfileResponse |
|||
GetUserPermissionsResponse = usercenter.GetUserPermissionsResponse |
|||
GetUserRolesResponse = usercenter.GetUserRolesResponse |
|||
LoginRequest = usercenter.LoginRequest |
|||
LoginResponse = usercenter.LoginResponse |
|||
Permission = usercenter.Permission |
|||
RegisterRequest = usercenter.RegisterRequest |
|||
RegisterResponse = usercenter.RegisterResponse |
|||
Request = usercenter.Request |
|||
Response = usercenter.Response |
|||
Role = usercenter.Role |
|||
UpdateProfileRequest = usercenter.UpdateProfileRequest |
|||
UpdateProfileResponse = usercenter.UpdateProfileResponse |
|||
UserProfile = usercenter.UserProfile |
|||
|
|||
Usercenter interface { |
|||
Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) |
|||
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) |
|||
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) |
|||
GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) |
|||
UpdateProfile(ctx context.Context, in *UpdateProfileRequest, opts ...grpc.CallOption) (*UpdateProfileResponse, error) |
|||
ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*ChangePasswordResponse, error) |
|||
GetUserRoles(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserRolesResponse, error) |
|||
GetUserPermissions(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserPermissionsResponse, error) |
|||
} |
|||
|
|||
defaultUsercenter struct { |
|||
cli zrpc.Client |
|||
} |
|||
) |
|||
|
|||
func NewUsercenter(cli zrpc.Client) Usercenter { |
|||
return &defaultUsercenter{ |
|||
cli: cli, |
|||
} |
|||
} |
|||
|
|||
func (m *defaultUsercenter) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.Ping(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.Register(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.Login(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.GetProfile(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) UpdateProfile(ctx context.Context, in *UpdateProfileRequest, opts ...grpc.CallOption) (*UpdateProfileResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.UpdateProfile(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*ChangePasswordResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.ChangePassword(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) GetUserRoles(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserRolesResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.GetUserRoles(ctx, in, opts...) |
|||
} |
|||
|
|||
func (m *defaultUsercenter) GetUserPermissions(ctx context.Context, in *Request, opts ...grpc.CallOption) (*GetUserPermissionsResponse, error) { |
|||
client := usercenter.NewUsercenterClient(m.cli.Conn()) |
|||
return client.GetUserPermissions(ctx, in, opts...) |
|||
} |
@ -0,0 +1,29 @@ |
|||
# 全局运行环境, docker-compose 文件 |
|||
## mysql 数据库 |
|||
version: "3.8" |
|||
services: |
|||
mysql: |
|||
image: mysql:8.0 |
|||
ports: |
|||
- 3306:3306 |
|||
environment: |
|||
MYSQL_ROOT_PASSWORD: root |
|||
MYSQL_DATABASE: usercenter |
|||
## redis 缓存 |
|||
redis: |
|||
image: redis:latest |
|||
ports: |
|||
- 6379:6379 |
|||
volumes: |
|||
- ./redis/data:/data |
|||
command: redis-server --appendonly yes |
|||
## etcd 注册中心 |
|||
etcd: |
|||
image: bitnami/etcd:latest |
|||
ports: |
|||
- 2379:2379 |
|||
- 2380:2380 |
|||
volumes: |
|||
- ./etcd/data:/bitnami |
|||
environment: |
|||
ALLOW_NONE_AUTHENTICATION: "yes" |
Loading…
Reference in new issue